PagePathHeader.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. import type { ChangeEvent, JSX } from 'react';
  2. import {
  3. useState, useCallback, memo,
  4. } from 'react';
  5. import nodePath from 'path';
  6. import type { IPagePopulatedToShowRevision } from '@growi/core';
  7. import { DevidedPagePath } from '@growi/core/dist/models';
  8. import { normalizePath } from '@growi/core/dist/utils/path-utils';
  9. import { useTranslation } from 'next-i18next';
  10. import { debounce } from 'throttle-debounce';
  11. import type { InputValidationResult } from '~/client/util/use-input-validator';
  12. import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
  13. import type { IPageForItem } from '~/interfaces/page';
  14. import LinkedPagePath from '~/models/linked-page-path';
  15. import { usePageSelectModal } from '~/stores/modal';
  16. import { PagePathHierarchicalLink } from '../../../components/Common/PagePathHierarchicalLink';
  17. import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
  18. import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
  19. import styles from './PagePathHeader.module.scss';
  20. const moduleClass = styles['page-path-header'];
  21. type Props = {
  22. currentPage: IPagePopulatedToShowRevision,
  23. className?: string,
  24. maxWidth?: number,
  25. onRenameTerminated?: () => void,
  26. }
  27. export const PagePathHeader = memo((props: Props): JSX.Element => {
  28. const { t } = useTranslation();
  29. const {
  30. currentPage, className, maxWidth, onRenameTerminated,
  31. } = props;
  32. const dPagePath = new DevidedPagePath(currentPage.path, true);
  33. const parentPagePath = dPagePath.former;
  34. const linkedPagePath = new LinkedPagePath(parentPagePath);
  35. const [isRenameInputShown, setRenameInputShown] = useState(false);
  36. const [isHover, setHover] = useState(false);
  37. const { open: openPageSelectModal } = usePageSelectModal();
  38. const [validationResult, setValidationResult] = useState<InputValidationResult>();
  39. const inputValidator = useInputValidator(ValidationTarget.PAGE);
  40. const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
  41. const validationResult = inputValidator(e.target.value);
  42. setValidationResult(validationResult ?? undefined);
  43. }, [inputValidator]);
  44. const changeHandlerDebounced = debounce(300, changeHandler);
  45. const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
  46. const onClickOpenPageSelectModalButton = useCallback(() => {
  47. const onSelected = (page: IPageForItem): void => {
  48. if (page == null || page.path == null) {
  49. return;
  50. }
  51. const currentPageTitle = nodePath.basename(currentPage?.path ?? '') || '/';
  52. const newPagePath = nodePath.resolve(page.path, currentPageTitle);
  53. pagePathRenameHandler(newPagePath);
  54. };
  55. openPageSelectModal({ onSelected });
  56. }, [currentPage?.path, openPageSelectModal, pagePathRenameHandler]);
  57. const rename = useCallback((inputText) => {
  58. const pathToRename = normalizePath(`${inputText}/${dPagePath.latter}`);
  59. pagePathRenameHandler(pathToRename,
  60. () => {
  61. setRenameInputShown(false);
  62. setValidationResult(undefined);
  63. onRenameTerminated?.();
  64. },
  65. () => {
  66. setRenameInputShown(true);
  67. });
  68. }, [dPagePath.latter, pagePathRenameHandler, onRenameTerminated]);
  69. const cancel = useCallback(() => {
  70. // reset
  71. setValidationResult(undefined);
  72. setRenameInputShown(false);
  73. }, []);
  74. const onClickEditButton = useCallback(() => {
  75. // reset
  76. setRenameInputShown(true);
  77. }, []);
  78. if (dPagePath.isRoot) {
  79. return <></>;
  80. }
  81. const isInvalid = validationResult != null;
  82. const inputMaxWidth = maxWidth != null
  83. ? getAdjustedMaxWidthForAutosizeInput(maxWidth, 'sm', validationResult != null ? false : undefined) - 16
  84. : undefined;
  85. return (
  86. <div
  87. id="page-path-header"
  88. className={`d-flex ${moduleClass} ${className ?? ''} small position-relative ms-2`}
  89. onMouseEnter={() => setHover(true)}
  90. onMouseLeave={() => setHover(false)}
  91. >
  92. <div
  93. className="page-path-header-input d-inline-block"
  94. >
  95. { isRenameInputShown && (
  96. <div className="position-relative">
  97. <div className="position-absolute w-100">
  98. <AutosizeSubmittableInput
  99. value={parentPagePath}
  100. inputClassName={`form-control form-control-sm ${isInvalid ? 'is-invalid' : ''}`}
  101. inputStyle={{ maxWidth: inputMaxWidth }}
  102. placeholder={t('Input parent page path')}
  103. onChange={changeHandlerDebounced}
  104. onSubmit={rename}
  105. onCancel={cancel}
  106. autoFocus
  107. />
  108. </div>
  109. </div>
  110. ) }
  111. <div className={`${isRenameInputShown ? 'invisible' : ''} text-truncate`}>
  112. <PagePathHierarchicalLink
  113. linkedPagePath={linkedPagePath}
  114. />
  115. </div>
  116. </div>
  117. <div
  118. className={`page-path-header-buttons d-flex align-items-center ms-2 ${isHover && !isRenameInputShown ? '' : 'invisible'}`}
  119. >
  120. <button
  121. type="button"
  122. className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
  123. onClick={onClickEditButton}
  124. >
  125. <span className="material-symbols-outlined fs-6">edit</span>
  126. </button>
  127. <button
  128. type="button"
  129. className="btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center"
  130. onClick={onClickOpenPageSelectModalButton}
  131. >
  132. <span className="material-symbols-outlined fs-6">account_tree</span>
  133. </button>
  134. </div>
  135. </div>
  136. );
  137. });