PagePathHeader.tsx 5.1 KB

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