PagePathNav.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import React, {
  2. useEffect,
  3. useRef,
  4. useState,
  5. } from 'react';
  6. import { DevidedPagePath } from '@growi/core/dist/models';
  7. import { pagePathUtils } from '@growi/core/dist/utils';
  8. import dynamic from 'next/dynamic';
  9. import Sticky from 'react-stickynode';
  10. import { useIsNotFound } from '~/stores/page';
  11. import {
  12. usePageControlsX, useCurrentProductNavWidth, useSidebarMode,
  13. } from '~/stores/ui';
  14. import LinkedPagePath from '../../../models/linked-page-path';
  15. import { PagePathHierarchicalLink } from '../PagePathHierarchicalLink';
  16. import { CollapsedParentsDropdown } from '../PagePathHierarchicalLink/CollapsedParentsDropdown';
  17. import styles from './PagePathNav.module.scss';
  18. const { isTrashPage } = pagePathUtils;
  19. type Props = {
  20. pagePath: string,
  21. pageId?: string | null,
  22. isWipPage?: boolean,
  23. isSingleLineMode?: boolean,
  24. isCollapseParents?: boolean,
  25. formerLinkClassName?: string,
  26. latterLinkClassName?: string,
  27. maxWidth?: number,
  28. }
  29. const CopyDropdown = dynamic(() => import('../CopyDropdown').then(mod => mod.CopyDropdown), { ssr: false });
  30. const Separator = ({ className }: {className?: string}): JSX.Element => {
  31. return <span className={`separator ${className ?? ''} ${styles['grw-mx-02em']}`}>/</span>;
  32. };
  33. export const PagePathNav = (props: Props): JSX.Element => {
  34. const {
  35. pageId, pagePath, isWipPage, isSingleLineMode, isCollapseParents,
  36. formerLinkClassName, latterLinkClassName, maxWidth,
  37. } = props;
  38. const dPagePath = new DevidedPagePath(pagePath, false, true);
  39. const { data: isNotFound } = useIsNotFound();
  40. const isInTrash = isTrashPage(pagePath);
  41. let formerLink;
  42. let latterLink;
  43. // one line
  44. if (dPagePath.isRoot || dPagePath.isFormerRoot || (!isCollapseParents && isSingleLineMode)) {
  45. const linkedPagePath = new LinkedPagePath(pagePath);
  46. latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} isInTrash={isInTrash} />;
  47. }
  48. // collapse parents
  49. else if (isCollapseParents) {
  50. const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
  51. const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
  52. latterLink = (
  53. <>
  54. <CollapsedParentsDropdown linkedPagePath={linkedPagePathFormer} />
  55. <Separator />
  56. <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
  57. </>
  58. );
  59. }
  60. // two line
  61. else {
  62. const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
  63. const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
  64. formerLink = (
  65. <>
  66. <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} isInTrash={isInTrash} />
  67. <Separator />
  68. </>
  69. );
  70. latterLink = (
  71. <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.former} isInTrash={isInTrash} />
  72. );
  73. }
  74. const copyDropdownId = `copydropdown-${pageId}`;
  75. return (
  76. <div style={{ maxWidth }}>
  77. <span className={`${formerLinkClassName ?? ''} ${styles['grw-former-link']}`}>{formerLink}</span>
  78. <div className="d-flex align-items-center">
  79. <h1 className={`m-0 ${latterLinkClassName}`}>
  80. {latterLink}
  81. </h1>
  82. { pageId != null && !isNotFound && (
  83. <div className="d-flex align-items-center ms-2">
  84. { isWipPage && (
  85. <span className="badge rounded-pill text-bg-secondary ms-1 me-1">WIP</span>
  86. )}
  87. <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName="p-2">
  88. <span className="material-symbols-outlined">content_paste</span>
  89. </CopyDropdown>
  90. </div>
  91. ) }
  92. </div>
  93. </div>
  94. );
  95. };
  96. PagePathNav.displayName = 'PagePathNav';
  97. type PagePathNavStickyProps = Omit<Props, 'isCollapseParents'>;
  98. export const PagePathNavSticky = (props: PagePathNavStickyProps): JSX.Element => {
  99. const { data: pageControlsX } = usePageControlsX();
  100. const { data: sidebarWidth } = useCurrentProductNavWidth();
  101. const { data: sidebarMode } = useSidebarMode();
  102. const pagePathNavRef = useRef<HTMLDivElement>(null);
  103. const [navMaxWidth, setNavMaxWidth] = useState<number | undefined>();
  104. useEffect(() => {
  105. if (pageControlsX == null || pagePathNavRef.current == null || sidebarWidth == null) {
  106. return;
  107. }
  108. setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
  109. }, [pageControlsX, pagePathNavRef, sidebarWidth]);
  110. useEffect(() => {
  111. // wait for the end of the animation of the opening and closing of the sidebar
  112. const timeout = setTimeout(() => {
  113. if (pageControlsX == null || pagePathNavRef.current == null || sidebarMode == null) {
  114. return;
  115. }
  116. setNavMaxWidth(pageControlsX - pagePathNavRef.current.getBoundingClientRect().x - 10);
  117. }, 200);
  118. return () => {
  119. clearTimeout(timeout);
  120. };
  121. }, [pageControlsX, pagePathNavRef, sidebarMode]);
  122. return (
  123. // Controlling pointer-events
  124. // 1. disable pointer-events with 'pe-none'
  125. <div ref={pagePathNavRef}>
  126. <Sticky className={`${styles['grw-page-path-nav-sticky']} mb-4`} innerClass="mt-1 pe-none" innerActiveClass="active">
  127. {({ status }: { status: boolean }) => {
  128. const isCollapseParents = status === Sticky.STATUS_FIXED;
  129. return (
  130. // Controlling pointer-events
  131. // 2. enable pointer-events with 'pe-auto' only against the children
  132. // which width is minimized by 'd-inline-block'
  133. //
  134. <div className="d-inline-block pe-auto">
  135. <PagePathNav
  136. {...props}
  137. isCollapseParents={isCollapseParents}
  138. latterLinkClassName={isCollapseParents ? 'fs-3 text-truncate' : 'fs-2'}
  139. maxWidth={isCollapseParents ? navMaxWidth : undefined}
  140. />
  141. </div>
  142. );
  143. }}
  144. </Sticky>
  145. </div>
  146. );
  147. };
  148. PagePathNavSticky.displayName = 'PagePathNavSticky';