PagePathHeader.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { useState, useEffect, useCallback } from 'react';
  2. import type { FC } from 'react';
  3. import type { IPagePopulatedToShowRevision } from '@growi/core';
  4. import { DevidedPagePath } from '@growi/core/dist/models';
  5. import { normalizePath } from '@growi/core/dist/utils/path-utils';
  6. import { useTranslation } from 'next-i18next';
  7. import { ValidationTarget } from '~/client/util/input-validator';
  8. import LinkedPagePath from '~/models/linked-page-path';
  9. import { usePageSelectModal } from '~/stores/modal';
  10. import ClosableTextInput from '../Common/ClosableTextInput';
  11. import { PagePathHierarchicalLink } from '../Common/PagePathHierarchicalLink';
  12. import { PageSelectModal } from '../PageSelectModal/PageSelectModal';
  13. import { usePagePathRenameHandler } from './page-header-utils';
  14. import styles from './PagePathHeader.module.scss';
  15. const moduleClass = styles['page-path-header'];
  16. type Props = {
  17. currentPage: IPagePopulatedToShowRevision
  18. }
  19. export const PagePathHeader: FC<Props> = (props) => {
  20. const { t } = useTranslation();
  21. const { currentPage } = props;
  22. const dPagePath = new DevidedPagePath(currentPage.path, true);
  23. const parentPagePath = dPagePath.former;
  24. const linkedPagePath = new LinkedPagePath(parentPagePath);
  25. const [isRenameInputShown, setRenameInputShown] = useState(false);
  26. const [isHover, setHover] = useState(false);
  27. const [editingParentPagePath, setEditingParentPagePath] = useState(parentPagePath);
  28. const { data: PageSelectModalData, open: openPageSelectModal } = usePageSelectModal();
  29. const isOpened = PageSelectModalData?.isOpened ?? false;
  30. const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
  31. const onRenameFinish = useCallback(() => {
  32. setRenameInputShown(false);
  33. }, []);
  34. const onRenameFailure = useCallback(() => {
  35. setRenameInputShown(true);
  36. }, []);
  37. const onInputChange = useCallback((inputText: string) => {
  38. setEditingParentPagePath(inputText);
  39. }, []);
  40. const onPressEnter = useCallback(() => {
  41. const pathToRename = normalizePath(`${editingParentPagePath}/${dPagePath.latter}`);
  42. pagePathRenameHandler(pathToRename, onRenameFinish, onRenameFailure);
  43. }, [editingParentPagePath, onRenameFailure, onRenameFinish, pagePathRenameHandler, dPagePath.latter]);
  44. const onPressEscape = useCallback(() => {
  45. // reset
  46. setEditingParentPagePath(parentPagePath);
  47. setRenameInputShown(false);
  48. }, [parentPagePath]);
  49. const onClickEditButton = useCallback(() => {
  50. // reset
  51. setEditingParentPagePath(parentPagePath);
  52. setRenameInputShown(true);
  53. }, [parentPagePath]);
  54. const clickOutSideHandler = useCallback((e) => {
  55. const container = document.getElementById('page-path-header');
  56. if (container && !container.contains(e.target)) {
  57. setRenameInputShown(false);
  58. }
  59. }, []);
  60. useEffect(() => {
  61. document.addEventListener('click', clickOutSideHandler);
  62. return () => {
  63. document.removeEventListener('click', clickOutSideHandler);
  64. };
  65. }, [clickOutSideHandler]);
  66. if (dPagePath.isRoot) {
  67. return <></>;
  68. }
  69. return (
  70. <div
  71. id="page-path-header"
  72. className={`d-flex ${moduleClass} small`}
  73. onMouseEnter={() => setHover(true)}
  74. onMouseLeave={() => setHover(false)}
  75. >
  76. <div className="me-2">
  77. { isRenameInputShown && (
  78. <div className="position-absolute">
  79. <ClosableTextInput
  80. useAutosizeInput
  81. value={editingParentPagePath}
  82. placeholder={t('Input page name')}
  83. inputClassName="form-control-sm"
  84. onPressEnter={onPressEnter}
  85. onPressEscape={onPressEscape}
  86. onChange={onInputChange}
  87. validationTarget={ValidationTarget.PAGE}
  88. />
  89. </div>
  90. ) }
  91. <div className={`${isRenameInputShown ? 'invisible' : ''}`}>
  92. <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />
  93. </div>
  94. </div>
  95. <div className={`page-path-header-buttons d-flex align-items-center ${isHover && !isRenameInputShown ? '' : 'invisible'}`}>
  96. <button
  97. type="button"
  98. className="btn btn-outline-neutral-secondary me-2 d-flex align-items-center justify-content-center"
  99. onClick={onClickEditButton}
  100. >
  101. <span className="material-symbols-outlined fs-6">edit</span>
  102. </button>
  103. <button
  104. type="button"
  105. className="btn btn-outline-neutral-secondary d-flex align-items-center justify-content-center"
  106. onClick={openPageSelectModal}
  107. >
  108. <span className="material-symbols-outlined fs-6">account_tree</span>
  109. </button>
  110. </div>
  111. {isOpened && <PageSelectModal />}
  112. </div>
  113. );
  114. };