|
@@ -1,19 +1,21 @@
|
|
|
|
|
+import type { ChangeEvent } from 'react';
|
|
|
import {
|
|
import {
|
|
|
useState, useCallback, memo,
|
|
useState, useCallback, memo,
|
|
|
} from 'react';
|
|
} from 'react';
|
|
|
-import type { FC } from 'react';
|
|
|
|
|
|
|
|
|
|
import type { IPagePopulatedToShowRevision } from '@growi/core';
|
|
import type { IPagePopulatedToShowRevision } from '@growi/core';
|
|
|
import { DevidedPagePath } from '@growi/core/dist/models';
|
|
import { DevidedPagePath } from '@growi/core/dist/models';
|
|
|
import { normalizePath } from '@growi/core/dist/utils/path-utils';
|
|
import { normalizePath } from '@growi/core/dist/utils/path-utils';
|
|
|
import { useTranslation } from 'next-i18next';
|
|
import { useTranslation } from 'next-i18next';
|
|
|
|
|
+import { debounce } from 'throttle-debounce';
|
|
|
|
|
|
|
|
-import { ValidationTarget } from '~/client/util/input-validator';
|
|
|
|
|
|
|
+import type { InputValidationResult } from '~/client/util/use-input-validator';
|
|
|
|
|
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
|
|
|
import LinkedPagePath from '~/models/linked-page-path';
|
|
import LinkedPagePath from '~/models/linked-page-path';
|
|
|
import { usePageSelectModal } from '~/stores/modal';
|
|
import { usePageSelectModal } from '~/stores/modal';
|
|
|
|
|
|
|
|
-import ClosableTextInput from '../Common/ClosableTextInput';
|
|
|
|
|
import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
|
|
import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
|
|
|
|
|
+import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
|
|
|
import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
|
|
import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
|
|
|
import { PageSelectModal } from '../PageSelectModal/PageSelectModal';
|
|
import { PageSelectModal } from '../PageSelectModal/PageSelectModal';
|
|
|
|
|
|
|
@@ -25,11 +27,15 @@ const moduleClass = styles['page-path-header'];
|
|
|
type Props = {
|
|
type Props = {
|
|
|
currentPage: IPagePopulatedToShowRevision,
|
|
currentPage: IPagePopulatedToShowRevision,
|
|
|
className?: string,
|
|
className?: string,
|
|
|
|
|
+ maxWidth?: number,
|
|
|
|
|
+ onRenameTerminated?: () => void,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-export const PagePathHeader: FC<Props> = memo((props: Props) => {
|
|
|
|
|
|
|
+export const PagePathHeader = memo((props: Props): JSX.Element => {
|
|
|
const { t } = useTranslation();
|
|
const { t } = useTranslation();
|
|
|
- const { currentPage, className } = props;
|
|
|
|
|
|
|
+ const {
|
|
|
|
|
+ currentPage, className, maxWidth, onRenameTerminated,
|
|
|
|
|
+ } = props;
|
|
|
|
|
|
|
|
const dPagePath = new DevidedPagePath(currentPage.path, true);
|
|
const dPagePath = new DevidedPagePath(currentPage.path, true);
|
|
|
const parentPagePath = dPagePath.former;
|
|
const parentPagePath = dPagePath.former;
|
|
@@ -38,68 +44,59 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
|
|
|
|
|
|
|
|
const [isRenameInputShown, setRenameInputShown] = useState(false);
|
|
const [isRenameInputShown, setRenameInputShown] = useState(false);
|
|
|
const [isHover, setHover] = useState(false);
|
|
const [isHover, setHover] = useState(false);
|
|
|
- const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
|
|
|
|
|
-
|
|
|
|
|
- // const [isIconHidden, setIsIconHidden] = useState(false);
|
|
|
|
|
|
|
|
|
|
const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
|
|
const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
|
|
|
const isOpened = PageSelectModalData?.isOpened ?? false;
|
|
const isOpened = PageSelectModalData?.isOpened ?? false;
|
|
|
|
|
|
|
|
- const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
|
|
|
|
|
|
|
+ const [validationResult, setValidationResult] = useState<InputValidationResult>();
|
|
|
|
|
|
|
|
- const onRenameFinish = useCallback(() => {
|
|
|
|
|
- setRenameInputShown(false);
|
|
|
|
|
- }, []);
|
|
|
|
|
|
|
+ const inputValidator = useInputValidator(ValidationTarget.PAGE);
|
|
|
|
|
|
|
|
- const onRenameFailure = useCallback(() => {
|
|
|
|
|
- setRenameInputShown(true);
|
|
|
|
|
- }, []);
|
|
|
|
|
|
|
+ const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ const validationResult = inputValidator(e.target.value);
|
|
|
|
|
+ setValidationResult(validationResult ?? undefined);
|
|
|
|
|
+ }, [inputValidator]);
|
|
|
|
|
+ const changeHandlerDebounced = debounce(300, changeHandler);
|
|
|
|
|
|
|
|
- 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 pagePathRenameHandler = usePagePathRenameHandler(currentPage);
|
|
|
|
|
+
|
|
|
|
|
|
|
|
- const onPressEscape = useCallback(() => {
|
|
|
|
|
|
|
+ const rename = useCallback((inputText) => {
|
|
|
|
|
+ const pathToRename = normalizePath(`${inputText}/${dPagePath.latter}`);
|
|
|
|
|
+ pagePathRenameHandler(pathToRename,
|
|
|
|
|
+ () => {
|
|
|
|
|
+ setRenameInputShown(false);
|
|
|
|
|
+ setValidationResult(undefined);
|
|
|
|
|
+ onRenameTerminated?.();
|
|
|
|
|
+ },
|
|
|
|
|
+ () => {
|
|
|
|
|
+ setRenameInputShown(true);
|
|
|
|
|
+ });
|
|
|
|
|
+ }, [dPagePath.latter, pagePathRenameHandler, onRenameTerminated]);
|
|
|
|
|
+
|
|
|
|
|
+ const cancel = useCallback(() => {
|
|
|
// reset
|
|
// reset
|
|
|
- setEditingParentPagePath(parentPagePath);
|
|
|
|
|
|
|
+ setValidationResult(undefined);
|
|
|
setRenameInputShown(false);
|
|
setRenameInputShown(false);
|
|
|
- }, [parentPagePath]);
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
const onClickEditButton = useCallback(() => {
|
|
const onClickEditButton = useCallback(() => {
|
|
|
// reset
|
|
// reset
|
|
|
- setEditingParentPagePath(parentPagePath);
|
|
|
|
|
setRenameInputShown(true);
|
|
setRenameInputShown(true);
|
|
|
- }, [parentPagePath]);
|
|
|
|
|
-
|
|
|
|
|
- // TODO: https://redmine.weseek.co.jp/issues/141062
|
|
|
|
|
- // Truncate left side and don't use getElementById
|
|
|
|
|
- //
|
|
|
|
|
- // useEffect(() => {
|
|
|
|
|
- // const areaElem = document.getElementById('grw-page-path-header-container');
|
|
|
|
|
- // const linkElem = document.getElementById('grw-page-path-hierarchical-link');
|
|
|
|
|
-
|
|
|
|
|
- // const areaElemWidth = areaElem?.offsetWidth;
|
|
|
|
|
- // const linkElemWidth = linkElem?.offsetWidth;
|
|
|
|
|
-
|
|
|
|
|
- // if (areaElemWidth && linkElemWidth) {
|
|
|
|
|
- // setIsIconHidden(linkElemWidth > areaElemWidth);
|
|
|
|
|
- // }
|
|
|
|
|
- // else {
|
|
|
|
|
- // setIsIconHidden(false);
|
|
|
|
|
- // }
|
|
|
|
|
- // }, [currentPage]);
|
|
|
|
|
- //
|
|
|
|
|
- // const styles: CSSProperties | undefined = isIconHidden ? { direction: 'rtl' } : undefined;
|
|
|
|
|
|
|
+ }, []);
|
|
|
|
|
|
|
|
if (dPagePath.isRoot) {
|
|
if (dPagePath.isRoot) {
|
|
|
return <></>;
|
|
return <></>;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const isInvalid = validationResult != null;
|
|
|
|
|
+
|
|
|
|
|
+ const inputMaxWidth = maxWidth != null
|
|
|
|
|
+ ? getAdjustedMaxWidthForAutosizeInput(maxWidth, 'sm', validationResult != null ? false : undefined) - 16
|
|
|
|
|
+ : undefined;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
id="page-path-header"
|
|
id="page-path-header"
|
|
@@ -108,35 +105,34 @@ export const PagePathHeader: FC<Props> = memo((props: Props) => {
|
|
|
onMouseLeave={() => setHover(false)}
|
|
onMouseLeave={() => setHover(false)}
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
|
- id="grw-page-path-header-container"
|
|
|
|
|
- className="me-2 d-inline-block overflow-hidden"
|
|
|
|
|
|
|
+ className="page-path-header-input d-inline-block"
|
|
|
>
|
|
>
|
|
|
{ isRenameInputShown && (
|
|
{ isRenameInputShown && (
|
|
|
- <div className="position-absolute w-100">
|
|
|
|
|
- <ClosableTextInput
|
|
|
|
|
- value={editingParentPagePath}
|
|
|
|
|
- placeholder={t('Input parent page path')}
|
|
|
|
|
- inputClassName="form-control-sm"
|
|
|
|
|
- onPressEnter={onPressEnter}
|
|
|
|
|
- onPressEscape={onPressEscape}
|
|
|
|
|
- onChange={onInputChange}
|
|
|
|
|
- validationTarget={ValidationTarget.PAGE}
|
|
|
|
|
- onClickOutside={onPressEscape}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="position-relative">
|
|
|
|
|
+ <div className="position-absolute w-100">
|
|
|
|
|
+ <AutosizeSubmittableInput
|
|
|
|
|
+ value={parentPagePath}
|
|
|
|
|
+ inputClassName={`form-control form-control-sm ${isInvalid ? 'is-invalid' : ''}`}
|
|
|
|
|
+ inputStyle={{ maxWidth: inputMaxWidth }}
|
|
|
|
|
+ placeholder={t('Input parent page path')}
|
|
|
|
|
+ onChange={changeHandlerDebounced}
|
|
|
|
|
+ onSubmit={rename}
|
|
|
|
|
+ onCancel={cancel}
|
|
|
|
|
+ autoFocus
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
) }
|
|
) }
|
|
|
- <div
|
|
|
|
|
- className={`${isRenameInputShown ? 'invisible' : ''} text-truncate`}
|
|
|
|
|
- // style={styles}
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div className={`${isRenameInputShown ? 'invisible' : ''} text-truncate`}>
|
|
|
<PagePathHierarchicalLink
|
|
<PagePathHierarchicalLink
|
|
|
linkedPagePath={linkedPagePath}
|
|
linkedPagePath={linkedPagePath}
|
|
|
- // isIconHidden={isIconHidden}
|
|
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}>
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`page-path-header-buttons d-flex align-items-center ms-2 ${isHover && !isRenameInputShown ? '' : 'invisible'}`}
|
|
|
|
|
+ >
|
|
|
<button
|
|
<button
|
|
|
type="button"
|
|
type="button"
|
|
|
className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
|
|
className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
|