feat: width, height support for image

pull/8/head
fantasticit 2022-03-13 12:13:42 +08:00
parent c9c5badbb5
commit 94425befcf
8 changed files with 133 additions and 10 deletions

View File

@ -54,6 +54,13 @@ export const Resizeable: React.FC<IProps> = ({ width, height, onChange, children
}); });
}, []); }, []);
useEffect(() => {
Object.assign($container.current.style, {
width: `${width}px`,
height: `${height}px`,
});
}, [width, height]);
return ( return (
<div <div
id="js-resizeable-container" id="js-resizeable-container"

View File

@ -1,8 +1,6 @@
import { Plugin } from 'prosemirror-state';
import { Image as TImage } from '@tiptap/extension-image'; import { Image as TImage } from '@tiptap/extension-image';
import { NodeViewWrapper, NodeViewContent, ReactNodeViewRenderer } from '@tiptap/react'; import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import { Resizeable } from 'components/resizeable'; import { Resizeable } from 'components/resizeable';
import { uploadFile } from 'services/file';
const Render = ({ editor, node, updateAttributes }) => { const Render = ({ editor, node, updateAttributes }) => {
const isEditable = editor.isEditable; const isEditable = editor.isEditable;
@ -12,7 +10,7 @@ const Render = ({ editor, node, updateAttributes }) => {
updateAttributes({ height: size.height, width: size.width }); updateAttributes({ height: size.height, width: size.width });
}; };
const content = src && <img src={src} alt={title} style={{ width: '100%', height: '100%' }} />; const content = src && <img src={src} alt={alt} width={width} height={height} />;
return ( return (
<NodeViewWrapper as="div" style={{ textAlign, fontSize: 0, maxWidth: '100%' }}> <NodeViewWrapper as="div" style={{ textAlign, fontSize: 0, maxWidth: '100%' }}>

View File

@ -132,6 +132,11 @@ const defaultSerializerConfig = {
state.write(`documentChildren$`); state.write(`documentChildren$`);
state.closeBlock(node); state.closeBlock(node);
}, },
[Mind.name]: (state, node) => {
state.ensureNewLine();
state.write(`mind$`);
state.closeBlock(node);
},
// [DescriptionList.name]: renderHTMLNode("dl", true), // [DescriptionList.name]: renderHTMLNode("dl", true),
// [DescriptionItem.name]: (state, node, parent, index) => { // [DescriptionItem.name]: (state, node, parent, index) => {
// if (index === 1) state.ensureNewLine(); // if (index === 1) state.ensureNewLine();

View File

@ -1,4 +1,5 @@
import { Space, Button, Tooltip } from '@douyinfe/semi-ui'; import React, { useEffect, useState } from 'react';
import { Space, Button, Tooltip, InputNumber, Typography } from '@douyinfe/semi-ui';
import { import {
IconAlignLeft, IconAlignLeft,
IconAlignCenter, IconAlignCenter,
@ -10,8 +11,21 @@ import { Upload } from 'components/upload';
import { BubbleMenu } from '../components/bubble-menu'; import { BubbleMenu } from '../components/bubble-menu';
import { Divider } from '../components/divider'; import { Divider } from '../components/divider';
import { Image } from '../extensions/image'; import { Image } from '../extensions/image';
import { getImageOriginSize } from '../utils/image';
const { Text } = Typography;
export const ImageBubbleMenu = ({ editor }) => { export const ImageBubbleMenu = ({ editor }) => {
const attrs = editor.getAttributes(Image.name);
const { width: currentWidth, height: currentHeight } = attrs;
const [width, setWidth] = useState(currentWidth);
const [height, setHeight] = useState(currentHeight);
useEffect(() => {
setWidth(parseInt(currentWidth));
setHeight(parseInt(currentHeight));
}, [currentWidth, currentHeight]);
return ( return (
<BubbleMenu <BubbleMenu
className={'bubble-menu'} className={'bubble-menu'}
@ -79,14 +93,54 @@ export const ImageBubbleMenu = ({ editor }) => {
/> />
</Tooltip> </Tooltip>
<Divider /> <Divider />
<Text></Text>
<InputNumber
size="small"
hideButtons
value={width}
style={{ width: 60 }}
onEnterPress={(e) => {
const value = (e.target as HTMLInputElement).value;
editor
.chain()
.updateAttributes(Image.name, {
width: value,
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
/>
<Text></Text>
<InputNumber
size="small"
hideButtons
value={height}
style={{ width: 60 }}
onEnterPress={(e) => {
const value = (e.target as HTMLInputElement).value;
editor
.chain()
.updateAttributes(Image.name, {
height: value,
})
.setNodeSelection(editor.state.selection.from)
.focus()
.run();
}}
/>
<Divider />
<Upload <Upload
accept="image/*" accept="image/*"
onOK={(url) => { onOK={async (url, fileName) => {
const { width, height } = await getImageOriginSize(url);
editor editor
.chain() .chain()
.updateAttributes(Image.name, { .updateAttributes(Image.name, {
src: url, src: url,
alt: 'filename', alt: fileName,
width,
height,
}) })
.setNodeSelection(editor.state.selection.from) .setNodeSelection(editor.state.selection.from)
.focus() .focus()

View File

@ -16,6 +16,7 @@ import {
} from 'components/icons'; } from 'components/icons';
import { GridSelect } from 'components/grid-select'; import { GridSelect } from 'components/grid-select';
import { isTitleActive } from '../utils/active'; import { isTitleActive } from '../utils/active';
import { getImageOriginSize } from '../utils/image';
export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => { export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
if (!editor) { if (!editor) {
@ -65,7 +66,11 @@ export const MediaInsertMenu: React.FC<{ editor: any }> = ({ editor }) => {
<IconImage /> <IconImage />
<Upload <Upload
accept="image/*" accept="image/*"
onOK={(url) => editor.chain().focus().setImage({ src: url }).run()} onOK={async (url, fileName) => {
const { width, height } = await getImageOriginSize(url);
console.log('upload', width, height);
editor.chain().focus().setImage({ src: url, alt: fileName, width, height }).run();
}}
> >
{() => '图片'} {() => '图片'}
</Upload> </Upload>

View File

@ -0,0 +1,15 @@
export function getImageOriginSize(
src: string
): Promise<{ width: number | string; height: number | string }> {
return new Promise((resolve) => {
const image = document.createElement('img');
image.onload = function () {
console.log(image.width, image.height);
resolve({ width: image.width, height: image.height });
};
image.onerror = function () {
resolve({ width: 'auto', height: 'auto' });
};
image.src = src;
});
}

View File

@ -5,7 +5,7 @@ import { useAsyncLoading } from 'hooks/useAsyncLoading';
import { uploadFile } from 'services/file'; import { uploadFile } from 'services/file';
interface IProps { interface IProps {
onOK: (arg: string, fileName: string) => void; onOK: (arg: string, fileName: string, fileSize: number) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
accept?: string; accept?: string;
children?: (loading: boolean) => React.ReactNode; children?: (loading: boolean) => React.ReactNode;
@ -17,7 +17,7 @@ export const Upload: React.FC<IProps> = ({ onOK, accept, style = {}, children })
const beforeUpload = ({ file }) => { const beforeUpload = ({ file }) => {
uploadFileWithLoading(file.fileInstance).then((res: string) => { uploadFileWithLoading(file.fileInstance).then((res: string) => {
Toast.success('上传成功'); Toast.success('上传成功');
onOK && onOK(res, file.name); onOK && onOK(res, file.name, file.size);
}); });
return false; return false;
}; };

39
packages/config/.gitignore vendored 100644
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo
yaml/dev.yaml