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

Merge pull request #8441 from weseek/fix/140019-link-edit-modal

fix: LinkEditModal
Yuki Takei 2 лет назад
Родитель
Сommit
a5b4ee2840

+ 1 - 2
apps/app/src/components/PageEditor/MarkdownLinkUtil.js → apps/app/_obsolete/src/components/PageEditor/MarkdownLinkUtil.js

@@ -1,5 +1,4 @@
-import Linker from '~/client/models/Linker';
-
+import Linker from '@growi/editor/src/services/link-util/Linker';
 /**
  * Utility for markdown link
  */

+ 2 - 3
apps/app/src/components/PageEditor/LinkEditModal.tsx

@@ -2,6 +2,8 @@ import React, { useEffect, useState, useCallback } from 'react';
 
 import path from 'path';
 
+import Linker from '@growi/editor/src/services/link-util/Linker';
+import { useLinkEditModal } from '@growi/editor/src/stores/use-link-edit-modal';
 import { useTranslation } from 'next-i18next';
 import {
   Modal,
@@ -13,10 +15,7 @@ import {
 } from 'reactstrap';
 import validator from 'validator';
 
-
-import Linker from '~/client/models/Linker';
 import { apiv3Get } from '~/client/util/apiv3-client';
-import { useLinkEditModal } from '~/stores/modal';
 import { useCurrentPagePath } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';

+ 3 - 32
apps/app/src/stores/modal.tsx

@@ -3,11 +3,11 @@ import { useCallback, useMemo } from 'react';
 import type {
   IAttachmentHasId, IPageToDeleteWithMeta, IPageToRenameWithMeta, IUserGroupHasId,
 } from '@growi/core';
-import { SWRResponse } from 'swr';
+import type { SWRResponse } from 'swr';
+
 
-import Linker from '~/client/models/Linker';
 import MarkdownTable from '~/client/models/MarkdownTable';
-import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import type { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import type {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction, onDeletedBookmarkFolderFunction, OnSelectedFunction,
 } from '~/interfaces/ui';
@@ -675,35 +675,6 @@ export const useDeleteAttachmentModal = (): SWRResponse<DeleteAttachmentModalSta
   };
 };
 
-/*
- * LinkEditModal
- */
-type LinkEditModalStatus = {
-  isOpened: boolean,
-  defaultMarkdownLink?: Linker,
-  onSave?: (linkText: string) => void
-}
-
-type LinkEditModalUtils = {
-  open(defaultMarkdownLink: Linker, onSave: (linkText: string) => void): void,
-  close(): void,
-}
-
-export const useLinkEditModal = (): SWRResponse<LinkEditModalStatus, Error> & LinkEditModalUtils => {
-
-  const initialStatus: LinkEditModalStatus = { isOpened: false };
-  const swrResponse = useStaticSWR<LinkEditModalStatus, Error>('linkEditModal', undefined, { fallbackData: initialStatus });
-
-  return Object.assign(swrResponse, {
-    open: (defaultMarkdownLink: Linker, onSave: (linkText: string) => void) => {
-      swrResponse.mutate({ isOpened: true, defaultMarkdownLink, onSave });
-    },
-    close: () => {
-      swrResponse.mutate({ isOpened: false });
-    },
-  });
-};
-
 /*
 * PageSelectModal
 */

+ 5 - 6
packages/editor/src/components/CodeMirrorEditor/Toolbar/AttachmentsDropup.tsx

@@ -5,19 +5,21 @@ import {
   DropdownItem,
 } from 'reactstrap';
 
+import type { GlobalCodeMirrorEditorKey } from '../../../consts';
 import { AcceptedUploadFileType } from '../../../consts/accepted-upload-file-type';
 
 import { AttachmentsButton } from './AttachmentsButton';
-
+import { LinkEditButton } from './LinkEditButton';
 
 type Props = {
+  editorKey: string | GlobalCodeMirrorEditorKey,
   onFileOpen: () => void,
   acceptedFileType: AcceptedUploadFileType,
 }
 
 export const AttachmentsDropup = (props: Props): JSX.Element => {
+  const { onFileOpen, acceptedFileType, editorKey } = props;
 
-  const { onFileOpen, acceptedFileType } = props;
   return (
     <>
       <UncontrolledDropdown direction="up" className="lh-1">
@@ -31,10 +33,7 @@ export const AttachmentsDropup = (props: Props): JSX.Element => {
           </DropdownItem>
           <DropdownItem divider />
           <AttachmentsButton onFileOpen={onFileOpen} acceptedFileType={acceptedFileType} />
-          <DropdownItem className="d-flex gap-1 align-items-center">
-            <span className="material-symbols-outlined fs-5">link</span>
-            Link
-          </DropdownItem>
+          <LinkEditButton editorKey={editorKey} />
         </DropdownMenu>
       </UncontrolledDropdown>
     </>

+ 39 - 0
packages/editor/src/components/CodeMirrorEditor/Toolbar/LinkEditButton.tsx

@@ -0,0 +1,39 @@
+import { useCallback } from 'react';
+
+import { DropdownItem } from 'reactstrap';
+
+import type { GlobalCodeMirrorEditorKey } from '../../../consts';
+import { getMarkdownLink, replaceFocusedMarkdownLinkWithEditor } from '../../../services/link-util/markdown-link-util';
+import { useCodeMirrorEditorIsolated } from '../../../stores';
+import { useLinkEditModal } from '../../../stores/use-link-edit-modal';
+
+type Props = {
+  editorKey: string | GlobalCodeMirrorEditorKey,
+}
+
+export const LinkEditButton = (props: Props): JSX.Element => {
+  const { editorKey } = props;
+  const { open: openLinkEditModal } = useLinkEditModal();
+  const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(editorKey);
+
+  const onClickOpenLinkEditModal = useCallback(() => {
+    const editor = codeMirrorEditor?.view;
+    if (editor == null) {
+      return;
+    }
+    const onSubmit = (linkText: string) => {
+      replaceFocusedMarkdownLinkWithEditor(editor, linkText);
+      return;
+    };
+
+    const defaultMarkdownLink = getMarkdownLink(editor);
+
+    openLinkEditModal(defaultMarkdownLink, onSubmit);
+  }, [codeMirrorEditor?.view, openLinkEditModal]);
+
+  return (
+    <DropdownItem className="d-flex gap-1 align-items-center" onClick={onClickOpenLinkEditModal}>
+      <span className="material-symbols-outlined fs-5">link</span>Link
+    </DropdownItem>
+  );
+};

+ 1 - 1
packages/editor/src/components/CodeMirrorEditor/Toolbar/Toolbar.tsx

@@ -22,7 +22,7 @@ export const Toolbar = memo((props: Props): JSX.Element => {
   const { editorKey, onFileOpen, acceptedFileType } = props;
   return (
     <div className={`d-flex gap-2 p-2 codemirror-editor-toolbar ${styles['codemirror-editor-toolbar']}`}>
-      <AttachmentsDropup onFileOpen={onFileOpen} acceptedFileType={acceptedFileType} />
+      <AttachmentsDropup editorKey={editorKey} onFileOpen={onFileOpen} acceptedFileType={acceptedFileType} />
       <TextFormatTools editorKey={editorKey} />
       <EmojiButton
         editorKey={editorKey}

+ 27 - 23
apps/app/src/client/models/Linker.js → packages/editor/src/services/link-util/Linker.ts

@@ -1,14 +1,14 @@
-
 import { encodeSpaces } from '@growi/core/dist/utils/page-path-utils';
 
 export default class Linker {
 
-  constructor(
-      type = Linker.types.markdownLink,
-      label = '',
-      link = '',
-  ) {
+  type: string;
+
+  label: string | undefined;
+
+  link: string | undefined;
 
+  constructor(type = Linker.types.markdownLink, label = '', link = '') {
     this.type = type;
     this.label = label;
     this.link = link;
@@ -33,7 +33,7 @@ export default class Linker {
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
   };
 
-  initWhenMarkdownLink() {
+  initWhenMarkdownLink(): void {
     // fill label with link if empty
     if (this.label === '') {
       this.label = this.link;
@@ -42,7 +42,7 @@ export default class Linker {
     this.link = encodeSpaces(this.link);
   }
 
-  generateMarkdownText() {
+  generateMarkdownText(): string | undefined {
     if (this.type === Linker.types.pukiwikiLink) {
       if (this.label === '') return `[[${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
@@ -56,7 +56,7 @@ export default class Linker {
   }
 
   // create an instance of Linker from string
-  static fromMarkdownString(str) {
+  static fromMarkdownString(str: string): Linker {
     // if str doesn't mean a linker, create a link whose label is str
     let label = str;
     let link = '';
@@ -65,23 +65,27 @@ export default class Linker {
     // pukiwiki with separator ">".
     if (str.match(this.patterns.pukiwikiLinkWithLabel)) {
       type = this.types.pukiwikiLink;
-      ({ label, link } = str.match(this.patterns.pukiwikiLinkWithLabel).groups);
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ({ label, link } = str.match(this.patterns.pukiwikiLinkWithLabel)!.groups!);
     }
     // pukiwiki without separator ">".
     else if (str.match(this.patterns.pukiwikiLinkWithoutLabel)) {
       type = this.types.pukiwikiLink;
-      ({ label } = str.match(this.patterns.pukiwikiLinkWithoutLabel).groups);
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ({ label } = str.match(this.patterns.pukiwikiLinkWithoutLabel)!.groups!);
       link = label;
     }
     // markdown
     else if (str.match(this.patterns.markdownLink)) {
       type = this.types.markdownLink;
-      ({ label, link } = str.match(this.patterns.markdownLink).groups);
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ({ label, link } = str.match(this.patterns.markdownLink)!.groups!);
     }
     // growi
     else if (str.match(this.patterns.growiLink)) {
       type = this.types.growiLink;
-      ({ label } = str.match(this.patterns.growiLink).groups);
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ({ label } = str.match(this.patterns.growiLink)!.groups!);
       link = label;
     }
 
@@ -93,7 +97,7 @@ export default class Linker {
   }
 
   // create an instance of Linker from text with index
-  static fromLineWithIndex(line, index) {
+  static fromLineWithIndex(line: string, index: number): Linker {
     const { beginningOfLink, endOfLink } = this.getBeginningAndEndIndexOfLink(line, index);
     // if index is in a link, extract it from line
     let linkStr = '';
@@ -103,11 +107,11 @@ export default class Linker {
     return this.fromMarkdownString(linkStr);
   }
 
-  // return beginning and end indexies of link
+  // return beginning and end indices of link
   // if index is not in a link, return { beginningOfLink: -1, endOfLink: -1 }
-  static getBeginningAndEndIndexOfLink(line, index) {
-    let beginningOfLink;
-    let endOfLink;
+  static getBeginningAndEndIndexOfLink(line: string, index: number): { beginningOfLink: number; endOfLink: number } {
+    let beginningOfLink: number;
+    let endOfLink: number;
 
     // pukiwiki link ('[[link]]')
     [beginningOfLink, endOfLink] = this.getBeginningAndEndIndexWithPrefixAndSuffix(line, index, '[[', ']]');
@@ -130,13 +134,13 @@ export default class Linker {
     return { beginningOfLink, endOfLink };
   }
 
-  // return begin and end indexies as array only when index is between prefix and suffix and link contains containText.
-  static getBeginningAndEndIndexWithPrefixAndSuffix(line, index, prefix, suffix, containText = '') {
+  // return begin and end indices as an array only when index is between prefix and suffix and link contains containText.
+  static getBeginningAndEndIndexWithPrefixAndSuffix(line: string, index: number, prefix: string, suffix: string, containText = ''): [number, number] {
     const beginningIndex = line.lastIndexOf(prefix, index);
-    const IndexOfContainText = line.indexOf(containText, beginningIndex + prefix.length);
-    const endIndex = line.indexOf(suffix, IndexOfContainText + containText.length);
+    const indexOfContainText = line.indexOf(containText, beginningIndex + prefix.length);
+    const endIndex = line.indexOf(suffix, indexOfContainText + containText.length);
 
-    if (beginningIndex < 0 || IndexOfContainText < 0 || endIndex < 0) {
+    if (beginningIndex < 0 || indexOfContainText < 0 || endIndex < 0) {
       return [-1, -1];
     }
     return [beginningIndex, endIndex + suffix.length];

+ 41 - 0
packages/editor/src/services/link-util/markdown-link-util.ts

@@ -0,0 +1,41 @@
+import type { EditorView } from '@codemirror/view';
+
+import Linker from './Linker';
+
+const curPos = (editor: EditorView) => {
+  return editor.state.selection.main.head;
+};
+
+const doc = (editor: EditorView) => {
+  return editor.state.doc;
+};
+
+const getCursorLine = (editor: EditorView) => {
+  return doc(editor).lineAt(curPos(editor));
+
+};
+
+export const isInLink = (editor: EditorView): boolean => {
+  const cursorLine = getCursorLine(editor);
+  const startPos = curPos(editor) - cursorLine.from;
+
+  const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(cursorLine.text, startPos);
+  return beginningOfLink >= 0 && endOfLink >= 0;
+};
+export const getMarkdownLink = (editor: EditorView): Linker => {
+  if (!isInLink(editor)) {
+    const selection = editor?.state.sliceDoc(
+      editor?.state.selection.main.from,
+      editor?.state.selection.main.to,
+    );
+    return Linker.fromMarkdownString(selection);
+  }
+
+  const cursorLine = getCursorLine(editor);
+  const startPos = curPos(editor) - cursorLine.from;
+  return Linker.fromLineWithIndex(cursorLine.text, startPos);
+};
+
+export const replaceFocusedMarkdownLinkWithEditor = (editor: EditorView, linkText: string): void => {
+  editor.dispatch(editor.state.replaceSelection(linkText));
+};

+ 30 - 0
packages/editor/src/stores/use-link-edit-modal.ts

@@ -0,0 +1,30 @@
+import { useSWRStatic } from '@growi/core/dist/swr';
+import { SWRResponse } from 'swr';
+
+import Linker from '../services/link-util/Linker';
+
+type LinkEditModalStatus = {
+  isOpened: boolean,
+  defaultMarkdownLink?: Linker,
+  onSave?: (linkText: string) => void
+}
+
+type LinkEditModalUtils = {
+  open(defaultMarkdownLink: Linker, onSave: (linkText: string) => void): void,
+  close(): void,
+}
+
+export const useLinkEditModal = (): SWRResponse<LinkEditModalStatus, Error> & LinkEditModalUtils => {
+
+  const initialStatus: LinkEditModalStatus = { isOpened: false };
+  const swrResponse = useSWRStatic<LinkEditModalStatus, Error>('linkEditModal', undefined, { fallbackData: initialStatus });
+
+  return Object.assign(swrResponse, {
+    open: (defaultMarkdownLink: Linker, onSave: (linkText: string) => void) => {
+      swrResponse.mutate({ isOpened: true, defaultMarkdownLink, onSave });
+    },
+    close: () => {
+      swrResponse.mutate({ isOpened: false });
+    },
+  });
+};