TreeItemLayout.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import type { JSX, MouseEvent } from 'react';
  2. import { useCallback, useMemo } from 'react';
  3. import type { TreeItemProps, TreeItemToolProps } from '../interfaces';
  4. import { SimpleItemContent } from './SimpleItemContent';
  5. import styles from './TreeItemLayout.module.scss';
  6. const moduleClass = styles['tree-item-layout'] ?? '';
  7. const indentSize = 10; // in px
  8. type TreeItemLayoutProps = TreeItemProps & {
  9. className?: string;
  10. };
  11. export const TreeItemLayout = (props: TreeItemLayoutProps): JSX.Element => {
  12. const {
  13. className,
  14. itemClassName,
  15. item,
  16. targetPathOrId,
  17. isEnableActions,
  18. isReadOnlyUser,
  19. isWipPageShown = true,
  20. showAlternativeContent,
  21. onRenamed,
  22. onClick,
  23. onClickDuplicateMenuItem,
  24. onClickDeleteMenuItem,
  25. onWheelClick,
  26. onToggle,
  27. } = props;
  28. const page = item.getItemData();
  29. const itemLevel = item.getItemMeta().level;
  30. const toggleHandler = useCallback(() => {
  31. if (item.isExpanded()) {
  32. item.collapse();
  33. } else {
  34. item.expand();
  35. }
  36. onToggle?.();
  37. }, [item, onToggle]);
  38. const itemClickHandler = useCallback(
  39. (e: MouseEvent) => {
  40. // DO NOT handle the event when e.currentTarget and e.target is different
  41. if (e.target !== e.currentTarget) {
  42. return;
  43. }
  44. onClick?.(page);
  45. },
  46. [onClick, page],
  47. );
  48. const itemMouseupHandler = useCallback(
  49. (e: MouseEvent) => {
  50. // DO NOT handle the event when e.currentTarget and e.target is different
  51. if (e.target !== e.currentTarget) {
  52. return;
  53. }
  54. if (e.button === 1) {
  55. e.preventDefault();
  56. onWheelClick?.(page);
  57. }
  58. },
  59. [onWheelClick, page],
  60. );
  61. // Use item.isFolder() which is evaluated by headless-tree's isItemFolder config
  62. // This will be re-evaluated after rebuildTree()
  63. const hasDescendants = item.isFolder();
  64. const isSelected = useMemo(() => {
  65. return page._id === targetPathOrId || page.path === targetPathOrId;
  66. }, [page, targetPathOrId]);
  67. // Check if this item is a drag target (being dragged over)
  68. const isDragTarget = item.isDragTarget?.() ?? false;
  69. const toolProps: TreeItemToolProps = {
  70. item,
  71. isEnableActions,
  72. isReadOnlyUser,
  73. onRenamed,
  74. onClickDuplicateMenuItem,
  75. onClickDeleteMenuItem,
  76. };
  77. const EndComponents = props.customEndComponents;
  78. const HoveredEndComponents = props.customHoveredEndComponents;
  79. const AlternativeComponents = props.customAlternativeComponents;
  80. if (!isWipPageShown && page.wip) {
  81. // biome-ignore lint/complexity/noUselessFragments: ignore
  82. return <></>;
  83. }
  84. return (
  85. <div
  86. id={`tree-item-layout-${page._id}`}
  87. data-testid="grw-pagetree-item-container"
  88. className={`${moduleClass} ${className}`}
  89. style={{ paddingLeft: `${itemLevel > 0 ? indentSize * itemLevel : 0}px` }}
  90. >
  91. {/* biome-ignore lint/a11y/useKeyWithClickEvents: tree item interaction */}
  92. <li
  93. className={`list-group-item list-group-item-action
  94. ${isSelected ? 'active' : ''}
  95. ${isDragTarget ? 'drag-target' : ''}
  96. ${itemClassName ?? ''}
  97. border-0 py-0 ps-0 d-flex align-items-center rounded-1`}
  98. id={`grw-pagetree-list-${page._id}`}
  99. onClick={itemClickHandler}
  100. onMouseUp={itemMouseupHandler}
  101. >
  102. <div className="btn-triangle-container d-flex justify-content-center">
  103. {hasDescendants && (
  104. <button
  105. type="button"
  106. className={`btn btn-triangle p-0 ${item.isExpanded() ? 'open' : ''}`}
  107. onClick={toggleHandler}
  108. >
  109. <div className="d-flex justify-content-center">
  110. <span className="material-symbols-outlined fs-5">
  111. arrow_right
  112. </span>
  113. </div>
  114. </button>
  115. )}
  116. </div>
  117. {showAlternativeContent && AlternativeComponents != null ? (
  118. AlternativeComponents.map((AlternativeContent, index) => (
  119. // biome-ignore lint/suspicious/noArrayIndexKey: static component list
  120. <AlternativeContent key={index} {...toolProps} />
  121. ))
  122. ) : (
  123. <>
  124. <SimpleItemContent page={page} />
  125. <div className="d-hover-none">
  126. {EndComponents?.map((EndComponent, index) => (
  127. // biome-ignore lint/suspicious/noArrayIndexKey: static component list
  128. <EndComponent key={index} {...toolProps} />
  129. ))}
  130. </div>
  131. <div className="d-none d-hover-flex">
  132. {HoveredEndComponents?.map((HoveredEndContent, index) => (
  133. // biome-ignore lint/suspicious/noArrayIndexKey: static component list
  134. <HoveredEndContent key={index} {...toolProps} />
  135. ))}
  136. </div>
  137. </>
  138. )}
  139. </li>
  140. </div>
  141. );
  142. };