PageTitleHeader.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import { useState, useCallback } from 'react';
  2. import nodePath from 'path';
  3. import type { IPagePopulatedToShowRevision } from '@growi/core';
  4. import { DevidedPagePath } from '@growi/core/dist/models';
  5. import { pathUtils } from '@growi/core/dist/utils';
  6. import { isMovablePage } from '@growi/core/dist/utils/page-path-utils';
  7. import { useTranslation } from 'next-i18next';
  8. import { ValidationTarget } from '~/client/util/input-validator';
  9. import ClosableTextInput from '../Common/ClosableTextInput';
  10. import { CopyDropdown } from '../Common/CopyDropdown';
  11. import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
  12. import styles from './PageTitleHeader.module.scss';
  13. const moduleClass = styles['page-title-header'] ?? '';
  14. const borderColorClass = styles['page-title-header-border-color'] ?? '';
  15. type Props = {
  16. currentPage: IPagePopulatedToShowRevision,
  17. className?: string,
  18. maxWidth?: number,
  19. onMoveTerminated?: () => void,
  20. };
  21. export const PageTitleHeader = (props: Props): JSX.Element => {
  22. const { t } = useTranslation();
  23. const { currentPage, maxWidth, onMoveTerminated } = props;
  24. const currentPagePath = currentPage.path;
  25. const isMovable = isMovablePage(currentPagePath);
  26. const dPagePath = new DevidedPagePath(currentPage.path, true);
  27. const pageTitle = dPagePath.latter;
  28. const [isRenameInputShown, setRenameInputShown] = useState(false);
  29. const [editedPagePath, setEditedPagePath] = useState(currentPagePath);
  30. const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
  31. const editedPageTitle = nodePath.basename(editedPagePath);
  32. // TODO: https://redmine.weseek.co.jp/issues/142729
  33. // https://regex101.com/r/Wg2Hh6/1
  34. const untitledPageRegex = /^Untitled-\d+$/;
  35. const isNewlyCreatedPage = (currentPage.wip && currentPage.latestRevision == null && untitledPageRegex.test(editedPageTitle)) ?? false;
  36. const inputChangeHandler = useCallback((inputText: string) => {
  37. const newPageTitle = pathUtils.removeHeadingSlash(inputText);
  38. const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
  39. const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
  40. setEditedPagePath(newPagePath);
  41. }, [currentPage?.path, setEditedPagePath]);
  42. const rename = useCallback(() => {
  43. pagePathRenameHandler(editedPagePath,
  44. () => {
  45. setRenameInputShown(false);
  46. onMoveTerminated?.();
  47. },
  48. () => {
  49. setRenameInputShown(true);
  50. });
  51. }, [editedPagePath, onMoveTerminated, pagePathRenameHandler]);
  52. const cancel = useCallback(() => {
  53. setEditedPagePath(currentPagePath);
  54. setRenameInputShown(false);
  55. }, [currentPagePath]);
  56. const onClickPageTitle = useCallback(() => {
  57. if (!isMovable) {
  58. return;
  59. }
  60. setEditedPagePath(currentPagePath);
  61. setRenameInputShown(true);
  62. }, [currentPagePath, isMovable]);
  63. // TODO: auto focus when create new page
  64. // https://redmine.weseek.co.jp/issues/136128
  65. // useEffect(() => {
  66. // if (isNewlyCreatedPage) {
  67. // setRenameInputShown(true);
  68. // }
  69. // }, [currentPage._id, isNewlyCreatedPage]);
  70. return (
  71. <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`} style={{ maxWidth }}>
  72. <div className="page-title-header-input me-1 d-inline-block overflow-x-scroll">
  73. { isRenameInputShown && (
  74. <div className="position-relative">
  75. <div className="position-absolute w-100">
  76. <ClosableTextInput
  77. value={isNewlyCreatedPage ? '' : editedPageTitle}
  78. placeholder={t('Input page name')}
  79. inputClassName="fs-4"
  80. onPressEnter={rename}
  81. onPressEscape={cancel}
  82. onChange={inputChangeHandler}
  83. onBlur={rename}
  84. validationTarget={ValidationTarget.PAGE}
  85. useAutosizeInput
  86. />
  87. </div>
  88. </div>
  89. ) }
  90. <h1
  91. className={`mb-0 mb-sm-1 px-2 fs-4
  92. ${isRenameInputShown ? 'invisible' : ''} text-truncate
  93. ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
  94. `}
  95. onClick={onClickPageTitle}
  96. >
  97. {pageTitle}
  98. </h1>
  99. </div>
  100. <div className={`${isRenameInputShown ? 'invisible' : ''} d-flex align-items-center`}>
  101. { currentPage.wip && (
  102. <span className="badge rounded-pill text-bg-secondary ms-2">WIP</span>
  103. )}
  104. <CopyDropdown
  105. pageId={currentPage._id}
  106. pagePath={currentPage.path}
  107. dropdownToggleId={`copydropdown-${currentPage._id}`}
  108. dropdownToggleClassName="p-1"
  109. >
  110. <span className="material-symbols-outlined fs-6">content_paste</span>
  111. </CopyDropdown>
  112. </div>
  113. </div>
  114. );
  115. };