mirror of https://github.com/fantasticit/think.git
feat: improve link and iframe menu
This commit is contained in:
parent
666ee01a01
commit
9bea9c35b6
|
@ -34,7 +34,7 @@ export const Iframe = Node.create({
|
||||||
parseHTML: getDatasetAttribute('width'),
|
parseHTML: getDatasetAttribute('width'),
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
default: 200,
|
default: 66,
|
||||||
parseHTML: getDatasetAttribute('height'),
|
parseHTML: getDatasetAttribute('height'),
|
||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
|
|
|
@ -53,9 +53,8 @@ export const Link = BuiltInLink.extend({
|
||||||
title: null,
|
title: null,
|
||||||
parseHTML: (element) => element.getAttribute('title'),
|
parseHTML: (element) => element.getAttribute('title'),
|
||||||
},
|
},
|
||||||
canonicalSrc: {
|
hasTrigger: {
|
||||||
default: null,
|
default: false,
|
||||||
parseHTML: (element) => element.dataset.canonicalSrc,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { BaseBubbleMenu } from './menus/base-bubble-menu';
|
||||||
import { ImageBubbleMenu } from './menus/image';
|
import { ImageBubbleMenu } from './menus/image';
|
||||||
import { BannerBubbleMenu } from './menus/banner';
|
import { BannerBubbleMenu } from './menus/banner';
|
||||||
import { LinkBubbleMenu } from './menus/link';
|
import { LinkBubbleMenu } from './menus/link';
|
||||||
|
import { IframeBubbleMenu } from './menus/iframe';
|
||||||
import { TableBubbleMenu } from './menus/table';
|
import { TableBubbleMenu } from './menus/table';
|
||||||
|
|
||||||
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
|
@ -76,6 +77,7 @@ export const MenuBar: React.FC<{ editor: any }> = ({ editor }) => {
|
||||||
<BaseBubbleMenu editor={editor} />
|
<BaseBubbleMenu editor={editor} />
|
||||||
<ImageBubbleMenu editor={editor} />
|
<ImageBubbleMenu editor={editor} />
|
||||||
<LinkBubbleMenu editor={editor} />
|
<LinkBubbleMenu editor={editor} />
|
||||||
|
<IframeBubbleMenu editor={editor} />
|
||||||
<BannerBubbleMenu editor={editor} />
|
<BannerBubbleMenu editor={editor} />
|
||||||
<TableBubbleMenu editor={editor} />
|
<TableBubbleMenu editor={editor} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { Space, Button, Modal, Form, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
import { IconEdit, IconExternalOpen, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { BubbleMenu } from '../views/bubble-menu';
|
||||||
|
import { Iframe } from '../extensions/iframe';
|
||||||
|
import { Divider } from '../divider';
|
||||||
|
import { Size } from './size';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const EXAMPLE_LINK =
|
||||||
|
'https://proxy.tencentsuite.com/openapi/proxy/v2/addon?uid=144115212008575217&creator=144115212008575217&redirect=https%3A%2F%2Fi.y.qq.com%2Fn2%2Fm%2Foutchain%2Fplayer%2Findex.html%3Fsongid%3D5408217&docType=1&docID=300000000$RwqOunTcpXjs&addonID=0b69e1b9517e44a4aee35d33ee021b55&packageID=817&nonce=m3rqxn';
|
||||||
|
|
||||||
|
export const IframeBubbleMenu = ({ editor }) => {
|
||||||
|
const attrs = editor.getAttributes(Iframe.name);
|
||||||
|
const { width, height, url } = attrs;
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
|
||||||
|
const useExample = useCallback(() => {
|
||||||
|
$form.current.setValue('url', EXAMPLE_LINK);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
toggleVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.updateAttributes(Iframe.name, {
|
||||||
|
url: values.url,
|
||||||
|
})
|
||||||
|
.setNodeSelection(editor.state.selection.from)
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
toggleVisible(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visitLink = useCallback(() => {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const openEditLinkModal = useCallback(() => {
|
||||||
|
toggleVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setSize = useCallback(
|
||||||
|
(size) => {
|
||||||
|
editor.chain().updateAttributes(Iframe.name, size).setNodeSelection(editor.state.selection.from).focus().run();
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteNode = useCallback(() => editor.chain().deleteSelection().run(), [editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BubbleMenu
|
||||||
|
className={'bubble-menu'}
|
||||||
|
editor={editor}
|
||||||
|
pluginKey="link-bubble-menu"
|
||||||
|
shouldShow={() => editor.isActive(Iframe.name)}
|
||||||
|
tippyOptions={{ maxWidth: 456 }}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
title="编辑链接"
|
||||||
|
visible={visible}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={() => toggleVisible(false)}
|
||||||
|
centered
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Text type="tertiary" style={{ cursor: 'pointer' }} onClick={useExample}>
|
||||||
|
查看示例
|
||||||
|
</Text>
|
||||||
|
<div>
|
||||||
|
<Button type="secondary" onClick={handleCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button theme="solid" onClick={handleOk}>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form initValues={{ url }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
|
<Form.Input autofocus label="链接" field="url" placeholder="请输入外链地址"></Form.Input>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Space>
|
||||||
|
<Tooltip content="访问链接">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content="编辑链接">
|
||||||
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Size width={width} height={height} onOk={setSize}>
|
||||||
|
<Tooltip content="设置宽高">
|
||||||
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Size>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Tooltip content="删除节点">
|
||||||
|
<Button onClick={deleteNode} icon={<IconDelete />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</BubbleMenu>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,11 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Space, Button, InputNumber, Typography } from '@douyinfe/semi-ui';
|
import { Space, Button } from '@douyinfe/semi-ui';
|
||||||
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconUpload, IconDelete } from '@douyinfe/semi-icons';
|
import { IconAlignLeft, IconAlignCenter, IconAlignRight, IconLineHeight, IconDelete } from '@douyinfe/semi-icons';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { Upload } from 'components/upload';
|
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
import { BubbleMenu } from '../views/bubble-menu';
|
||||||
import { Divider } from '../divider';
|
import { Divider } from '../divider';
|
||||||
import { Image } from '../extensions/image';
|
import { Image } from '../extensions/image';
|
||||||
import { getImageOriginSize } from '../services/image';
|
import { Size } from './size';
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
export const ImageBubbleMenu = ({ editor }) => {
|
export const ImageBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Image.name);
|
const attrs = editor.getAttributes(Image.name);
|
||||||
|
@ -92,43 +89,22 @@ export const ImageBubbleMenu = ({ editor }) => {
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Text>宽</Text>
|
<Size
|
||||||
<InputNumber
|
width={width}
|
||||||
size="small"
|
height={height}
|
||||||
hideButtons
|
onOk={(size) => {
|
||||||
value={width}
|
|
||||||
style={{ width: 60 }}
|
|
||||||
onEnterPress={(e) => {
|
|
||||||
const value = (e.target as HTMLInputElement).value;
|
|
||||||
editor
|
editor
|
||||||
.chain()
|
.chain()
|
||||||
.updateAttributes(Image.name, {
|
.updateAttributes(Image.name, size)
|
||||||
width: value,
|
|
||||||
})
|
|
||||||
.setNodeSelection(editor.state.selection.from)
|
.setNodeSelection(editor.state.selection.from)
|
||||||
.focus()
|
.focus()
|
||||||
.run();
|
.run();
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Tooltip content="设置宽高">
|
||||||
<Text>高</Text>
|
<Button icon={<IconLineHeight />} type="tertiary" theme="borderless" size="small" />
|
||||||
<InputNumber
|
</Tooltip>
|
||||||
size="small"
|
</Size>
|
||||||
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 />
|
<Divider />
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,89 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||||
import { Space, Button, Input } from '@douyinfe/semi-ui';
|
import { Space, Button, Modal, Form, Toast } from '@douyinfe/semi-ui';
|
||||||
import { IconExternalOpen, IconUnlink, IconTickCircle } from '@douyinfe/semi-icons';
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
import { IconExternalOpen, IconUnlink, IconEdit } from '@douyinfe/semi-icons';
|
||||||
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
|
import { Divider } from '../divider';
|
||||||
import { BubbleMenu } from '../views/bubble-menu';
|
import { BubbleMenu } from '../views/bubble-menu';
|
||||||
import { Link } from '../extensions/link';
|
import { Link } from '../extensions/link';
|
||||||
|
import { isMarkActive } from '../services/is-active';
|
||||||
|
import { findMarkPosition } from '../services/find-position';
|
||||||
|
import { isValidURL } from '../services/valid-url';
|
||||||
|
|
||||||
export const LinkBubbleMenu = ({ editor }) => {
|
export const LinkBubbleMenu = ({ editor }) => {
|
||||||
const attrs = editor.getAttributes(Link.name);
|
const attrs = editor.getAttributes(Link.name);
|
||||||
const { href, target } = attrs;
|
const { href, target } = attrs;
|
||||||
const [url, setUrl] = useState('');
|
const isLinkActive = editor.isActive(Link.name);
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
const [text, setText] = useState();
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
toggleVisible(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
if (!values.href) {
|
||||||
|
Toast.error('请输入有效链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.text) {
|
||||||
|
values.text = values.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.text !== text && values.text) {
|
||||||
|
editor.chain().extendMarkRange(Link.name).run();
|
||||||
|
const { view } = editor;
|
||||||
|
const schema = view.state.schema;
|
||||||
|
const node = schema.text(values.text, [schema.marks.link.create({ href: values.href })]);
|
||||||
|
view.dispatch(view.state.tr.replaceSelectionWith(node, true).scrollIntoView());
|
||||||
|
} else {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.extendMarkRange(Link.name)
|
||||||
|
.updateAttributes(Link.name, {
|
||||||
|
href: values.href,
|
||||||
|
})
|
||||||
|
.focus()
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
toggleVisible(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const visitLink = useCallback(() => {
|
||||||
|
window.open(href, target);
|
||||||
|
}, [href, target]);
|
||||||
|
|
||||||
|
const openEditLinkModal = useCallback(() => {
|
||||||
|
toggleVisible(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unsetLink = useCallback(() => editor.chain().unsetLink().run(), [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUrl(href);
|
if (!isLinkActive) return;
|
||||||
}, [href]);
|
|
||||||
|
const { state } = editor;
|
||||||
|
const isInLink = isMarkActive(state.schema.marks.link)(state);
|
||||||
|
|
||||||
|
if (!isInLink) return;
|
||||||
|
|
||||||
|
const { $head } = editor.state.selection;
|
||||||
|
const marks = $head.marks();
|
||||||
|
if (!marks.length) return;
|
||||||
|
|
||||||
|
const mark = marks[0];
|
||||||
|
const node = $head.node($head.depth);
|
||||||
|
const startPosOfThisLine = $head.pos - $head.parentOffset;
|
||||||
|
const endPosOfThisLine = startPosOfThisLine + node.content.size;
|
||||||
|
const { start, end } = findMarkPosition(state, mark, startPosOfThisLine, endPosOfThisLine);
|
||||||
|
const text = state.doc.textBetween(start, end);
|
||||||
|
setText(text);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BubbleMenu
|
<BubbleMenu
|
||||||
|
@ -22,60 +93,32 @@ export const LinkBubbleMenu = ({ editor }) => {
|
||||||
shouldShow={() => editor.isActive(Link.name)}
|
shouldShow={() => editor.isActive(Link.name)}
|
||||||
tippyOptions={{ maxWidth: 456 }}
|
tippyOptions={{ maxWidth: 456 }}
|
||||||
>
|
>
|
||||||
|
<Modal title="编辑链接" visible={visible} onOk={handleOk} onCancel={handleCancel} centered>
|
||||||
|
<Form initValues={{ text, href }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
|
<Form.Input label="文本" field="text" placeholder="请输入文本"></Form.Input>
|
||||||
|
<Form.Input
|
||||||
|
autofocus
|
||||||
|
label="链接"
|
||||||
|
field="href"
|
||||||
|
placeholder="请输入外链地址"
|
||||||
|
rules={[{ validator: (_, value) => isValidURL(value), message: '请输入有效链接' }]}
|
||||||
|
></Form.Input>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Space>
|
<Space>
|
||||||
<Input
|
|
||||||
value={url}
|
|
||||||
onChange={(v) => setUrl(v)}
|
|
||||||
placeholder={'输入链接'}
|
|
||||||
onEnterPress={(e) => {
|
|
||||||
const url = (e.target as HTMLInputElement).value;
|
|
||||||
setUrl(url);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Tooltip content="设置链接">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
icon={<IconTickCircle />}
|
|
||||||
onClick={() => {
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.extendMarkRange(Link.name)
|
|
||||||
.updateAttributes(Link.name, {
|
|
||||||
href: url,
|
|
||||||
})
|
|
||||||
.focus()
|
|
||||||
.run();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="去除链接">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().unsetLink().run();
|
|
||||||
}}
|
|
||||||
icon={<IconUnlink />}
|
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip content="访问链接">
|
<Tooltip content="访问链接">
|
||||||
<Button
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconExternalOpen />} onClick={visitLink} />
|
||||||
size="small"
|
</Tooltip>
|
||||||
type="tertiary"
|
|
||||||
theme="borderless"
|
<Tooltip content="编辑链接">
|
||||||
icon={<IconExternalOpen />}
|
<Button size="small" type="tertiary" theme="borderless" icon={<IconEdit />} onClick={openEditLinkModal} />
|
||||||
onClick={() => {
|
</Tooltip>
|
||||||
if (href) {
|
|
||||||
window.open(href, target);
|
<Divider />
|
||||||
}
|
|
||||||
}}
|
<Tooltip content="去除链接" hideOnClick>
|
||||||
/>
|
<Button onClick={unsetLink} icon={<IconUnlink />} type="tertiary" theme="borderless" size="small" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</BubbleMenu>
|
</BubbleMenu>
|
||||||
|
|
|
@ -21,8 +21,6 @@ export const Paragraph = ({ editor }) => {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// console.log(getCurrentCaretTitle(editor));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
disabled={isTitleActive(editor)}
|
disabled={isTitleActive(editor)}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { Button, Form, Dropdown } from '@douyinfe/semi-ui';
|
||||||
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
|
|
||||||
|
type ISize = { width: number; height: number };
|
||||||
|
|
||||||
|
export const Size: React.FC<{ width: number; height: number; onOk: (arg: ISize) => void }> = ({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
onOk,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const $form = useRef<FormApi>();
|
||||||
|
|
||||||
|
const handleOk = useCallback(() => {
|
||||||
|
$form.current.validate().then((values) => {
|
||||||
|
onOk(values as ISize);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
zIndex={10000}
|
||||||
|
trigger="click"
|
||||||
|
position={'bottomLeft'}
|
||||||
|
spacing={10}
|
||||||
|
render={
|
||||||
|
<div style={{ padding: '0 12px 12px' }}>
|
||||||
|
<Form initValues={{ width, height }} getFormApi={(formApi) => ($form.current = formApi)} labelPosition="left">
|
||||||
|
<Form.InputNumber autofocus label="宽" field="width" />
|
||||||
|
<Form.InputNumber label="高" field="height" />
|
||||||
|
</Form>
|
||||||
|
<Button size="small" type="primary" theme="solid" htmlType="submit" onClick={handleOk}>
|
||||||
|
设置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'inline-block' }}>{children}</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { EditorState } from 'prosemirror-state';
|
||||||
|
|
||||||
|
export function findMarkPosition(state: EditorState, mark, from, to) {
|
||||||
|
let markPos = { start: -1, end: -1 };
|
||||||
|
state.doc.nodesBetween(from, to, (node, pos) => {
|
||||||
|
if (markPos.start > -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (markPos.start === -1 && mark.isInSet(node.marks)) {
|
||||||
|
markPos = {
|
||||||
|
start: pos,
|
||||||
|
end: pos + Math.max(node.textContent.length, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return markPos;
|
||||||
|
}
|
|
@ -1,13 +0,0 @@
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function isValidURL(str) {
|
||||||
|
var pattern = new RegExp(
|
||||||
|
'^(https?:\\/\\/)?' + // protocol
|
||||||
|
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
|
||||||
|
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
|
||||||
|
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
|
||||||
|
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
|
||||||
|
'(\\#[-a-z\\d_]*)?$',
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
return !!pattern.test(str);
|
||||||
|
}
|
|
@ -22,6 +22,13 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emptyWrap {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
:global {
|
:global {
|
||||||
iframe {
|
iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { Input } from '@douyinfe/semi-ui';
|
import { Input, Typography, Space } from '@douyinfe/semi-ui';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const { url, width, height } = node.attrs;
|
const { url, width, height } = node.attrs;
|
||||||
|
@ -13,15 +15,19 @@ export const IframeWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
};
|
};
|
||||||
const content = (
|
const content = (
|
||||||
<NodeViewContent as="div" className={cls(styles.wrap, 'render-wrapper')}>
|
<NodeViewContent as="div" className={cls(styles.wrap, 'render-wrapper')}>
|
||||||
{isEditable && (
|
{/* {isEditable && (
|
||||||
<div className={styles.handlerWrap}>
|
<div className={styles.handlerWrap}>
|
||||||
<Input placeholder={'输入外链地址'} value={url} onChange={(url) => updateAttributes({ url })}></Input>
|
<Input placeholder={'输入外链地址'} value={url} onChange={(url) => updateAttributes({ url })}></Input>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
{url && (
|
{url ? (
|
||||||
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
<div className={styles.innerWrap} style={{ pointerEvents: !isEditable ? 'auto' : 'none' }}>
|
||||||
<iframe src={url}></iframe>
|
<iframe src={url}></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.emptyWrap}>
|
||||||
|
<Text>请设置外链地址</Text>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</NodeViewContent>
|
</NodeViewContent>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue