Kaynağa Gözat

rename on blur

Yuki Takei 1 yıl önce
ebeveyn
işleme
ab6f14a48c

+ 28 - 33
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -1,6 +1,6 @@
 import type { FC } from 'react';
 import React, {
-  memo, useEffect, useRef, useState,
+  memo, useCallback, useEffect, useRef, useState,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -9,90 +9,85 @@ import AutosizeInput from 'react-input-autosize';
 import type { AlertInfo } from '~/client/util/input-validator';
 import { AlertType, inputValidator } from '~/client/util/input-validator';
 
-type ClosableTextInputProps = {
+export type ClosableTextInputProps = {
   value?: string
   placeholder?: string
   validationTarget?: string,
   useAutosizeInput?: boolean
   inputClassName?: string,
-  onPressEnter?(inputText: string | null): void
-  onPressEscape?: () => void
-  onClickOutside?(): void
+  onPressEnter?(inputText: string): void
+  onPressEscape?(inputText: string): void
+  onBlur?(inputText: string): void
   onChange?(inputText: string): void
 }
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
   const { t } = useTranslation();
-  const { validationTarget } = props;
+  const {
+    validationTarget, onPressEnter, onPressEscape, onBlur, onChange,
+  } = props;
 
   const inputRef = useRef<HTMLInputElement>(null);
-  const [inputText, setInputText] = useState(props.value);
+  const [inputText, setInputText] = useState(props.value ?? '');
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
   const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
   const [isComposing, setComposing] = useState(false);
 
 
-  const createValidation = async(inputText: string) => {
+  const createValidation = useCallback(async(inputText: string) => {
     const alertInfo = await inputValidator(inputText, validationTarget);
     if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
       alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
     }
     setAlertInfo(alertInfo);
-  };
+  }, [t, validationTarget]);
 
-  const onChangeHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
+  const changeHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
     const inputText = e.target.value;
     createValidation(inputText);
     setInputText(inputText);
     setIsAbleToShowAlert(true);
 
-    props.onChange?.(inputText);
-  };
+    onChange?.(inputText);
+  }, [createValidation, onChange]);
 
-  const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
+  const onFocusHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
     const inputText = e.target.value;
     await createValidation(inputText);
-  };
+  }, [createValidation]);
 
-  const onPressEnter = () => {
-    if (props.onPressEnter != null) {
-      const text = inputText != null ? inputText.trim() : null;
-      if (currentAlertInfo == null) {
-        props.onPressEnter(text);
-      }
+  const pressEnterHandler = useCallback(() => {
+    if (currentAlertInfo == null) {
+      onPressEnter?.(inputText.trim());
     }
-  };
+  }, [currentAlertInfo, inputText, onPressEnter]);
 
-  const onKeyDownHandler = (e) => {
+  const onKeyDownHandler = useCallback((e) => {
     switch (e.key) {
       case 'Enter':
         // Do nothing when composing
         if (isComposing) {
           return;
         }
-        onPressEnter();
+        pressEnterHandler();
         break;
       case 'Escape':
         if (isComposing) {
           return;
         }
-        props.onPressEscape?.();
+        onPressEscape?.(inputText.trim());
         break;
       default:
         break;
     }
-  };
+  }, [inputText, isComposing, pressEnterHandler, onPressEscape]);
 
   /*
    * Hide when click outside the ref
    */
-  const onBlurHandler = () => {
-    if (props.onClickOutside == null) {
-      return;
-    }
-
-    props.onClickOutside();
-  };
+  const onBlurHandler = useCallback(() => {
+    onBlur?.(inputText.trim());
+  }, [inputText, onBlur]);
 
   // didMount
   useEffect(() => {
@@ -126,7 +121,7 @@ const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextI
     placeholder: props.placeholder,
     name: 'input',
     onFocus: onFocusHandler,
-    onChange: onChangeHandler,
+    onChange: changeHandler,
     onKeyDown: onKeyDownHandler,
     onCompositionStart: () => setComposing(true),
     onCompositionEnd: () => setComposing(false),

+ 18 - 27
apps/app/src/components/PageHeader/PagePathHeader.tsx

@@ -41,7 +41,6 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {
 
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   const [isHover, setHover] = useState(false);
-  const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
 
   // const [isIconHidden, setIsIconHidden] = useState(false);
 
@@ -50,35 +49,28 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {
 
   const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
 
-  const onRenameFinish = useCallback(() => {
-    setRenameInputShown(false);
-    onRenameTerminated?.();
-  }, [onRenameTerminated]);
-
-  const onRenameFailure = useCallback(() => {
-    setRenameInputShown(true);
-  }, []);
 
-  const onInputChange = useCallback((inputText: string) => {
-    setEditingParentPagePath(inputText);
-  }, []);
-
-  const onPressEnter = useCallback(() => {
-    const pathToRename = normalizePath(`${editingParentPagePath}/${dPagePath.latter}`);
-    pagePathRenameHandler(pathToRename, onRenameFinish, onRenameFailure);
-  }, [editingParentPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler, dPagePath.latter]);
+  const rename = useCallback((inputText) => {
+    const pathToRename = normalizePath(`${inputText}/${dPagePath.latter}`);
+    pagePathRenameHandler(pathToRename,
+      () => {
+        setRenameInputShown(false);
+        onRenameTerminated?.();
+      },
+      () => {
+        setRenameInputShown(true);
+      });
+  }, [dPagePath.latter, pagePathRenameHandler, onRenameTerminated]);
 
-  const onPressEscape = useCallback(() => {
+  const cancel = useCallback(() => {
     // reset
-    setEditingParentPagePath(parentPagePath);
     setRenameInputShown(false);
-  }, [parentPagePath]);
+  }, []);
 
   const onClickEditButton = useCallback(() => {
     // reset
-    setEditingParentPagePath(parentPagePath);
     setRenameInputShown(true);
-  }, [parentPagePath]);
+  }, []);
 
   // TODO: https://redmine.weseek.co.jp/issues/141062
   // Truncate left side and don't use getElementById
@@ -119,14 +111,13 @@ export const PagePathHeader = memo((props: Props): JSX.Element => {
           <div className="position-relative">
             <div className="position-absolute w-100">
               <ClosableTextInput
-                value={editingParentPagePath}
+                value={parentPagePath}
                 placeholder={t('Input parent page path')}
                 inputClassName="form-control-sm"
-                onPressEnter={onPressEnter}
-                onPressEscape={onPressEscape}
-                onChange={onInputChange}
+                onPressEnter={rename}
+                onPressEscape={cancel}
+                onBlur={rename}
                 validationTarget={ValidationTarget.PAGE}
-                onClickOutside={onPressEscape}
                 useAutosizeInput
               />
             </div>

+ 17 - 19
apps/app/src/components/PageHeader/PageTitleHeader.tsx

@@ -51,16 +51,7 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
 
   const isNewlyCreatedPage = (currentPage.wip && currentPage.latestRevision == null && untitledPageRegex.test(editedPageTitle)) ?? false;
 
-  const onRenameFinish = useCallback(() => {
-    setRenameInputShown(false);
-    onMoveTerminated?.();
-  }, [onMoveTerminated]);
-
-  const onRenameFailure = useCallback(() => {
-    setRenameInputShown(true);
-  }, []);
-
-  const onInputChange = useCallback((inputText: string) => {
+  const inputChangeHandler = useCallback((inputText: string) => {
     const newPageTitle = pathUtils.removeHeadingSlash(inputText);
     const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
     const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
@@ -68,11 +59,18 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
     setEditedPagePath(newPagePath);
   }, [currentPage?.path, setEditedPagePath]);
 
-  const onPressEnter = useCallback(() => {
-    pagePathRenameHandler(editedPagePath, onRenameFinish, onRenameFailure);
-  }, [editedPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler]);
-
-  const onPressEscape = useCallback(() => {
+  const rename = useCallback(() => {
+    pagePathRenameHandler(editedPagePath,
+      () => {
+        setRenameInputShown(false);
+        onMoveTerminated?.();
+      },
+      () => {
+        setRenameInputShown(true);
+      });
+  }, [editedPagePath, onMoveTerminated, pagePathRenameHandler]);
+
+  const cancel = useCallback(() => {
     setEditedPagePath(currentPagePath);
     setRenameInputShown(false);
   }, [currentPagePath]);
@@ -104,10 +102,10 @@ export const PageTitleHeader = (props: Props): JSX.Element => {
                 value={isNewlyCreatedPage ? '' : editedPageTitle}
                 placeholder={t('Input page name')}
                 inputClassName="fs-4"
-                onPressEnter={onPressEnter}
-                onPressEscape={onPressEscape}
-                onChange={onInputChange}
-                onClickOutside={() => { setRenameInputShown(false) }}
+                onPressEnter={rename}
+                onPressEscape={cancel}
+                onChange={inputChangeHandler}
+                onBlur={rename}
                 validationTarget={ValidationTarget.PAGE}
                 useAutosizeInput
               />

+ 10 - 7
apps/app/src/components/Sidebar/PageTreeItem/Ellipsis.tsx

@@ -66,7 +66,7 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
     setRenameInputShown(true);
   }, []);
 
-  const onPressEnterForRenameHandler = async(inputText: string) => {
+  const rename = useCallback(async(inputText) => {
     const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
     const newPagePath = nodePath.resolve(parentPath, inputText);
 
@@ -83,9 +83,7 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
         newPagePath,
       });
 
-      if (onRenamed != null) {
-        onRenamed(page.path, newPagePath);
-      }
+      onRenamed?.(page.path, newPagePath);
 
       toastSuccess(t('renamed_pages', { path: page.path }));
     }
@@ -93,7 +91,11 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
       setRenameInputShown(true);
       toastError(err);
     }
-  };
+  }, [onRenamed, page._id, page.path, page.revision, t]);
+
+  const cancel = useCallback(() => {
+    setRenameInputShown(false);
+  }, []);
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
     if (onClickDeleteMenuItem == null) {
@@ -136,8 +138,9 @@ export const Ellipsis: FC<TreeItemToolProps> = (props) => {
             <ClosableTextInput
               value={nodePath.basename(page.path ?? '')}
               placeholder={t('Input page name')}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={onPressEnterForRenameHandler}
+              onPressEnter={rename}
+              onBlur={rename}
+              onPressEscape={cancel}
               validationTarget={ValidationTarget.PAGE}
             />
           </NotDraggableForClosableTextInput>

+ 10 - 16
apps/app/src/components/TreeItem/NewPageInput/NewPageInput.tsx

@@ -1,4 +1,6 @@
-import React, { type FC, useCallback, useEffect } from 'react';
+import React, {
+  type FC, useCallback,
+} from 'react';
 
 import nodePath from 'path';
 
@@ -29,7 +31,7 @@ export const NewPageInput: FC<Props> = (props) => {
     onCanceled,
   } = props;
 
-  const onPressEnterForCreateHandler = async(inputText: string) => {
+  const create = useCallback(async(inputText) => {
     const parentPath = pathUtils.addTrailingSlash(page.path as string);
     const newPagePath = nodePath.resolve(parentPath, inputText);
     const isCreatable = pagePathUtils.isCreatablePage(newPagePath);
@@ -49,29 +51,21 @@ export const NewPageInput: FC<Props> = (props) => {
     finally {
       onSubmittionFailed?.();
     }
-  };
+  }, [onSubmit, onSubmittionFailed, page.path, t]);
 
-  const onPressEscHandler = useCallback((event) => {
-    if (event.keyCode === 27) {
-      onCanceled?.();
-    }
+  const cancel = useCallback(() => {
+    onCanceled?.();
   }, [onCanceled]);
 
-  useEffect(() => {
-    document.addEventListener('keydown', onPressEscHandler, false);
-    return () => {
-      document.removeEventListener('keydown', onPressEscHandler, false);
-    };
-  }, [onPressEscHandler]);
-
   return (
     <>
       {isEnableActions && (
         <NotDraggableForClosableTextInput>
           <ClosableTextInput
             placeholder={t('Input page name')}
-            onClickOutside={onCanceled}
-            onPressEnter={onPressEnterForCreateHandler}
+            onPressEnter={create}
+            onPressEscape={cancel}
+            onBlur={create}
             validationTarget={ValidationTarget.PAGE}
           />
         </NotDraggableForClosableTextInput>