Header.tsx 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { useCallback, useEffect, useState } from 'react';
  2. import EventEmitter from 'events';
  3. import { useRouter } from 'next/router';
  4. import { Element } from 'react-markdown/lib/rehype-filter';
  5. import { useIsGuestUser, useIsSharedUser, useShareLinkId } from '~/stores/context';
  6. import { NextLink } from './NextLink';
  7. import styles from './Header.module.scss';
  8. declare global {
  9. // eslint-disable-next-line vars-on-top, no-var
  10. var globalEmitter: EventEmitter;
  11. }
  12. function setCaretLine(line?: number): void {
  13. if (line != null) {
  14. globalEmitter.emit('setCaretLine', line);
  15. }
  16. }
  17. type EditLinkProps = {
  18. line?: number,
  19. }
  20. /**
  21. * Inner FC to display edit link icon
  22. */
  23. const EditLink = (props: EditLinkProps): JSX.Element => {
  24. const isDisabled = props.line == null;
  25. return (
  26. <span className="revision-head-edit-button">
  27. <a href="#edit" aria-disabled={isDisabled} onClick={() => setCaretLine(props.line)}>
  28. <i className="icon-note"></i>
  29. </a>
  30. </span>
  31. );
  32. };
  33. type HeaderProps = {
  34. children: React.ReactNode,
  35. node: Element,
  36. level: number,
  37. id?: string,
  38. }
  39. export const Header = (props: HeaderProps): JSX.Element => {
  40. const {
  41. node, id, children, level,
  42. } = props;
  43. const { data: isGuestUser } = useIsGuestUser();
  44. const { data: isSharedUser } = useIsSharedUser();
  45. const { data: shareLinkId } = useShareLinkId();
  46. const router = useRouter();
  47. const [isActive, setActive] = useState(false);
  48. const CustomTag = `h${level}` as keyof JSX.IntrinsicElements;
  49. const activateByHash = useCallback((url: string) => {
  50. const hash = (new URL(url, 'https://example.com')).hash.slice(1);
  51. setActive(hash === id);
  52. }, [id]);
  53. // init
  54. useEffect(() => {
  55. activateByHash(window.location.href);
  56. }, [activateByHash]);
  57. // update isActive when hash is changed
  58. useEffect(() => {
  59. router.events.on('hashChangeComplete', activateByHash);
  60. return () => {
  61. router.events.off('hashChangeComplete', activateByHash);
  62. };
  63. }, [activateByHash, router.events]);
  64. const showEditButton = !isGuestUser && !isSharedUser && shareLinkId == null;
  65. return (
  66. <CustomTag id={id} className={`revision-head ${styles['revision-head']} ${isActive ? 'blink' : ''}`}>
  67. {children}
  68. <NextLink href={`#${id}`} className="revision-head-link">
  69. <span className="icon-link"></span>
  70. </NextLink>
  71. {showEditButton && (
  72. <EditLink line={node.position?.start.line} />
  73. )}
  74. </CustomTag>
  75. );
  76. };