diff --git a/docs/en/guide/contextmenu-internal.md b/docs/en/guide/contextmenu-internal.md index aca6e3639..8b8facb2c 100644 --- a/docs/en/guide/contextmenu-internal.md +++ b/docs/en/guide/contextmenu-internal.md @@ -19,8 +19,10 @@ - Change the picture - Save as picture - Text wrapping -- Embedded type -- Upper and lower surrounding + - Embedded + - Upper and lower surrounding + - Float above text + - Float below text ## Table diff --git a/docs/guide/contextmenu-internal.md b/docs/guide/contextmenu-internal.md index e502e973b..897d07c16 100644 --- a/docs/guide/contextmenu-internal.md +++ b/docs/guide/contextmenu-internal.md @@ -21,6 +21,8 @@ - 文字环绕 - 嵌入型 - 上下型环绕 + - 浮于文字上方 + - 衬于文字下方 ## 表格 diff --git a/src/editor/assets/css/index.css b/src/editor/assets/css/index.css index f7c7ad6eb..32e880303 100644 --- a/src/editor/assets/css/index.css +++ b/src/editor/assets/css/index.css @@ -68,4 +68,10 @@ to { opacity: 1 } +} + +.ce-float-image { + position: absolute; + opacity: 0.5; + pointer-events: none; } \ No newline at end of file diff --git a/src/editor/assets/css/resizer/resizer.css b/src/editor/assets/css/resizer/resizer.css index eb7bb5669..924d99ced 100644 --- a/src/editor/assets/css/resizer/resizer.css +++ b/src/editor/assets/css/resizer/resizer.css @@ -54,6 +54,7 @@ height: 20px; white-space: nowrap; position: absolute; + z-index: 9; top: -30px; left: 0; opacity: .9; diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 6eeb47ffe..6d1cadaae 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -2,7 +2,8 @@ import { NBSP, WRAP, ZERO } from '../../dataset/constant/Common' import { EDITOR_ELEMENT_STYLE_ATTR } from '../../dataset/constant/Element' import { titleSizeMapping } from '../../dataset/constant/Title' import { defaultWatermarkOption } from '../../dataset/constant/Watermark' -import { ControlComponent, ImageDisplay } from '../../dataset/enum/Control' +import { ImageDisplay } from '../../dataset/enum/Common' +import { ControlComponent } from '../../dataset/enum/Control' import { EditorContext, EditorMode, @@ -1944,6 +1945,22 @@ export class CommandAdapt { public changeImageDisplay(element: IElement, display: ImageDisplay) { if (element.imgDisplay === display) return element.imgDisplay = display + if ( + display === ImageDisplay.FLOAT_TOP || + display === ImageDisplay.FLOAT_BOTTOM + ) { + const positionList = this.position.getPositionList() + const { startIndex } = this.range.getRange() + const { + coordinate: { leftTop } + } = positionList[startIndex] + element.imgFloatPosition = { + x: leftTop[0], + y: leftTop[1] + } + } else { + delete element.imgFloatPosition + } this.draw.getPreviewer().clearResizer() this.draw.render({ isSetCursor: false diff --git a/src/editor/core/contextmenu/menus/imageMenus.ts b/src/editor/core/contextmenu/menus/imageMenus.ts index b2c657723..75df2b2a1 100644 --- a/src/editor/core/contextmenu/menus/imageMenus.ts +++ b/src/editor/core/contextmenu/menus/imageMenus.ts @@ -1,5 +1,5 @@ import { INTERNAL_CONTEXT_MENU_KEY } from '../../../dataset/constant/ContextMenu' -import { ImageDisplay } from '../../../dataset/enum/Control' +import { ImageDisplay } from '../../../dataset/enum/Common' import { ElementType } from '../../../dataset/enum/Element' import { IContextMenuContext, @@ -7,7 +7,15 @@ import { } from '../../../interface/contextmenu/ContextMenu' import { Command } from '../../command/Command' const { - IMAGE: { CHANGE, SAVE_AS, TEXT_WRAP, TEXT_WRAP_EMBED, TEXT_WRAP_UP_DOWN } + IMAGE: { + CHANGE, + SAVE_AS, + TEXT_WRAP, + TEXT_WRAP_EMBED, + TEXT_WRAP_UP_DOWN, + TEXT_WRAP_FLOAT_TOP, + TEXT_WRAP_FLOAT_BOTTOM + } } = INTERNAL_CONTEXT_MENU_KEY export const imageMenus: IRegisterContextMenu[] = [ @@ -86,6 +94,28 @@ export const imageMenus: IRegisterContextMenu[] = [ ImageDisplay.INLINE ) } + }, + { + key: TEXT_WRAP_FLOAT_TOP, + i18nPath: 'contextmenu.image.textWrapType.floatTop', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.FLOAT_TOP + ) + } + }, + { + key: TEXT_WRAP_FLOAT_BOTTOM, + i18nPath: 'contextmenu.image.textWrapType.floatBottom', + when: () => true, + callback: (command: Command, context: IContextMenuContext) => { + command.executeChangeImageDisplay( + context.startElement!, + ImageDisplay.FLOAT_BOTTOM + ) + } } ] } diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index 902d4bba2..7d5733893 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -3,6 +3,7 @@ import { ZERO } from '../../dataset/constant/Common' import { RowFlex } from '../../dataset/enum/Row' import { IAppendElementListOption, + IDrawFloatPayload, IDrawOption, IDrawPagePayload, IDrawRowPayload, @@ -66,8 +67,7 @@ import { CheckboxParticle } from './particle/CheckboxParticle' import { DeepRequired, IPadding } from '../../interface/Common' import { ControlComponent, - ControlIndentation, - ImageDisplay + ControlIndentation } from '../../dataset/enum/Control' import { formatElementList } from '../../utils/element' import { WorkerManager } from '../worker/WorkerManager' @@ -87,6 +87,7 @@ import { EventBus } from '../event/eventbus/EventBus' import { EventBusMap } from '../../interface/EventBus' import { Group } from './interactive/Group' import { Override } from '../override/Override' +import { ImageDisplay } from '../../dataset/enum/Common' export class Draw { private container: HTMLDivElement @@ -1106,28 +1107,39 @@ export class Draw { element.type === ElementType.IMAGE || element.type === ElementType.LATEX ) { - const elementWidth = element.width! * scale - const elementHeight = element.height! * scale - // 图片超出尺寸后自适应 - const curRowWidth = - element.imgDisplay === ImageDisplay.INLINE ? 0 : curRow.width - if (curRowWidth + elementWidth > availableWidth) { - // 计算剩余大小 - const surplusWidth = availableWidth - curRowWidth - const adaptiveWidth = - surplusWidth > 0 - ? surplusWidth - : Math.min(elementWidth, availableWidth) - const adaptiveHeight = (elementHeight * adaptiveWidth) / elementWidth - element.width = adaptiveWidth / scale - element.height = adaptiveHeight / scale - metrics.width = adaptiveWidth - metrics.height = adaptiveHeight - metrics.boundingBoxDescent = adaptiveHeight + // 浮动图片无需计算数据 + if ( + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + metrics.width = 0 + metrics.height = 0 + metrics.boundingBoxDescent = 0 } else { - metrics.width = elementWidth - metrics.height = elementHeight - metrics.boundingBoxDescent = elementHeight + const elementWidth = element.width! * scale + const elementHeight = element.height! * scale + // 图片超出尺寸后自适应 + const curRowWidth = + element.imgDisplay === ImageDisplay.INLINE ? 0 : curRow.width + if (curRowWidth + elementWidth > availableWidth) { + // 计算剩余大小 + const surplusWidth = availableWidth - curRowWidth + const adaptiveWidth = + surplusWidth > 0 + ? surplusWidth + : Math.min(elementWidth, availableWidth) + const adaptiveHeight = + (elementHeight * adaptiveWidth) / elementWidth + element.width = adaptiveWidth / scale + element.height = adaptiveHeight / scale + metrics.width = adaptiveWidth + metrics.height = adaptiveHeight + metrics.boundingBoxDescent = adaptiveHeight + } else { + metrics.width = elementWidth + metrics.height = elementHeight + metrics.boundingBoxDescent = elementHeight + } } metrics.boundingBoxAscent = 0 } else if (element.type === ElementType.TABLE) { @@ -1607,7 +1619,13 @@ export class Draw { // 元素绘制 if (element.type === ElementType.IMAGE) { this._drawRichText(ctx) - this.imageParticle.render(ctx, element, x, y + offsetY) + // 浮动图片单独绘制 + if ( + element.imgDisplay !== ImageDisplay.FLOAT_TOP && + element.imgDisplay !== ImageDisplay.FLOAT_BOTTOM + ) { + this.imageParticle.render(ctx, element, x, y + offsetY) + } } else if (element.type === ElementType.LATEX) { this._drawRichText(ctx) this.laTexParticle.render(ctx, element, x, y + offsetY) @@ -1816,6 +1834,31 @@ export class Draw { } } + private _drawFloat( + ctx: CanvasRenderingContext2D, + payload: IDrawFloatPayload + ) { + const floatPositionList = this.position.getFloatPositionList() + const { imgDisplay, pageNo } = payload + for (let e = 0; e < floatPositionList.length; e++) { + const floatPosition = floatPositionList[e] + const element = floatPosition.element + if ( + pageNo === floatPosition.pageNo && + element.imgDisplay === imgDisplay && + element.type === ElementType.IMAGE + ) { + const imgFloatPosition = element.imgFloatPosition! + this.imageParticle.render( + ctx, + element, + imgFloatPosition.x, + imgFloatPosition.y + ) + } + } + } + private _clearPage(pageNo: number) { const ctx = this.ctxList[pageNo] const pageDom = this.pageList[pageNo] @@ -1837,6 +1880,11 @@ export class Draw { if (this.mode !== EditorMode.PRINT) { this.margin.render(ctx, pageNo) } + // 渲染衬于文字下方元素 + this._drawFloat(ctx, { + pageNo, + imgDisplay: ImageDisplay.FLOAT_BOTTOM + }) // 控件高亮 this.control.renderHighlightList(ctx, pageNo) // 渲染元素 @@ -1864,6 +1912,11 @@ export class Draw { this.footer.render(ctx, pageNo) } } + // 渲染浮于文字上方元素 + this._drawFloat(ctx, { + pageNo, + imgDisplay: ImageDisplay.FLOAT_TOP + }) // 搜索匹配绘制 if (this.search.getSearchKeyword()) { this.search.render(ctx, pageNo) diff --git a/src/editor/core/draw/particle/ImageParticle.ts b/src/editor/core/draw/particle/ImageParticle.ts index 42c371929..436a363c7 100644 --- a/src/editor/core/draw/particle/ImageParticle.ts +++ b/src/editor/core/draw/particle/ImageParticle.ts @@ -1,3 +1,5 @@ +import { EDITOR_PREFIX } from '../../../dataset/constant/Editor' +import { ImageDisplay } from '../../../dataset/enum/Common' import { IEditorOption } from '../../../interface/Editor' import { IElement } from '../../../interface/Element' import { convertStringToBase64 } from '../../../utils' @@ -7,11 +9,62 @@ export class ImageParticle { private draw: Draw protected options: Required protected imageCache: Map + private container: HTMLDivElement + private floatImageContainer: HTMLDivElement | null + private floatImage: HTMLImageElement | null constructor(draw: Draw) { this.draw = draw this.options = draw.getOptions() + this.container = draw.getContainer() this.imageCache = new Map() + this.floatImageContainer = null + this.floatImage = null + } + + public createFloatImage(element: IElement) { + const { scale } = this.options + // 复用浮动元素 + let floatImageContainer = this.floatImageContainer + let floatImage = this.floatImage + if (!floatImageContainer) { + floatImageContainer = document.createElement('div') + floatImageContainer.classList.add(`${EDITOR_PREFIX}-float-image`) + this.container.append(floatImageContainer) + this.floatImageContainer = floatImageContainer + } + if (!floatImage) { + floatImage = document.createElement('img') + floatImageContainer.append(floatImage) + this.floatImage = floatImage + } + floatImageContainer.style.display = 'none' + floatImage.style.width = `${element.width! * scale}px` + floatImage.style.height = `${element.height! * scale}px` + // 浮动图片初始信息 + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = this.draw.getPageNo() * (height + pageGap) + const imgFloatPosition = element.imgFloatPosition! + floatImageContainer.style.left = `${imgFloatPosition.x}px` + floatImageContainer.style.top = `${preY + imgFloatPosition.y}px` + floatImage.src = element.value + } + + public dragFloatImage(movementX: number, movementY: number) { + if (!this.floatImageContainer) return + this.floatImageContainer.style.display = 'block' + // 之前的坐标加移动长度 + const x = parseFloat(this.floatImageContainer.style.left) + movementX + const y = parseFloat(this.floatImageContainer.style.top) + movementY + this.floatImageContainer.style.left = `${x}px` + this.floatImageContainer.style.top = `${y}px` + } + + public destroyFloatImage() { + if (this.floatImageContainer) { + this.floatImageContainer.style.display = 'none' + } } protected addImageObserver(promise: Promise) { @@ -58,9 +111,18 @@ export class ImageParticle { img.setAttribute('crossOrigin', 'Anonymous') img.src = element.value img.onload = () => { - ctx.drawImage(img, x, y, width, height) this.imageCache.set(element.id!, img) resolve(element) + // 衬于文字下方图片需要重新首先绘制 + if (element.imgDisplay === ImageDisplay.FLOAT_BOTTOM) { + this.draw.render({ + isCompute: false, + isSetCursor: false, + isSubmitHistory: false + }) + } else { + ctx.drawImage(img, x, y, width, height) + } } img.onerror = error => { const fallbackImage = this.getFallbackImage(width, height) diff --git a/src/editor/core/draw/particle/previewer/Previewer.ts b/src/editor/core/draw/particle/previewer/Previewer.ts index 10ead627e..15b9fb9f0 100644 --- a/src/editor/core/draw/particle/previewer/Previewer.ts +++ b/src/editor/core/draw/particle/previewer/Previewer.ts @@ -63,6 +63,32 @@ export class Previewer { this.previewerImage = null } + private _getElementPosition( + element: IElement, + position: IElementPosition | null = null + ): { x: number; y: number } { + let x = 0 + let y = 0 + const height = this.draw.getHeight() + const pageGap = this.draw.getPageGap() + const preY = this.draw.getPageNo() * (height + pageGap) + // 优先使用浮动位置 + if (element.imgFloatPosition) { + x = element.imgFloatPosition.x! + y = element.imgFloatPosition.y + preY + } else if (position) { + const { + coordinate: { + leftTop: [left, top] + }, + ascent + } = position + x = left + y = top + preY + ascent + } + return { x, y } + } + private _createResizerDom(): IPreviewerCreateResult { // 拖拽边框 const resizerSelection = document.createElement('div') @@ -114,10 +140,8 @@ export class Previewer { private _mousedown(evt: MouseEvent) { this.canvas = this.draw.getPage() - if (!this.curPosition || !this.curElement) return + if (!this.curElement) return const { scale } = this.options - const height = this.draw.getHeight() - const pageGap = this.draw.getPageGap() this.mousedownX = evt.x this.mousedownY = evt.y const target = evt.target as HTMLDivElement @@ -129,15 +153,13 @@ export class Previewer { // 拖拽图片镜像 this.resizerImage.src = this.curElementSrc this.resizerImageContainer.style.display = 'block' - const { - coordinate: { - leftTop: [left, top] - }, - ascent - } = this.curPosition - const prePageHeight = this.draw.getPageNo() * (height + pageGap) - this.resizerImageContainer.style.left = `${left}px` - this.resizerImageContainer.style.top = `${top + prePageHeight + ascent}px` + // 优先使用浮动位置信息 + const { x: resizerLeft, y: resizerTop } = this._getElementPosition( + this.curElement, + this.curPosition + ) + this.resizerImageContainer.style.left = `${resizerLeft}px` + this.resizerImageContainer.style.top = `${resizerTop}px` this.resizerImage.style.width = `${this.curElement.width! * scale}px` this.resizerImage.style.height = `${this.curElement.height! * scale}px` // 追加全局事件 @@ -147,7 +169,7 @@ export class Previewer { 'mouseup', () => { // 改变尺寸 - if (this.curElement && this.curPosition) { + if (this.curElement) { this.curElement.width = this.width this.curElement.height = this.height this.draw.render({ isSetCursor: false }) @@ -398,27 +420,22 @@ export class Previewer { public drawResizer( element: IElement, - position: IElementPosition, + position: IElementPosition | null = null, options: IPreviewerDrawOption = {} ) { this.previewerDrawOption = options const { scale } = this.options - const { - coordinate: { - leftTop: [left, top] - }, - ascent - } = position const elementWidth = element.width! * scale const elementHeight = element.height! * scale - const height = this.draw.getHeight() - const pageGap = this.draw.getPageGap() - const preY = this.draw.getPageNo() * (height + pageGap) // 尺寸预览 this._updateResizerSizeView(elementWidth, elementHeight) - // 边框 - this.resizerSelection.style.left = `${left}px` - this.resizerSelection.style.top = `${top + preY + ascent}px` + // 优先使用浮动位置信息 + const { x: resizerLeft, y: resizerTop } = this._getElementPosition( + element, + position + ) + this.resizerSelection.style.left = `${resizerLeft}px` + this.resizerSelection.style.top = `${resizerTop}px` // 更新预览包围框尺寸 this._updateResizerRect(elementWidth, elementHeight) this.resizerSelection.style.display = 'block' diff --git a/src/editor/core/event/handlers/mousedown.ts b/src/editor/core/event/handlers/mousedown.ts index fcbc11e6c..ec8d32f10 100644 --- a/src/editor/core/event/handlers/mousedown.ts +++ b/src/editor/core/event/handlers/mousedown.ts @@ -1,3 +1,4 @@ +import { ImageDisplay } from '../../../dataset/enum/Common' import { ElementType } from '../../../dataset/enum/Element' import { MouseEventButton } from '../../../dataset/enum/Event' import { deepClone } from '../../../utils' @@ -60,7 +61,9 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { // 记录选区开始位置 host.mouseDownStartPosition = { ...positionResult, - index: isTable ? tdValueIndex! : index + index: isTable ? tdValueIndex! : index, + x: evt.offsetX, + y: evt.offsetY } const elementList = draw.getElementList() const positionList = position.getPositionList() @@ -129,6 +132,13 @@ export function mousedown(evt: MouseEvent, host: CanvasEvent) { }) // 点击图片允许拖拽调整位置 setRangeCache(host) + // 浮动元素创建镜像图片 + if ( + curElement.imgDisplay === ImageDisplay.FLOAT_TOP || + curElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getImageParticle().createFloatImage(curElement) + } } // 表格工具组件 const tableTool = draw.getTableTool() diff --git a/src/editor/core/event/handlers/mousemove.ts b/src/editor/core/event/handlers/mousemove.ts index 5a59802f7..c114cfbdd 100644 --- a/src/editor/core/event/handlers/mousemove.ts +++ b/src/editor/core/event/handlers/mousemove.ts @@ -1,3 +1,5 @@ +import { ImageDisplay } from '../../../dataset/enum/Common' +import { ElementType } from '../../../dataset/enum/Element' import { CanvasEvent } from '../CanvasEvent' export function mousemove(evt: MouseEvent, host: CanvasEvent) { @@ -22,6 +24,19 @@ export function mousemove(evt: MouseEvent, host: CanvasEvent) { return } } + const cacheStartIndex = host.cacheRange?.startIndex + if (cacheStartIndex) { + // 浮动元素拖拽调整位置 + const dragElement = host.cacheElementList![cacheStartIndex] + if ( + dragElement?.type === ElementType.IMAGE && + (dragElement.imgDisplay === ImageDisplay.FLOAT_TOP || + dragElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM) + ) { + draw.getPreviewer().clearResizer() + draw.getImageParticle().dragFloatImage(evt.movementX, evt.movementY) + } + } host.dragover(evt) host.isAllowDrop = true return diff --git a/src/editor/core/event/handlers/mouseup.ts b/src/editor/core/event/handlers/mouseup.ts index 61ac50f03..16b3eaa3b 100644 --- a/src/editor/core/event/handlers/mouseup.ts +++ b/src/editor/core/event/handlers/mouseup.ts @@ -1,4 +1,5 @@ import { EDITOR_ELEMENT_STYLE_ATTR } from '../../../dataset/constant/Element' +import { ImageDisplay } from '../../../dataset/enum/Common' import { ControlComponent, ControlType } from '../../../dataset/enum/Control' import { ElementType } from '../../../dataset/enum/Element' import { IElement } from '../../../interface/Element' @@ -18,6 +19,28 @@ function getElementIndexByDragId(dragId: string, elementList: IElement[]) { return (elementList).findIndex(el => el.dragId === dragId) } +// 移动悬浮图片位置 +function moveImgPosition( + element: IElement, + evt: MouseEvent, + host: CanvasEvent +) { + const draw = host.getDraw() + if ( + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + const moveX = evt.offsetX - host.mouseDownStartPosition!.x! + const moveY = evt.offsetY - host.mouseDownStartPosition!.y! + const imgFloatPosition = element.imgFloatPosition! + element.imgFloatPosition = { + x: imgFloatPosition.x + moveX, + y: imgFloatPosition.y + moveY + } + } + draw.getImageParticle().destroyFloatImage() +} + export function mouseup(evt: MouseEvent, host: CanvasEvent) { // 判断是否允许拖放 if (host.isAllowDrop) { @@ -42,13 +65,34 @@ export function mouseup(evt: MouseEvent, host: CanvasEvent) { range.startIndex >= cacheStartIndex && range.endIndex <= cacheEndIndex ) { + // 清除渲染副作用 + draw.clearSideEffect() + // 浮动元素拖拽需要提交历史 + let isSubmitHistory = false + if (isCacheRangeCollapsed) { + // 图片移动 + const dragElement = cacheElementList[cacheEndIndex] + if (dragElement.type === ElementType.IMAGE) { + moveImgPosition(dragElement, evt, host) + if ( + dragElement.imgDisplay === ImageDisplay.FLOAT_TOP || + dragElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getPreviewer().drawResizer(dragElement) + isSubmitHistory = true + } else { + const cachePosition = cachePositionList[cacheEndIndex] + draw.getPreviewer().drawResizer(dragElement, cachePosition) + } + } + } rangeManager.replaceRange({ ...cacheRange }) draw.render({ isSetCursor: false, isCompute: false, - isSubmitHistory: false + isSubmitHistory }) return } @@ -207,11 +251,35 @@ export function mouseup(evt: MouseEvent, host: CanvasEvent) { range.startTrIndex, range.endTrIndex ) + // 清除渲染副作用 + draw.clearSideEffect() + // 移动图片 + let imgElement: IElement | null = null + if (isCacheRangeCollapsed) { + const elementList = draw.getElementList() + const dragElement = elementList[rangeEndIndex] + if (dragElement.type === ElementType.IMAGE) { + moveImgPosition(dragElement, evt, host) + imgElement = dragElement + } + } // 重新渲染 draw.render({ isSetCursor: false }) - draw.clearSideEffect() + // 拖拽后渲染图片工具 + if (imgElement) { + if ( + imgElement.imgDisplay === ImageDisplay.FLOAT_TOP || + imgElement.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + draw.getPreviewer().drawResizer(imgElement) + } else { + const dragPositionList = position.getPositionList() + const dragPosition = dragPositionList[rangeEndIndex] + draw.getPreviewer().drawResizer(imgElement, dragPosition) + } + } } else if (host.isAllowDrag) { // 如果是允许拖拽不允许拖放则光标重置 host.mousedown(evt) diff --git a/src/editor/core/i18n/lang/en.json b/src/editor/core/i18n/lang/en.json index 05abc7263..81bc495be 100644 --- a/src/editor/core/i18n/lang/en.json +++ b/src/editor/core/i18n/lang/en.json @@ -21,7 +21,9 @@ "textWrap": "Text wrap", "textWrapType": { "embed": "Embed", - "upDown": "Up down" + "upDown": "Up down", + "floatTop": "Float above text", + "floatBottom": "Float below text" } }, "table": { diff --git a/src/editor/core/i18n/lang/zh-CN.json b/src/editor/core/i18n/lang/zh-CN.json index 4d116111e..972117789 100644 --- a/src/editor/core/i18n/lang/zh-CN.json +++ b/src/editor/core/i18n/lang/zh-CN.json @@ -21,7 +21,9 @@ "textWrap": "文字环绕", "textWrapType": { "embed": "嵌入型", - "upDown": "上下型环绕" + "upDown": "上下型环绕", + "floatTop": "浮于文字上方", + "floatBottom": "衬于文字下方" } }, "table": { diff --git a/src/editor/core/position/Position.ts b/src/editor/core/position/Position.ts index 9d7243684..ce7114373 100644 --- a/src/editor/core/position/Position.ts +++ b/src/editor/core/position/Position.ts @@ -1,10 +1,12 @@ import { ElementType, RowFlex, VerticalAlign } from '../..' import { ZERO } from '../../dataset/constant/Common' -import { ControlComponent, ImageDisplay } from '../../dataset/enum/Control' +import { ControlComponent } from '../../dataset/enum/Control' import { IComputePageRowPositionPayload, IComputePageRowPositionResult, - IComputeRowPositionPayload + IComputeRowPositionPayload, + IFloatPosition, + IGetFloatPositionByXYPayload } from '../../interface/Position' import { IEditorOption } from '../../interface/Editor' import { IElement, IElementPosition } from '../../interface/Element' @@ -16,17 +18,20 @@ import { import { Draw } from '../draw/Draw' import { EditorMode, EditorZone } from '../../dataset/enum/Editor' import { deepClone } from '../../utils' +import { ImageDisplay } from '../../dataset/enum/Common' export class Position { private cursorPosition: IElementPosition | null private positionContext: IPositionContext private positionList: IElementPosition[] + private floatPositionList: IFloatPosition[] private draw: Draw private options: Required constructor(draw: Draw) { this.positionList = [] + this.floatPositionList = [] this.cursorPosition = null this.positionContext = { isTable: false, @@ -37,6 +42,10 @@ export class Position { this.options = draw.getOptions() } + public getFloatPositionList(): IFloatPosition[] { + return this.floatPositionList + } + public getTablePositionList( sourceElementList: IElement[] ): IElementPosition[] { @@ -149,6 +158,28 @@ export class Position { rightBottom: [x + metrics.width, y + curRow.height] } } + // 缓存浮动元素信息 + if ( + element.imgDisplay === ImageDisplay.FLOAT_TOP || + element.imgDisplay === ImageDisplay.FLOAT_BOTTOM + ) { + // 浮动元素使用上一位置信息 + const prePosition = positionList[positionList.length - 1] + if (prePosition) { + positionItem.metrics = prePosition.metrics + positionItem.coordinate = prePosition.coordinate + } + this.floatPositionList.push({ + pageNo, + element, + position: positionItem, + isTable: payload.isTable, + index: payload.index, + tdIndex: payload.tdIndex, + trIndex: payload.trIndex, + tdValueIndex: index + }) + } positionList.push(positionItem) index++ x += metrics.width @@ -170,7 +201,11 @@ export class Position { startIndex: 0, startX: (td.x! + tdPadding[3]) * scale + tablePreX, startY: (td.y! + tdPadding[0]) * scale + tablePreY, - innerWidth: (td.width! - tdPaddingWidth) * scale + innerWidth: (td.width! - tdPaddingWidth) * scale, + isTable: true, + index: index - 1, + tdIndex: d, + trIndex: t }) // 垂直对齐方式 if ( @@ -217,6 +252,7 @@ export class Position { public computePositionList() { // 置空原位置信息 this.positionList = [] + this.floatPositionList = [] // 按每页行计算 const innerWidth = this.draw.getInnerWidth() const pageRowList = this.draw.getPageRowList() @@ -291,6 +327,15 @@ export class Position { const curPageNo = payload.pageNo ?? this.draw.getPageNo() const isMainActive = zoneManager.isMainActive() const positionNo = isMainActive ? curPageNo : 0 + // 验证浮于文字上方元素 + if (!isTable) { + const floatTopPosition = this.getFloatPositionByXY({ + ...payload, + imgDisplay: ImageDisplay.FLOAT_TOP + }) + if (floatTopPosition) return floatTopPosition + } + // 普通元素 for (let j = 0; j < positionList.length; j++) { const { index, @@ -388,6 +433,14 @@ export class Position { } } } + // 验证衬于文字下方元素 + if (!isTable) { + const floatBottomPosition = this.getFloatPositionByXY({ + ...payload, + imgDisplay: ImageDisplay.FLOAT_BOTTOM + }) + if (floatBottomPosition) return floatBottomPosition + } // 非命中区域 let isLastArea = false let curPositionIndex = -1 @@ -493,6 +546,55 @@ export class Position { } } + public getFloatPositionByXY( + payload: IGetFloatPositionByXYPayload + ): ICurrentPosition | void { + const { x, y } = payload + for (let f = 0; f < this.floatPositionList.length; f++) { + const { + position, + element, + isTable, + index, + trIndex, + tdIndex, + tdValueIndex + } = this.floatPositionList[f] + if ( + element.type === ElementType.IMAGE && + element.imgDisplay === payload.imgDisplay + ) { + const imgFloatPosition = element.imgFloatPosition! + if ( + x >= imgFloatPosition.x && + x <= imgFloatPosition.x + element.width! && + y >= imgFloatPosition.y && + y <= imgFloatPosition.y + element.height! + ) { + if (isTable) { + return { + index: index!, + isDirectHit: true, + isImage: true, + isTable, + trIndex, + tdIndex, + tdValueIndex, + tdId: element.tdId, + trId: element.trId, + tableId: element.tableId + } + } + return { + index: position.index, + isDirectHit: true, + isImage: true + } + } + } + } + } + public adjustPositionContext( payload: IGetPositionByXYPayload ): ICurrentPosition | null { diff --git a/src/editor/dataset/constant/ContextMenu.ts b/src/editor/dataset/constant/ContextMenu.ts index 282f96992..a50ae92f4 100644 --- a/src/editor/dataset/constant/ContextMenu.ts +++ b/src/editor/dataset/constant/ContextMenu.ts @@ -23,7 +23,9 @@ export const INTERNAL_CONTEXT_MENU_KEY = { SAVE_AS: 'imageSaveAs', TEXT_WRAP: 'imageTextWrap', TEXT_WRAP_EMBED: 'imageTextWrapEmbed', - TEXT_WRAP_UP_DOWN: 'imageTextWrapUpDown' + TEXT_WRAP_UP_DOWN: 'imageTextWrapUpDown', + TEXT_WRAP_FLOAT_TOP: 'imageTextWrapFloatTop', + TEXT_WRAP_FLOAT_BOTTOM: 'imageTextWrapFloatBottom' }, TABLE: { BORDER: 'border', diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 5ac1c5a87..3c340fd25 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -64,7 +64,9 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'listStyle', 'listWrap', 'groupIds', - 'conceptId' + 'conceptId', + 'imgDisplay', + 'imgFloatPosition' ] export const TABLE_TD_ZIP_ATTR: Array = [ diff --git a/src/editor/dataset/enum/Common.ts b/src/editor/dataset/enum/Common.ts index e07d28697..0b310cfe7 100644 --- a/src/editor/dataset/enum/Common.ts +++ b/src/editor/dataset/enum/Common.ts @@ -8,3 +8,10 @@ export enum NumberType { ARABIC = 'arabic', CHINESE = 'chinese' } + +export enum ImageDisplay { + INLINE = 'inline', + BLOCK = 'block', + FLOAT_TOP = 'float-top', + FLOAT_BOTTOM = 'float-bottom' +} diff --git a/src/editor/dataset/enum/Control.ts b/src/editor/dataset/enum/Control.ts index 895814ef3..80308ee46 100644 --- a/src/editor/dataset/enum/Control.ts +++ b/src/editor/dataset/enum/Control.ts @@ -12,11 +12,6 @@ export enum ControlComponent { CHECKBOX = 'checkbox' } -export enum ImageDisplay { - INLINE = 'inline', - BLOCK = 'block' -} - // 控件内容缩进方式 export enum ControlIndentation { ROW_START = 'rowStart', // 从行起始位置缩进 diff --git a/src/editor/index.ts b/src/editor/index.ts index f6be62392..40f83c2d3 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -6,6 +6,7 @@ import { Command } from './core/command/Command' import { CommandAdapt } from './core/command/CommandAdapt' import { Listener } from './core/listener/Listener' import { RowFlex } from './dataset/enum/Row' +import { ImageDisplay } from './dataset/enum/Common' import { ElementType } from './dataset/enum/Element' import { formatElementList } from './utils/element' import { Register } from './core/register/Register' @@ -27,11 +28,7 @@ import { IHeader } from './interface/Header' import { IWatermark } from './interface/Watermark' import { defaultHeaderOption } from './dataset/constant/Header' import { defaultWatermarkOption } from './dataset/constant/Watermark' -import { - ControlIndentation, - ControlType, - ImageDisplay -} from './dataset/enum/Control' +import { ControlIndentation, ControlType } from './dataset/enum/Control' import { defaultControlOption } from './dataset/constant/Control' import { IControlOption } from './interface/Control' import { ICheckboxOption } from './interface/Checkbox' diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts index 657f8bb00..da1d92f42 100644 --- a/src/editor/interface/Draw.ts +++ b/src/editor/interface/Draw.ts @@ -1,3 +1,4 @@ +import { ImageDisplay } from '../dataset/enum/Common' import { EditorMode, EditorZone } from '../dataset/enum/Editor' import { IElement, IElementPosition } from './Element' import { IRow } from './Row' @@ -32,6 +33,11 @@ export interface IDrawRowPayload { zone?: EditorZone } +export interface IDrawFloatPayload { + pageNo: number + imgDisplay: ImageDisplay +} + export interface IDrawPagePayload { elementList: IElement[] positionList: IElementPosition[] diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index 9ad9963f3..1992d08b6 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -1,4 +1,5 @@ -import { ControlComponent, ImageDisplay } from '../dataset/enum/Control' +import { ImageDisplay } from '../dataset/enum/Common' +import { ControlComponent } from '../dataset/enum/Control' import { ElementType } from '../dataset/enum/Element' import { ListStyle, ListType } from '../dataset/enum/List' import { RowFlex } from '../dataset/enum/Row' @@ -100,6 +101,10 @@ export interface IDateElement { export interface IImageElement { imgDisplay?: ImageDisplay + imgFloatPosition?: { + x: number + y: number + } } export interface IBlockElement { diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index 7d9f7614a..8af10f624 100644 --- a/src/editor/interface/Position.ts +++ b/src/editor/interface/Position.ts @@ -1,4 +1,4 @@ -import { IElement } from '..' +import { IElement, ImageDisplay } from '..' import { EditorZone } from '../dataset/enum/Editor' import { IElementPosition } from './Element' import { IRow } from './Row' @@ -6,6 +6,8 @@ import { ITd } from './table/Td' export interface ICurrentPosition { index: number + x?: number + y?: number isCheckbox?: boolean isControl?: boolean isImage?: boolean @@ -32,6 +34,10 @@ export interface IGetPositionByXYPayload { positionList?: IElementPosition[] } +export type IGetFloatPositionByXYPayload = IGetPositionByXYPayload & { + imgDisplay: ImageDisplay +} + export interface IPositionContext { isTable: boolean isCheckbox?: boolean @@ -58,6 +64,11 @@ export interface IComputePageRowPositionPayload { startX: number startY: number innerWidth: number + isTable?: boolean + index?: number + tdIndex?: number + trIndex?: number + tdValueIndex?: number } export interface IComputePageRowPositionResult { @@ -65,3 +76,14 @@ export interface IComputePageRowPositionResult { y: number index: number } + +export interface IFloatPosition { + pageNo: number + element: IElement + position: IElementPosition + isTable?: boolean + index?: number + tdIndex?: number + trIndex?: number + tdValueIndex?: number +}