TreeItemLayout.tsx 4.8 KB

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