Quiet 项目简介:juejin.cn/post/717122…
上上篇:
作为一个后端 Java 开发,为何、如何自己实现一个 Markdown 编辑器
上一篇:
如何结合 Minio 实现一个简单的可嵌入的 Spring Boot Starter 文件服务
前言
在上两篇文章中,我们实现了一个 Markdown 编辑器和文件上传的服务,现在可以实现 Markdown 图片上传的功能了。
问题分析
图片上传主要有两种操作方式,一种是使用快捷键 Ctrl/Command + C 和 Ctrl/Command + V 直接复制粘贴进编辑器,一种是点击 Toolbar 的图片上传图标,然后用户选择图片,点击上传。
Ctrl/Command + V的实现可以使用document.onpaste来获取粘贴板的信息,如果是图片,则上传文件到后端,然后在编辑器里面填入图片信息。- 点击 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 = ``;
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(
``,
''
);
});
});
}
<Upload
accept={'image/*'}
customRequest={handleUploadImage}
renderUploadList={() => <></>}
renderUploadItem={() => <></>}
>
<Option>
<IconImage />
</Option>
</Upload>
好了,到此,两种图片上传的方式都实现了,在上传图片之后发现图片的大小无法控制,会在 Viewer 占用大量空间,这样阅读起来就很不雅观,所以还需要实现图片缩放的功能。
图片缩放
图片缩放的过程:
- 读取光标所在的行
- 使用正则匹配,获取 Markdown 的图片文本
- 替换成 html 的图片标签,并设置缩放比例
- 使用正则匹配,获取 html 的图片标签
- 替换 width 的缩放比例
- 如果没有匹配到,则添加一个空的图片缩放标签
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>