实现在 Markdown 编辑器中图片的上传和缩放

370 阅读2分钟

Quiet 项目简介:juejin.cn/post/717122…

上上篇:

作为一个后端 Java 开发,为何、如何自己实现一个 Markdown 编辑器

上一篇:

如何结合 Minio 实现一个简单的可嵌入的 Spring Boot Starter 文件服务

前言

在上两篇文章中,我们实现了一个 Markdown 编辑器和文件上传的服务,现在可以实现 Markdown 图片上传的功能了。

问题分析

图片上传主要有两种操作方式,一种是使用快捷键 Ctrl/Command + CCtrl/Command + V 直接复制粘贴进编辑器,一种是点击 Toolbar 的图片上传图标,然后用户选择图片,点击上传。

  1. Ctrl/Command + V 的实现可以使用 document.onpaste 来获取粘贴板的信息,如果是图片,则上传文件到后端,然后在编辑器里面填入图片信息。
  2. 点击 Toolbar 我们可以结合 Arco Design 的上传组件实现图片上传,然后再在编辑器里面填入图片信息。

代码实现

  • 使用 Ctrl/Command + V ,这种方式的图片上传可以自定义 Monaco Editor 的 action 实现,但是直接复制文件然后粘贴的时候无法获取复制的文件(也可能是我的方式不对),只好用下面的方式实现复制粘贴。
document.onpaste = (event) => {
  if (!isFocus) {
    return;
  }
  const clipboardData = event.clipboardData;
  const file = clipboardData.files[0];
  if (file && file.type.startsWith('image/')) {
    const data = new FormData();
    data.append('files', file);
    data.append('classification', 'api/remark');
    req(`/doc/minio`, {
      method: 'POST',
      data,
    }).then((resp) => {
      const result: UploadResult[] = resp.data;
      result.every((value) => {
        addImage(value.user_metadata.original_file_name, value.view_path);
      });
    });
  }
};

function addImage(original_file_name: string, view_path: string) {
  const selection = editorRef.current.getSelection();
  const imageVal = `![${original_file_name}](${view_path})`;
  const newStartColumn = selection.startColumn - original_file_name.length;
  const newEndColumn = newStartColumn + imageVal.length;
  editorRef.current.getModel().pushEditOperations(
    [],
    [
      {
        forceMoveMarkers: true,
        range: {
          ...selection,
          startColumn: newStartColumn,
          endColumn: newEndColumn,
        },
        text: imageVal,
      },
    ],
    () => []
  );
  editorRef.current.setPosition({
    lineNumber: selection.endLineNumber,
    column: newEndColumn,
  });
  editorRef.current.focus();
}
  • 点击 Toolbar 的图片上传,实现图片上传,这种直接用文件上传组件就可以实现了
function handleUploadImage(options: RequestOptions) {
  const data = new FormData();
  data.append('files', options.file);
  data.append('classification', 'api/remark');
  req(`/doc/minio`, {
    method: 'POST',
    data,
  }).then((resp) => {
    const result: UploadResult[] = resp.data;
    result.every((value) => {
      setStartAndEndCharacters(
        `![${value.user_metadata.original_file_name}](${value.view_path})`,
        ''
      );
    });
  });
}

<Upload
  accept={'image/*'}
  customRequest={handleUploadImage}
  renderUploadList={() => <></>}
  renderUploadItem={() => <></>}
>
  <Option>
    <IconImage />
  </Option>
</Upload>

好了,到此,两种图片上传的方式都实现了,在上传图片之后发现图片的大小无法控制,会在 Viewer 占用大量空间,这样阅读起来就很不雅观,所以还需要实现图片缩放的功能。

图片缩放

图片缩放的过程:

  1. 读取光标所在的行
  2. 使用正则匹配,获取 Markdown 的图片文本
  3. 替换成 html 的图片标签,并设置缩放比例
  4. 使用正则匹配,获取 html 的图片标签
  5. 替换 width 的缩放比例
  6. 如果没有匹配到,则添加一个空的图片缩放标签
const ImageScale: number[] = [30, 50, 70, 100];

function handleImageScaling(value: number) {
  const position = editorRef.current.getPosition();
  const linePos = position.lineNumber;
  const lineContent = editorRef.current.getModel().getLineContent(linePos);
  const markdownPattern = /!\[(.*?)]\((.*?)\)/gm;
  const htmlPattern = /<img([^>]*)(width="([1-9][0-9]*)%")([^>]*) +\/>/gm;
  let matcher: RegExpExecArray;
  let appendPos = 0;
  let matched = false;
  while ((matcher = htmlPattern.exec(lineContent)) !== null) {
    matched = true;
    const startColumn = matcher.index + 1 + appendPos;
    const endColumn = startColumn + matcher[0].length;
    const newText = matcher[0].replace(matcher[2], `width="${value}%"`);
    setEditorValue(
      {
        startLineNumber: linePos,
        endLineNumber: linePos,
        startColumn: startColumn,
        endColumn: endColumn,
      },
      newText
    );
    appendPos += newText.length - matcher[0].length;
  }
  appendPos = 0;
  while ((matcher = markdownPattern.exec(lineContent)) !== null) {
    matched = true;
    const startColumn = matcher.index + 1 + appendPos;
    const endColumn = startColumn + matcher[0].length;
    const newText = `<img src="${matcher[2]}" alt="${matcher[1]}" width="${value}%" />`;
    setEditorValue(
      {
        startLineNumber: linePos,
        endLineNumber: linePos,
        startColumn: startColumn,
        endColumn: endColumn,
      },
      newText
    );
    appendPos += newText.length - matcher[0].length;
  }
  if (!matched) {
    setEditorValue(position, `<img src="" alt="" width="${value}%" />`);
    setEditorFocusPosition(position.endLineNumber, position.endColumn + 10);
  }
}

const ScaleList = (
  <Menu className={styles['dropdown']}>
    <Menu.ItemGroup title="图片缩放">
      {ImageScale.map((value) => (
        <Menu.Item key={`${value}`} onClick={() => handleImageScaling(value)}>
          {value}%
        </Menu.Item>
      ))}
    </Menu.ItemGroup>
  </Menu>
);

<Dropdown
  droplist={headingList}
  triggerProps={{ style: { zIndex: tooltipZIndex } }}
>
  <Option>
    <IconTitleLevel />
  </Option>
</Dropdown>

源码

github.com/lin-mt/quie…

下一篇

在 Spring Data JPA 中,优雅实现动态查询和连表查询