TreeItemLayout.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import React, {
  2. useCallback,
  3. useEffect,
  4. useMemo,
  5. type MouseEvent,
  6. type JSX,
  7. } from 'react';
  8. import { addTrailingSlash } from '@growi/core/dist/utils/path-utils';
  9. import { usePageTreeDescCountMap } from '~/states/ui/page-tree-desc-count-map';
  10. import { SimpleItemContent } from './SimpleItemContent';
  11. import type { TreeItemProps, TreeItemToolProps } from './interfaces';
  12. import styles from './TreeItemLayout.module.scss';
  13. const moduleClass = styles['tree-item-layout'] ?? '';
  14. type TreeItemLayoutProps = TreeItemProps & {
  15. className?: string,
  16. indentSize?: number,
  17. }
  18. export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
  19. const {
  20. className, itemClassName,
  21. indentSize = 10,
  22. item: page,
  23. itemLevel: baseItemLevel = 1,
  24. targetPath, targetPathOrId, isExpanded = false,
  25. isEnableActions, isReadOnlyUser, isWipPageShown = true,
  26. showAlternativeContent,
  27. onRenamed, onClick, onClickDuplicateMenuItem, onClickDeleteMenuItem, onWheelClick,
  28. onToggle,
  29. } = props;
  30. const itemClickHandler = useCallback((e: MouseEvent) => {
  31. // DO NOT handle the event when e.currentTarget and e.target is different
  32. if (e.target !== e.currentTarget) {
  33. return;
  34. }
  35. onClick?.(page);
  36. }, [onClick, page]);
  37. const itemMouseupHandler = useCallback((e: MouseEvent) => {
  38. // DO NOT handle the event when e.currentTarget and e.target is different
  39. if (e.target !== e.currentTarget) {
  40. return;
  41. }
  42. if (e.button === 1) {
  43. e.preventDefault();
  44. onWheelClick?.(page);
  45. }
  46. }, [onWheelClick, page]);
  47. // descendantCount
  48. const { getDescCount } = usePageTreeDescCountMap();
  49. const descendantCount = getDescCount(page._id) || page.descendantCount || 0;
  50. // hasDescendants flag
  51. const hasDescendants = descendantCount > 0;
  52. // auto open if targetPath is descendant of this page
  53. useEffect(() => {
  54. if (isExpanded) return;
  55. const isPathToTarget = page.path != null
  56. && targetPath.startsWith(addTrailingSlash(page.path))
  57. && targetPath !== page.path; // Target Page does not need to be opened
  58. if (isPathToTarget) onToggle?.();
  59. }, [targetPath, page.path, isExpanded, onToggle]);
  60. const isSelected = useMemo(() => {
  61. return page._id === targetPathOrId || page.path === targetPathOrId;
  62. }, [page, targetPathOrId]);
  63. const toolProps: TreeItemToolProps = {
  64. item: page,
  65. itemLevel: baseItemLevel,
  66. isEnableActions,
  67. isReadOnlyUser,
  68. onRenamed,
  69. onClickDuplicateMenuItem,
  70. onClickDeleteMenuItem,
  71. };
  72. const EndComponents = props.customEndComponents;
  73. const HoveredEndComponents = props.customHoveredEndComponents;
  74. const AlternativeComponents = props.customAlternativeComponents;
  75. if (!isWipPageShown && page.wip) {
  76. return <></>;
  77. }
  78. return (
  79. <div
  80. id={`tree-item-layout-${page._id}`}
  81. data-testid="grw-pagetree-item-container"
  82. className={`${moduleClass} ${className} level-${baseItemLevel}`}
  83. style={{ paddingLeft: `${baseItemLevel > 1 ? indentSize : 0}px` }}
  84. >
  85. <li
  86. role="button"
  87. className={`list-group-item list-group-item-action
  88. ${isSelected ? 'active' : ''}
  89. ${itemClassName ?? ''}
  90. border-0 py-0 ps-0 d-flex align-items-center rounded-1`}
  91. id={`grw-pagetree-list-${page._id}`}
  92. onClick={itemClickHandler}
  93. onMouseUp={itemMouseupHandler}
  94. >
  95. <div className="btn-triangle-container d-flex justify-content-center">
  96. {hasDescendants && (
  97. <button
  98. type="button"
  99. className={`btn btn-triangle p-0 ${isExpanded ? 'open' : ''}`}
  100. onClick={onToggle}
  101. >
  102. <div className="d-flex justify-content-center">
  103. <span className="material-symbols-outlined fs-5">arrow_right</span>
  104. </div>
  105. </button>
  106. )}
  107. </div>
  108. {showAlternativeContent && AlternativeComponents != null
  109. ? (
  110. AlternativeComponents.map((AlternativeContent, index) => (
  111. // eslint-disable-next-line react/no-array-index-key
  112. (<AlternativeContent key={index} {...toolProps} />)
  113. ))
  114. )
  115. : (
  116. <>
  117. <SimpleItemContent page={page} />
  118. <div className="d-hover-none">
  119. {EndComponents?.map((EndComponent, index) => (
  120. // eslint-disable-next-line react/no-array-index-key
  121. (<EndComponent key={index} {...toolProps} />)
  122. ))}
  123. </div>
  124. <div className="d-none d-hover-flex">
  125. {HoveredEndComponents?.map((HoveredEndContent, index) => (
  126. // eslint-disable-next-line react/no-array-index-key
  127. (<HoveredEndContent key={index} {...toolProps} />)
  128. ))}
  129. </div>
  130. </>
  131. )
  132. }
  133. </li>
  134. </div>
  135. );
  136. };