Jelajahi Sumber

Merge pull request #7399 from weseek/imprv/presentation-header-separator

imprv: Presentation behavior
Yuki Takei 3 tahun lalu
induk
melakukan
aa6f78b4e2

+ 21 - 8
packages/app/src/components/PagePresentationModal.tsx

@@ -1,13 +1,13 @@
 import React, { useCallback } from 'react';
 
 import type { PresentationProps } from '@growi/presentation';
+import { useFullScreen } from '@growi/ui';
 import dynamic from 'next/dynamic';
 import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import {
   Modal, ModalBody,
 } from 'reactstrap';
 
-
 import { usePagePresentationModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
@@ -30,13 +30,26 @@ const PagePresentationModal = (): JSX.Element => {
   const { data: presentationModalData, close: closePresentationModal } = usePagePresentationModal();
 
   const { isDarkMode } = useNextThemes();
+  const fullscreen = useFullScreen();
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: rendererOptions } = usePresentationViewOptions();
 
-  const requestFullscreen = useCallback(() => {
-    document.documentElement.requestFullscreen();
-  }, []);
+  const toggleFullscreenHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    else {
+      fullscreen.enter();
+    }
+  }, [fullscreen]);
+
+  const closeHandler = useCallback(() => {
+    if (fullscreen.active) {
+      fullscreen.exit();
+    }
+    closePresentationModal();
+  }, [fullscreen, closePresentationModal]);
 
   const isOpen = presentationModalData?.isOpened ?? false;
 
@@ -49,15 +62,15 @@ const PagePresentationModal = (): JSX.Element => {
   return (
     <Modal
       isOpen={isOpen}
-      toggle={closePresentationModal}
+      toggle={closeHandler}
       data-testid="page-presentation-modal"
       className={`grw-presentation-modal ${styles['grw-presentation-modal']}`}
     >
       <div className="grw-presentation-controls d-flex">
-        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={requestFullscreen}>
-          <i className="ti ti-fullscreen" aria-hidden></i>
+        <button className="close btn-fullscreen" type="button" aria-label="fullscreen" onClick={toggleFullscreenHandler}>
+          <i className={`${fullscreen.active ? 'icon-size-actual' : 'icon-size-fullscreen'}`} aria-hidden></i>
         </button>
-        <button className="close btn-close" type="button" aria-label="close" onClick={closePresentationModal}>
+        <button className="close btn-close" type="button" aria-label="close" onClick={closeHandler}>
           <i className="ti ti-close" aria-hidden></i>
         </button>
       </div>

+ 9 - 3
packages/presentation/src/components/Slides.tsx

@@ -32,9 +32,15 @@ type Props = {
 
 export const Slides = (props: Props): JSX.Element => {
   const { options, children } = props;
-  const { rendererOptions, isDarkMode } = options;
-
-  rendererOptions.remarkPlugins?.push([extractSections.remarkPlugin, { isDarkMode }]);
+  const { rendererOptions, isDarkMode, disableSeparationByHeader } = options;
+
+  rendererOptions.remarkPlugins?.push([
+    extractSections.remarkPlugin,
+    {
+      isDarkMode,
+      disableSeparationByHeader,
+    },
+  ]);
 
   const { css } = marp.render('', { htmlAsArray: true });
 

+ 1 - 0
packages/presentation/src/consts/index.ts

@@ -5,4 +5,5 @@ export type PresentationOptions = {
   rendererOptions: ReactMarkdownOptions,
   revealOptions?: RevealOptions,
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }

+ 19 - 4
packages/presentation/src/services/renderer/extract-sections.ts

@@ -37,33 +37,48 @@ function removeElement(parentNode: Parent, elem: Node): void {
 
 export type ExtractSectionsPluginParams = {
   isDarkMode?: boolean,
+  disableSeparationByHeader?: boolean,
 }
 
 export const remarkPlugin: Plugin<[ExtractSectionsPluginParams]> = (options) => {
-  const { isDarkMode } = options;
+  const { isDarkMode, disableSeparationByHeader } = options;
+
+  const startCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type !== 'thematicBreak';
+  };
+  const endCondition = (node: Node) => {
+    if (!disableSeparationByHeader && node.type === 'heading') {
+      return true;
+    }
+    return node.type === 'thematicBreak';
+  };
 
   return (tree) => {
     // wrap with <section>
     visit(
       tree,
-      node => node.type !== 'thematicBreak',
+      startCondition,
       (node, index, parent: Parent) => {
         if (parent == null || parent.type !== 'root') {
           return;
         }
 
         const startElem = node;
-        const endElem = findAfter(parent, startElem, node => node.type === 'thematicBreak');
+        const endElem = findAfter(parent, startElem, endCondition);
 
         wrapWithSection(parent, startElem, endElem, isDarkMode);
 
         // remove <hr>
-        if (endElem != null) {
+        if (endElem != null && endElem.type === 'thematicBreak') {
           removeElement(parent, endElem);
         }
       },
     );
   };
+
 };
 
 

+ 2 - 1
packages/ui/src/index.ts

@@ -3,7 +3,8 @@ export * from './interfaces/breakpoints';
 export * from './components/Attachment/Attachment';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
+export * from './components/SearchPage/FootstampIcon';
 export * from './components/User/UserPicture';
 
 export * from './utils/browser-utils';
-export * from './components/SearchPage/FootstampIcon';
+export * from './utils/use-fullscreen';

+ 50 - 0
packages/ui/src/utils/use-fullscreen.ts

@@ -0,0 +1,50 @@
+import {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+export interface FullScreenHandle {
+  active: boolean;
+  enter: () => Promise<void>;
+  exit: () => Promise<void>;
+}
+
+export const useFullScreen = (): FullScreenHandle => {
+  const [active, setActive] = useState(false);
+
+  useEffect(() => {
+    const handleChange = () => {
+      setActive(document.fullscreenElement != null);
+    };
+
+    document.addEventListener('fullscreenchange', handleChange);
+    return function cleanup() {
+      document.removeEventListener('fullscreenchange', handleChange);
+    };
+  }, []);
+
+  const enter = useCallback((elem?: HTMLElement) => {
+    if (document.fullscreenElement != null) {
+      return Promise.resolve();
+    }
+
+    const targetElem = elem ?? document.documentElement;
+    return targetElem.requestFullscreen();
+  }, []);
+
+  const exit = useCallback(() => {
+    if (document.fullscreenElement == null) {
+      return Promise.resolve();
+    }
+
+    return document.exitFullscreen();
+  }, []);
+
+  return useMemo(
+    () => ({
+      active,
+      enter,
+      exit,
+    }),
+    [active, enter, exit],
+  );
+};