PagePathHeader.tsx 5.3 KB

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