PageTitleHeader.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import type { ChangeEvent } from 'react';
  2. import { useState, useCallback, useEffect } from 'react';
  3. import nodePath from 'path';
  4. import type { IPagePopulatedToShowRevision } from '@growi/core';
  5. import { DevidedPagePath } from '@growi/core/dist/models';
  6. import { pathUtils } from '@growi/core/dist/utils';
  7. import { isMovablePage } from '@growi/core/dist/utils/page-path-utils';
  8. import { useTranslation } from 'next-i18next';
  9. import type { InputValidationResult } from '~/client/util/use-input-validator';
  10. import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
  11. import { EditorMode, useEditorMode } from '~/stores-universal/ui';
  12. import { useIsUntitledPage } from '~/stores/ui';
  13. import { CopyDropdown } from '../Common/CopyDropdown';
  14. import { AutosizeSubmittableInput, getAdjustedMaxWidthForAutosizeInput } from '../Common/SubmittableInput';
  15. import { usePagePathRenameHandler } from '../PageEditor/page-path-rename-utils';
  16. import styles from './PageTitleHeader.module.scss';
  17. const moduleClass = styles['page-title-header'] ?? '';
  18. const borderColorClass = styles['page-title-header-border-color'] ?? '';
  19. type Props = {
  20. currentPage: IPagePopulatedToShowRevision,
  21. className?: string,
  22. maxWidth?: number,
  23. onMoveTerminated?: () => void,
  24. };
  25. export const PageTitleHeader = (props: Props): JSX.Element => {
  26. const { t } = useTranslation();
  27. const { currentPage, maxWidth, onMoveTerminated } = props;
  28. const currentPagePath = currentPage.path;
  29. const isMovable = isMovablePage(currentPagePath);
  30. const dPagePath = new DevidedPagePath(currentPage.path, true);
  31. const pageTitle = dPagePath.latter;
  32. const [isRenameInputShown, setRenameInputShown] = useState(false);
  33. const [editedPagePath, setEditedPagePath] = useState(currentPagePath);
  34. const [validationResult, setValidationResult] = useState<InputValidationResult>();
  35. const pagePathRenameHandler = usePagePathRenameHandler(currentPage);
  36. const inputValidator = useInputValidator(ValidationTarget.PAGE);
  37. const editedPageTitle = nodePath.basename(editedPagePath);
  38. const { data: editorMode } = useEditorMode();
  39. const { data: isUntitledPage } = useIsUntitledPage();
  40. const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
  41. const newPageTitle = pathUtils.removeHeadingSlash(e.target.value);
  42. const parentPagePath = pathUtils.addTrailingSlash(nodePath.dirname(currentPage.path));
  43. const newPagePath = nodePath.resolve(parentPagePath, newPageTitle);
  44. setEditedPagePath(newPagePath);
  45. // validation
  46. const validationResult = inputValidator(e.target.value);
  47. setValidationResult(validationResult ?? undefined);
  48. }, [currentPage.path, inputValidator]);
  49. const rename = useCallback(() => {
  50. pagePathRenameHandler(editedPagePath,
  51. () => {
  52. setRenameInputShown(false);
  53. setValidationResult(undefined);
  54. onMoveTerminated?.();
  55. },
  56. () => {
  57. setRenameInputShown(true);
  58. });
  59. }, [editedPagePath, onMoveTerminated, pagePathRenameHandler]);
  60. const cancel = useCallback(() => {
  61. setEditedPagePath(currentPagePath);
  62. setValidationResult(undefined);
  63. setRenameInputShown(false);
  64. }, [currentPagePath]);
  65. const onClickPageTitle = useCallback(() => {
  66. if (!isMovable) {
  67. return;
  68. }
  69. setEditedPagePath(currentPagePath);
  70. setRenameInputShown(true);
  71. }, [currentPagePath, isMovable]);
  72. useEffect(() => {
  73. setEditedPagePath(currentPagePath);
  74. if (isUntitledPage != null) {
  75. setRenameInputShown(isUntitledPage && editorMode === EditorMode.Editor);
  76. }
  77. }, [currentPage._id, currentPagePath, editorMode, isUntitledPage]);
  78. const isInvalid = validationResult != null;
  79. const inputMaxWidth = maxWidth != null
  80. ? getAdjustedMaxWidthForAutosizeInput(maxWidth, 'md', validationResult != null ? false : undefined) - 16
  81. : undefined;
  82. return (
  83. <div className={`d-flex ${moduleClass} ${props.className ?? ''} position-relative`}>
  84. <div className="page-title-header-input me-1 d-inline-block">
  85. { isRenameInputShown && (
  86. <div className="position-relative">
  87. <div className="position-absolute w-100">
  88. <AutosizeSubmittableInput
  89. value={isUntitledPage ? '' : editedPageTitle}
  90. inputClassName={`form-control fs-4 ${isInvalid ? 'is-invalid' : ''}`}
  91. inputStyle={{ maxWidth: inputMaxWidth }}
  92. placeholder={t('Input page name')}
  93. onChange={changeHandler}
  94. onSubmit={rename}
  95. onCancel={cancel}
  96. autoFocus
  97. />
  98. </div>
  99. </div>
  100. ) }
  101. <h1
  102. className={`mb-0 mb-sm-1 px-2 fs-4
  103. ${isRenameInputShown ? 'invisible' : ''} text-truncate
  104. ${isMovable ? 'border border-2 rounded-2' : ''} ${borderColorClass}
  105. `}
  106. onClick={onClickPageTitle}
  107. >
  108. {pageTitle}
  109. </h1>
  110. </div>
  111. <div className={`${isRenameInputShown ? 'invisible' : ''} d-flex align-items-center`}>
  112. { currentPage.wip && (
  113. <span className="badge rounded-pill text-bg-secondary ms-2">WIP</span>
  114. )}
  115. <CopyDropdown
  116. pageId={currentPage._id}
  117. pagePath={currentPage.path}
  118. dropdownToggleId={`copydropdown-${currentPage._id}`}
  119. dropdownToggleClassName="p-1"
  120. >
  121. <span className="material-symbols-outlined fs-6">content_paste</span>
  122. </CopyDropdown>
  123. </div>
  124. </div>
  125. );
  126. };