diff --git a/examples/js/init-content.js b/examples/js/init-content.js index c8003a142..23f4ffe47 100644 --- a/examples/js/init-content.js +++ b/examples/js/init-content.js @@ -71,7 +71,18 @@ window.initContent = [ }, { type: 'paragraph', - children: [{ text: '一行文字' }], + children: [ + { text: '一行文字' }, + { + type: 'image', + src: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png', + alt: '百度', + url: 'https://www.baidu.com/', + style: { width: '101px', height: '33px' }, + children: [{ text: '' }], // void node 要有一个空 text + }, + { text: '一行文字' }, + ], }, { type: 'blockquote', diff --git a/packages/basic-modules/src/assets/image.less b/packages/basic-modules/src/assets/image.less index 0529cc18d..0a69f6a85 100644 --- a/packages/basic-modules/src/assets/image.less +++ b/packages/basic-modules/src/assets/image.less @@ -1,32 +1,37 @@ // 拖拽,修改图片尺寸 -.w-e-selected-image-container { - position: relative; - display: inline-block; - - .w-e-image-dragger { - width: 7px; - height: 7px; - background-color: #4290f7; - position: absolute; - } - .left-top { - top: -4px; - left: 0; - cursor: nwse-resize; - } - .right-top { - top: -4px; - right: 0; - cursor: nesw-resize; +.w-e-text-container [data-slate-editor] { + .w-e-image-container { + display: inline-block; } - .left-bottom { - left: 0; - bottom: 0; - cursor: nesw-resize; - } - .right-bottom { - right: 0; - bottom: 0; - cursor: nwse-resize; + .w-e-selected-image-container { + position: relative; + overflow: hidden; + + .w-e-image-dragger { + width: 7px; + height: 7px; + background-color: #4290f7; + position: absolute; + } + .left-top { + top: 0; + left: 0; + cursor: nwse-resize; + } + .right-top { + top: 0; + right: 0; + cursor: nesw-resize; + } + .left-bottom { + left: 0; + bottom: 0; + cursor: nesw-resize; + } + .right-bottom { + right: 0; + bottom: 0; + cursor: nwse-resize; + } } } \ No newline at end of file diff --git a/packages/basic-modules/src/modules/image/index.ts b/packages/basic-modules/src/modules/image/index.ts index 2ebab50d8..444ba3806 100644 --- a/packages/basic-modules/src/modules/image/index.ts +++ b/packages/basic-modules/src/modules/image/index.ts @@ -13,6 +13,8 @@ import { editImageMenuConf, viewImageLinkMenuConf, imageWidth30MenuConf, + imageWidth50MenuConf, + imageWidth100MenuConf, } from './menu/index' const image: IModuleConf = { @@ -24,6 +26,8 @@ const image: IModuleConf = { editImageMenuConf, viewImageLinkMenuConf, imageWidth30MenuConf, + imageWidth50MenuConf, + imageWidth100MenuConf, ], editorPlugin: withImage, } diff --git a/packages/basic-modules/src/modules/image/menu/Width100.ts b/packages/basic-modules/src/modules/image/menu/Width100.ts new file mode 100644 index 000000000..8735fd2e5 --- /dev/null +++ b/packages/basic-modules/src/modules/image/menu/Width100.ts @@ -0,0 +1,13 @@ +/** + * @description image width 100% + * @author wangfupeng + */ + +import ImageWidthBaseClass from './WidthBase' + +class ImageWidth100 extends ImageWidthBaseClass { + title = '100%' // 菜单标题 + value = '100%' // css width 的值 +} + +export default ImageWidth100 diff --git a/packages/basic-modules/src/modules/image/menu/Width30.ts b/packages/basic-modules/src/modules/image/menu/Width30.ts index 4dc2ee928..16b01b40d 100644 --- a/packages/basic-modules/src/modules/image/menu/Width30.ts +++ b/packages/basic-modules/src/modules/image/menu/Width30.ts @@ -3,66 +3,11 @@ * @author wangfupeng */ -import { Transforms, Node } from 'slate' -import { IButtonMenu, IDomEditor } from '@wangeditor/core' -import { checkNodeType, getSelectedNodeByType } from '../../_helpers/node' +import ImageWidthBaseClass from './WidthBase' -// TODO 宽度 30% 50% 100% -// 抽离一个 baseClass - -class ImageWidth implements IButtonMenu { - title = '30%' - tag = 'button' - private value = '30%' - - getValue(editor: IDomEditor): string | boolean { - // 无需获取 val - return '' - } - - isActive(editor: IDomEditor): boolean { - // 无需 active - return false - } - - private getSelectedNode(editor: IDomEditor): Node | null { - return getSelectedNodeByType(editor, 'image') - } - - isDisabled(editor: IDomEditor): boolean { - if (editor.selection == null) return true - - const imageNode = this.getSelectedNode(editor) - if (imageNode == null) { - // 选区未处于 image node ,则禁用 - return true - } - return false - } - - exec(editor: IDomEditor, value: string | boolean) { - if (this.isDisabled(editor)) return - - const imageNode = this.getSelectedNode(editor) - if (imageNode == null) return - - // @ts-ignore - const { style = {} } = imageNode - const newStyle = { - ...style, - width: this.value, // 修改 width - height: '', // 清空 height - } - - Transforms.setNodes( - editor, - // @ts-ignore - { style: newStyle }, - { - match: n => checkNodeType(n, 'image'), - } - ) - } +class ImageWidth30 extends ImageWidthBaseClass { + title = '30%' // 菜单标题 + value = '30%' // css width 的值 } -export default ImageWidth +export default ImageWidth30 diff --git a/packages/basic-modules/src/modules/image/menu/Width50.ts b/packages/basic-modules/src/modules/image/menu/Width50.ts new file mode 100644 index 000000000..3d1dd2d15 --- /dev/null +++ b/packages/basic-modules/src/modules/image/menu/Width50.ts @@ -0,0 +1,13 @@ +/** + * @description image width 50% + * @author wangfupeng + */ + +import ImageWidthBaseClass from './WidthBase' + +class ImageWidth50 extends ImageWidthBaseClass { + title = '50%' // 菜单标题 + value = '50%' // css width 的值 +} + +export default ImageWidth50 diff --git a/packages/basic-modules/src/modules/image/menu/WidthBase.ts b/packages/basic-modules/src/modules/image/menu/WidthBase.ts new file mode 100644 index 000000000..136fdbee0 --- /dev/null +++ b/packages/basic-modules/src/modules/image/menu/WidthBase.ts @@ -0,0 +1,65 @@ +/** + * @description image width base class + * @author wangfupeng + */ + +import { Transforms, Node } from 'slate' +import { IButtonMenu, IDomEditor } from '@wangeditor/core' +import { checkNodeType, getSelectedNodeByType } from '../../_helpers/node' + +abstract class ImageWidthBaseClass implements IButtonMenu { + abstract title: string // 菜单标题 + tag = 'button' + abstract value: string // css width 的值 + + getValue(editor: IDomEditor): string | boolean { + // 无需获取 val + return '' + } + + isActive(editor: IDomEditor): boolean { + // 无需 active + return false + } + + private getSelectedNode(editor: IDomEditor): Node | null { + return getSelectedNodeByType(editor, 'image') + } + + isDisabled(editor: IDomEditor): boolean { + if (editor.selection == null) return true + + const imageNode = this.getSelectedNode(editor) + if (imageNode == null) { + // 选区未处于 image node ,则禁用 + return true + } + return false + } + + exec(editor: IDomEditor, value: string | boolean) { + if (this.isDisabled(editor)) return + + const imageNode = this.getSelectedNode(editor) + if (imageNode == null) return + + // @ts-ignore + const { style = {} } = imageNode + const newStyle = { + ...style, + width: this.value, // 修改 width + height: '', // 清空 height + } + + Transforms.setNodes( + editor, + // @ts-ignore + { style: newStyle }, + { + match: n => checkNodeType(n, 'image'), + } + ) + } +} + +export default ImageWidthBaseClass diff --git a/packages/basic-modules/src/modules/image/menu/config.ts b/packages/basic-modules/src/modules/image/menu/config.ts index 1904d2cff..e9f66afbf 100644 --- a/packages/basic-modules/src/modules/image/menu/config.ts +++ b/packages/basic-modules/src/modules/image/menu/config.ts @@ -38,6 +38,6 @@ export function genImageMenuConfig() { /*自定义*/ }, - // TODO onDeletedImage ??? 考虑所有删除的场景 —— 可以使用插件,劫持 e.apply 中的 `remove_node` + // TODO onDeletedImage - 参考 plugin.ts 中的 `newEditor.apply = ` } } diff --git a/packages/basic-modules/src/modules/image/menu/index.ts b/packages/basic-modules/src/modules/image/menu/index.ts index 7d25e5846..a78ad099e 100644 --- a/packages/basic-modules/src/modules/image/menu/index.ts +++ b/packages/basic-modules/src/modules/image/menu/index.ts @@ -8,6 +8,8 @@ import DeleteImage from './DeleteImage' import EditImage from './EditImage' import ViewImageLink from './ViewImageLink' import ImageWidth30 from './Width30' +import ImageWidth50 from './Width50' +import ImageWidth100 from './Width100' import { genImageMenuConfig } from './config' const config = genImageMenuConfig() // menu config @@ -51,3 +53,17 @@ export const imageWidth30MenuConf = { return new ImageWidth30() }, } + +export const imageWidth50MenuConf = { + key: 'imageWidth50', + factory() { + return new ImageWidth50() + }, +} + +export const imageWidth100MenuConf = { + key: 'imageWidth100', + factory() { + return new ImageWidth100() + }, +} diff --git a/packages/basic-modules/src/modules/image/plugin.ts b/packages/basic-modules/src/modules/image/plugin.ts index 34fe56ebd..04c993bb5 100644 --- a/packages/basic-modules/src/modules/image/plugin.ts +++ b/packages/basic-modules/src/modules/image/plugin.ts @@ -3,10 +3,11 @@ * @author wangfupeng */ +import { Editor, Path, Operation } from 'slate' import { IDomEditor } from '@wangeditor/core' function withImage(editor: T): T { - const { isInline, isVoid, insertData } = editor + const { isInline, isVoid /*, apply */ } = editor const newEditor = editor // 重写 isInline @@ -33,6 +34,23 @@ function withImage(editor: T): T { return isVoid(elem) } + // // 监听删除图片 + // // 【注意】暂时不开放这个功能,因为图片删除还可能被撤销回来,所以无法通过 remove_node 这一个动作来处理删除图片 + // // 要实现“获取用户删除的图片”还需要更多的支持,例如收集 remove_node 的图片,最后和所有图片进行对比 + // // 还要考虑编辑图片 + // newEditor.apply = (op: Operation) => { + // if (op.type === 'remove_node') { + // const { node } = op + // // @ts-ignore + // if (node.type === 'image') { + // console.log('removed image node', node) + // } + // } + + // // 执行原本的 apply - 重要!!! + // apply(op) + // } + // 返回 editor ,重要! return newEditor } diff --git a/packages/basic-modules/src/modules/image/render-elem.tsx b/packages/basic-modules/src/modules/image/render-elem.tsx index 95412a316..812b2b131 100644 --- a/packages/basic-modules/src/modules/image/render-elem.tsx +++ b/packages/basic-modules/src/modules/image/render-elem.tsx @@ -10,40 +10,80 @@ import { IDomEditor, DomEditor } from '@wangeditor/core' import { isNodeSelected } from '../_helpers/node' import $, { Dom7Array } from '../../utils/dom' +interface IImageSize { + width?: string + height?: string +} + +function genContainerId(editor: IDomEditor, elemNode: SlateElement) { + const { id } = DomEditor.findKey(editor, elemNode) // node 唯一 id + return `w-e-image-container-${id}` +} + /** - * 渲染拖拽容器,修改图片尺寸 - * @param imageVnode image vnode - * @param elemNode image node - * @param editor editor + * 未选中时,渲染 image container */ -function renderResizeContainer(imageVnode: VNode, elemNode: SlateElement, editor: IDomEditor) { - const { id } = DomEditor.findKey(editor, elemNode) // node 唯一 id +function renderContainer( + editor: IDomEditor, + elemNode: SlateElement, + imageVnode: VNode, + imageInfo: IImageSize +): VNode { + const { width, height } = imageInfo + + const style: any = {} + if (width) style.width = width + if (height) style.height = height + + const containerId = genContainerId(editor, elemNode) + + return ( +
+ {imageVnode} +
+ ) +} + +/** + * 选中状态下,渲染 image container(渲染拖拽容器,修改图片尺寸) + */ +function renderResizeContainer( + editor: IDomEditor, + elemNode: SlateElement, + imageVnode: VNode, + imageInfo: IImageSize +) { const $body = $('body') - const containerId = `w-e-image-container-${id}` + const containerId = genContainerId(editor, elemNode) + const { width, height } = imageInfo let originalX = 0 let originalWith = 0 let originalHeight = 0 let revers = false // 是否反转。如向右拖拽 right-top 需增加宽度(非反转),但向右拖拽 left-top 则需要减少宽度(反转) + let $container: Dom7Array | null = null - function getImgElem(): Dom7Array { - const $img = $(`#${containerId}`).find('img') - if ($img.length === 0) throw new Error('Cannot find image elem') - return $img + function getContainerElem(): Dom7Array { + const $container = $(`#${containerId}`) + if ($container.length === 0) throw new Error('Cannot find image container elem') + return $container } /** * 初始化。监听事件,记录原始数据 */ function init(clientX: number) { - // 记录 img 原始宽高 - const $img = getImgElem() - originalWith = $img.width() - originalHeight = $img.height() + $container = getContainerElem() // 记录当前 x 坐标值 originalX = clientX + // 记录 img 原始宽高 + const $img = $container.find('img') + if ($img.length === 0) throw new Error('Cannot find image elem') + originalWith = $img.width() + originalHeight = $img.height() + // 监听 mousemove $body.on('mousemove', onMousemove) @@ -61,18 +101,18 @@ function renderResizeContainer(imageVnode: VNode, elemNode: SlateElement, editor const newHeight = originalHeight * (newWidth / originalWith) // 根据 width ,按比例计算 height // 实时修改 img 宽高 -【注意】这里只修改 DOM ,mouseup 时再统一不修改 node - const $img = getImgElem() - $img.css('width', `${newWidth}px`) - $img.css('height', `${newHeight}px`) + if ($container == null) return + $container.css('width', `${newWidth}px`) + $container.css('height', `${newHeight}px`) }, 100) function onMouseup(e: Event) { // 取消监听 mousemove $body.off('mousemove', onMousemove) - const $img = getImgElem() - const width = $img.width().toFixed(2) - const height = $img.height().toFixed(2) + if ($container == null) return + const newWidth = $container.width().toFixed(2) + const newHeight = $container.height().toFixed(2) // 修改 node Transforms.setNodes( @@ -82,8 +122,8 @@ function renderResizeContainer(imageVnode: VNode, elemNode: SlateElement, editor style: { // @ts-ignore ...imageVnode.style, - width: `${width}px`, - height: `${height}px`, + width: `${newWidth}px`, + height: `${newHeight}px`, }, }, { at: DomEditor.findPath(editor, elemNode) } @@ -93,10 +133,16 @@ function renderResizeContainer(imageVnode: VNode, elemNode: SlateElement, editor $body.off('mouseup', onMouseup) } + const style: any = {} + if (width) style.width = width + if (height) style.height = height + // style.boxShadow = '0 0 0 1px #B4D5FF' // 自定义 selected 样式,因为有拖拽触手 + return (
{ @@ -131,21 +177,20 @@ function renderImage(elemNode: SlateElement, children: VNode[] | null, editor: I const { width = '', height = '' } = style const selected = isNodeSelected(editor, elemNode, 'image') // 图片是否选中 - const renderStyle: any = {} - if (width) renderStyle.width = width - if (height) renderStyle.height = height - if (selected) renderStyle.boxShadow = '0 0 0 1px #B4D5FF' // 自定义 selected 样式,因为有拖拽触手 + const imageStyle: any = {} + if (width) imageStyle.width = '100%' + if (height) imageStyle.height = '100%' // 【注意】void node 中,renderElem 不用处理 children 。core 会统一处理。 - const vnode = {alt} + const vnode = {alt} - // 未选中,则直接渲染图片 if (!selected) { - return vnode + // 未选中,渲染普通 image container + return renderContainer(editor, elemNode, vnode, { width, height }) } // 选中,渲染 resize container - return renderResizeContainer(vnode, elemNode, editor) + return renderResizeContainer(editor, elemNode, vnode, { width, height }) } const renderImageConf = { diff --git a/packages/core/src/assets/textarea.less b/packages/core/src/assets/textarea.less index 55a69d002..8ec32ace2 100644 --- a/packages/core/src/assets/textarea.less +++ b/packages/core/src/assets/textarea.less @@ -28,6 +28,9 @@ img { margin: 0 3px; max-width: 100%; + min-width: 20px; + min-height: 20px; + cursor: default; } // 选中的节点 diff --git a/packages/core/src/menus/bar/HoverBar.ts b/packages/core/src/menus/bar/HoverBar.ts index 1accf01f0..8911f484b 100644 --- a/packages/core/src/menus/bar/HoverBar.ts +++ b/packages/core/src/menus/bar/HoverBar.ts @@ -22,6 +22,7 @@ class HoverBar { private menus: { [key: string]: MenuType } = {} private hoverbarItems: IBarItem[] = [] private selectedNode: Node | null = null + private $body = $('body') constructor() { // 异步,否则获取不到 DOM 和 editor @@ -38,7 +39,17 @@ class HoverBar { editor.on('change', this.onEditorChange) // 滚动时隐藏 - editor.on('scroll', this.hideAndClean.bind(this)) + const hideAndClean = this.hideAndClean.bind(this) + editor.on('scroll', hideAndClean) + + // 拖拽时隐藏(如拖拽修改图片尺寸) + const { $body } = this + $body.on('mousedown', () => { + $body.on('mousemove', hideAndClean) + }) + $body.on('mouseup', () => { + $body.off('mousemove', hideAndClean) + }) }) } diff --git a/packages/editor/src/editor-config.ts b/packages/editor/src/editor-config.ts index 29a6af67e..96b805c1e 100644 --- a/packages/editor/src/editor-config.ts +++ b/packages/editor/src/editor-config.ts @@ -97,7 +97,14 @@ function getDefaultEditorConfig() { { // @ts-ignore match: (editor, n) => n.type === 'image', // 匹配 image node - menuKeys: ['deleteImage', 'editImage', 'viewImageLink'], + menuKeys: [ + 'imageWidth30', + 'imageWidth50', + 'imageWidth100', + 'editImage', + 'viewImageLink', + 'deleteImage', + ], }, // video hover bar { diff --git a/packages/upload-image-module/src/module/menu/config.ts b/packages/upload-image-module/src/module/menu/config.ts index 74d9be10f..9745705b6 100644 --- a/packages/upload-image-module/src/module/menu/config.ts +++ b/packages/upload-image-module/src/module/menu/config.ts @@ -35,6 +35,7 @@ export function genUploadImageConfig(): IUploadConfig { /* on failed */ }, onError: (file: any, err: any, res: any) => { + // TODO onTimeout 也在这里,文档中说明 /* on error */ },