Просмотр исходного кода

Merge pull request #10463 from growilabs/feat/160341-172734-add-editor-guide

feat: Editor Guide button and modal
Yuki Takei 5 месяцев назад
Родитель
Сommit
1727ac6016

+ 72 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/EditorGuideModal.tsx

@@ -0,0 +1,72 @@
+import {
+  useState, useEffect, useLayoutEffect, type JSX, type RefObject,
+} from 'react';
+
+import { useEditorGuideModalStatus, useEditorGuideModalActions } from '@growi/editor/dist/states/modal/editor-guide';
+import { createPortal } from 'react-dom';
+
+type Props = {
+  containerRef: RefObject<HTMLDivElement | null>,
+};
+
+/**
+ * EditorGuideModal
+ *
+ * This modal overlays only the preview area (specified by containerRef),
+ * not the entire screen. Uses createPortal to render into document.body.
+ */
+export const EditorGuideModal = ({ containerRef }: Props): JSX.Element => {
+  const { isOpened } = useEditorGuideModalStatus();
+  const { close } = useEditorGuideModalActions();
+  const [isShown, setIsShown] = useState(false);
+  const [rect, setRect] = useState<DOMRect | null>(null);
+
+  // Get rect on open and on resize
+  useLayoutEffect(() => {
+    if (!isOpened || containerRef.current == null) return;
+
+    const updateRect = () => setRect(containerRef.current?.getBoundingClientRect() ?? null);
+    updateRect();
+    window.addEventListener('resize', updateRect);
+    return () => window.removeEventListener('resize', updateRect);
+  }, [isOpened, containerRef]);
+
+  // Trigger fade-in after mount
+  useEffect(() => {
+    if (!isOpened) {
+      setIsShown(false);
+      return;
+    }
+    const id = requestAnimationFrame(() => setIsShown(true));
+    return () => cancelAnimationFrame(id);
+  }, [isOpened]);
+
+  if (!isOpened || rect == null) return <></>;
+
+  const style = {
+    position: 'fixed' as const, top: rect.top, left: rect.left, width: rect.width, height: rect.height,
+  };
+
+  return createPortal(
+    <>
+      <div className={`modal-backdrop fade z-2 ${isShown ? 'show' : ''}`} style={style} onClick={close} aria-hidden="true" />
+      <div className={`d-flex align-items-center justify-content-center z-3 pe-none fade ${isShown ? 'show' : ''}`} style={style}>
+        <div className="px-3 pe-auto">
+          <div className="card shadow-lg">
+            <div className="card-header d-flex justify-content-between align-items-center">
+              <h5 className="mb-0">Editor Guide</h5>
+              <button type="button" className="btn-close" onClick={close} aria-label="Close" />
+            </div>
+            <div className="card-body overflow-auto">
+              <p>This is a test modal.</p>
+              <p>It appears in the center of the preview area on the right side.</p>
+              <p>The background is darkened to emphasize the modal.</p>
+              <p className="mb-0">Click the close button or the background to close.</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </>,
+    document.body,
+  );
+};

+ 23 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/dynamic.tsx

@@ -0,0 +1,23 @@
+import type { JSX, RefObject } from 'react';
+
+import { useEditorGuideModalStatus } from '@growi/editor/dist/states/modal/editor-guide';
+
+import { useLazyLoader } from '~/components/utils/use-lazy-loader';
+
+type Props = {
+  containerRef: RefObject<HTMLDivElement | null>,
+};
+
+export const EditorGuideModalLazyLoaded = ({ containerRef }: Props): JSX.Element => {
+  const { isOpened } = useEditorGuideModalStatus();
+
+  const EditorGuideModal = useLazyLoader(
+    'editor-guide-modal',
+    () => import('./EditorGuideModal').then(mod => ({ default: mod.EditorGuideModal })),
+    isOpened,
+  );
+
+  return (EditorGuideModal != null)
+    ? <EditorGuideModal containerRef={containerRef} />
+    : <></>;
+};

+ 1 - 0
apps/app/src/client/components/PageEditor/EditorGuideModal/index.ts

@@ -0,0 +1 @@
+export { EditorGuideModalLazyLoaded } from './dynamic';

+ 34 - 30
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -54,6 +54,7 @@ import { mutatePageTree, mutateRecentlyUpdated } from '~/stores/page-listing';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
+import { EditorGuideModalLazyLoaded } from './EditorGuideModal';
 import { EditorNavbar } from './EditorNavbar';
 import { EditorNavbarBottom } from './EditorNavbarBottom';
 import Preview from './Preview';
@@ -371,37 +372,40 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
   }
 
   return (
-    <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
-      <div className="page-editor-editor-container flex-expand-vert border-end">
-        <CodeMirrorEditorMain
-          enableUnifiedMergeView={isEnableUnifiedMergeView}
-          enableCollaboration={editorMode === EditorMode.Editor}
-          onSave={saveWithShortcut}
-          onUpload={uploadHandler}
-          acceptedUploadFileType={acceptedUploadFileType}
-          onScroll={scrollEditorHandlerThrottle}
-          indentSize={currentIndentSize ?? defaultIndentSize}
-          user={user ?? undefined}
-          pageId={pageId ?? undefined}
-          editorSettings={editorSettings}
-          onEditorsUpdated={setEditingClients}
-          cmProps={cmProps}
-        />
+    <>
+      <div className={`flex-expand-horiz ${props.visibility ? '' : 'd-none'}`}>
+        <div className="page-editor-editor-container flex-expand-vert border-end">
+          <CodeMirrorEditorMain
+            enableUnifiedMergeView={isEnableUnifiedMergeView}
+            enableCollaboration={editorMode === EditorMode.Editor}
+            onSave={saveWithShortcut}
+            onUpload={uploadHandler}
+            acceptedUploadFileType={acceptedUploadFileType}
+            onScroll={scrollEditorHandlerThrottle}
+            indentSize={currentIndentSize ?? defaultIndentSize}
+            user={user ?? undefined}
+            pageId={pageId ?? undefined}
+            editorSettings={editorSettings}
+            onEditorsUpdated={setEditingClients}
+            cmProps={cmProps}
+          />
+        </div>
+        <div
+          ref={previewRef}
+          onScroll={scrollPreviewHandlerThrottle}
+          className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex position-relative"
+        >
+          <Preview
+            rendererOptions={rendererOptions}
+            markdown={markdownToPreview}
+            pagePath={currentPagePath}
+            expandContentWidth={shouldExpandContent}
+            style={pastEndStyle}
+          />
+        </div>
       </div>
-      <div
-        ref={previewRef}
-        onScroll={scrollPreviewHandlerThrottle}
-        className="page-editor-preview-container flex-expand-vert overflow-y-auto d-none d-lg-flex"
-      >
-        <Preview
-          rendererOptions={rendererOptions}
-          markdown={markdownToPreview}
-          pagePath={currentPagePath}
-          expandContentWidth={shouldExpandContent}
-          style={pastEndStyle}
-        />
-      </div>
-    </div>
+      <EditorGuideModalLazyLoaded containerRef={previewRef} />
+    </>
   );
 };
 

+ 4 - 0
packages/custom-icons/svg/editor_guide.svg

@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+<defs><style>.a{fill:none;}</style></defs>
+<path d="M10.2163 1.15039C11.7248 0.903833 13.1901 0.947105 14.5825 1.32422C15.9893 1.65785 17.2657 2.25241 18.397 3.0791C19.5428 3.87686 20.5149 4.90689 21.2837 6.1543C22.0814 7.35815 22.6035 8.70726 22.8501 10.2012C23.0967 11.7096 23.0379 13.1606 22.6753 14.582C22.3562 15.9888 21.7617 17.28 20.9351 18.4258C20.1228 19.557 19.0926 20.5138 17.8599 21.3115C16.656 22.0802 15.3069 22.603 13.813 22.8496C12.3047 23.0961 10.8544 23.0376 9.43311 22.7041C8.72237 22.53 8.06936 22.2981 7.43115 22.0225L1.04932 22.9072L2.49951 17.541C1.83236 16.4098 1.38298 15.162 1.15088 13.7842C0.904307 12.2757 0.947333 10.8104 1.29541 9.41797C1.65798 8.01121 2.23821 6.73477 3.05029 5.60352C3.877 4.45772 4.89271 3.47102 6.12549 2.7168C7.3584 1.91903 8.70781 1.39697 10.2163 1.15039ZM17.4976 4.29785C15.3944 2.80386 13.0443 2.25221 10.4624 2.67285C7.86616 3.10804 5.83594 4.38515 4.31299 6.48828C2.8191 8.59138 2.28208 10.9408 2.70264 13.5225C2.78966 14.1171 2.93499 14.6829 3.12354 15.2051C3.35561 15.9158 3.68908 16.5832 4.09521 17.1924L3.5874 19.0781L3.02197 21.1523L5.41455 20.8184L7.8667 20.4854C8.43226 20.7753 9.01238 20.9927 9.60693 21.1377C10.8543 21.4568 12.1452 21.5152 13.5376 21.2832C16.1195 20.8626 18.1648 19.6003 19.6733 17.4971V17.4824C21.1672 15.3793 21.7324 13.0298 21.2974 10.4336C20.8767 7.85178 19.6007 5.80634 17.4976 4.29785ZM12.7983 17.6279H11.2173V10.666H12.7983V17.6279ZM12.0151 7.14062C12.2471 7.14067 12.4504 7.22833 12.6099 7.40234C12.7692 7.57632 12.856 7.76497 12.856 7.98242C12.8559 8.22878 12.7692 8.43169 12.6099 8.62012C12.4504 8.77963 12.2471 8.8525 12.0151 8.85254C11.7686 8.85254 11.5653 8.77967 11.4058 8.62012C11.2463 8.43166 11.1587 8.22883 11.1587 7.98242C11.1587 7.73585 11.2462 7.5471 11.4058 7.37305C11.5653 7.21349 11.7686 7.14062 12.0151 7.14062Z"/>
+</svg>

+ 22 - 0
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/EditorGuideButton.tsx

@@ -0,0 +1,22 @@
+import { type JSX, useCallback } from 'react';
+
+import { useEditorGuideModalActions } from '../../../../states/modal/editor-guide';
+
+export const EditorGuideButton = (): JSX.Element => {
+  const { open: openEditorGuideModal } = useEditorGuideModalActions();
+
+  const onClickEditorGuideButton = useCallback(() => {
+    openEditorGuideModal();
+  }, [openEditorGuideModal]);
+
+  return (
+    <button
+      type="button"
+      className="btn btn-toolbar-button d-none d-lg-block"
+      onClick={onClickEditorGuideButton}
+      aria-label="Open Editor Guide"
+    >
+      <span className="growi-custom-icons fs-6">editor_guide</span>
+    </button>
+  );
+};

+ 2 - 0
packages/editor/src/client/components-internal/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -5,6 +5,7 @@ import SimpleBar from 'simplebar-react';
 import type { GlobalCodeMirrorEditorKey } from '../../../../consts';
 import { AttachmentsDropup } from './AttachmentsDropup';
 import { DiagramButton } from './DiagramButton';
+import { EditorGuideButton } from './EditorGuideButton';
 import { EmojiButton } from './EmojiButton';
 import { TableButton } from './TableButton';
 import { TemplateButton } from './TemplateButton';
@@ -55,6 +56,7 @@ export const Toolbar = memo((props: Props): JSX.Element => {
               <TableButton editorKey={editorKey} />
               <DiagramButton editorKey={editorKey} />
               <TemplateButton editorKey={editorKey} />
+              <EditorGuideButton />
             </div>
           </SimpleBar>
         </div>

+ 26 - 0
packages/editor/src/states/modal/editor-guide.ts

@@ -0,0 +1,26 @@
+import { atom, useAtomValue, useSetAtom } from 'jotai';
+
+export type EditorGuideModalState = {
+  isOpened: boolean;
+};
+
+const editorGuideModalAtom = atom<EditorGuideModalState>({
+  isOpened: false,
+});
+
+export const useEditorGuideModalStatus = () => {
+  return useAtomValue(editorGuideModalAtom);
+};
+
+export const useEditorGuideModalActions = () => {
+  const setModalState = useSetAtom(editorGuideModalAtom);
+
+  return {
+    open: () => {
+      setModalState({ isOpened: true });
+    },
+    close: () => {
+      setModalState({ isOpened: false });
+    },
+  };
+};