diff --git a/docs/en/guide/command-execute.md b/docs/en/guide/command-execute.md index 9b4829d..cfaff8e 100644 --- a/docs/en/guide/command-execute.md +++ b/docs/en/guide/command-execute.md @@ -761,3 +761,33 @@ Usage: ```javascript instance.command.executeSetHTML(payload: Partial { el.underline = !!~noUnderlineIndex }) - this.draw.render({ isSetCursor: false }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) } public strikeout() { @@ -333,7 +336,10 @@ export class CommandAdapt { selection.forEach(el => { el.strikeout = !!~noStrikeoutIndex }) - this.draw.render({ isSetCursor: false }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) } public superscript() { @@ -1922,4 +1928,45 @@ export class CommandAdapt { footer: getElementList(footer) }) } + + public setGroup(): string | null { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return null + return this.draw.getGroup().setGroup() + } + + public deleteGroup(groupId: string) { + const isReadonly = this.draw.isReadonly() + if (isReadonly) return + this.draw.getGroup().deleteGroup(groupId) + } + + public getGroupIds(): Promise { + return this.draw.getWorkerManager().getGroupIds() + } + + public locationGroup(groupId: string) { + const elementList = this.draw.getOriginalMainElementList() + const context = this.draw + .getGroup() + .getContextByGroupId(elementList, groupId) + if (!context) return + const { isTable, index, trIndex, tdIndex, tdId, trId, tableId, endIndex } = + context + this.position.setPositionContext({ + isTable, + index, + trIndex, + tdIndex, + tdId, + trId, + tableId + }) + this.range.setRange(endIndex, endIndex) + this.draw.render({ + curIndex: endIndex, + isCompute: false, + isSubmitHistory: false + }) + } } diff --git a/src/editor/core/draw/Draw.ts b/src/editor/core/draw/Draw.ts index f43f137..8960086 100644 --- a/src/editor/core/draw/Draw.ts +++ b/src/editor/core/draw/Draw.ts @@ -82,6 +82,7 @@ import { Placeholder } from './frame/Placeholder' import { WORD_LIKE_REG } from '../../dataset/constant/Regular' import { EventBus } from '../event/eventbus/EventBus' import { EventBusMap } from '../../interface/EventBus' +import { Group } from './interactive/Group' export class Draw { private container: HTMLDivElement @@ -106,6 +107,7 @@ export class Draw { private margin: Margin private background: Background private search: Search + private group: Group private underline: Underline private strikeout: Strikeout private highlight: Highlight @@ -175,6 +177,7 @@ export class Draw { this.margin = new Margin(this) this.background = new Background(this) this.search = new Search(this) + this.group = new Group(this) this.underline = new Underline(this) this.strikeout = new Strikeout(this) this.highlight = new Highlight(this) @@ -451,6 +454,10 @@ export class Draw { return this.search } + public getGroup(): Group { + return this.group + } + public getHistoryManager(): HistoryManager { return this.historyManager } @@ -1443,8 +1450,13 @@ export class Draw { const { rowList, pageNo, elementList, positionList, startIndex, zone } = payload const isPrintMode = this.mode === EditorMode.PRINT - const { scale, tdPadding, defaultBasicRowMarginHeight, defaultRowMargin } = - this.options + const { + scale, + tdPadding, + defaultBasicRowMarginHeight, + defaultRowMargin, + group + } = this.options const { isCrossRowCol, tableId } = this.range.getRange() let index = startIndex for (let i = 0; i < rowList.length; i++) { @@ -1610,6 +1622,10 @@ export class Draw { } } } + // 组信息记录 + if (!group.disabled && element.groupIds) { + this.group.recordFillInfo(element, x, y, metrics.width, curRow.height) + } index++ // 绘制表格内元素 if (element.type === ElementType.TABLE) { @@ -1641,6 +1657,8 @@ export class Draw { } // 绘制富文本及文字 this._drawRichText(ctx) + // 绘制批注样式 + this.group.render(ctx) // 绘制选区 if (!isPrintMode) { if (rangeRecord.width && rangeRecord.height) { @@ -1767,7 +1785,8 @@ export class Draw { isSetCursor = true, isCompute = true, isLazy = true, - isInit = false + isInit = false, + isSourceHistory = false } = payload || {} let { curIndex } = payload || {} const innerWidth = this.getInnerWidth() @@ -1861,7 +1880,11 @@ export class Draw { self.footer.setElementList(deepClone(oldFooterElementList)) self.elementList = deepClone(oldElementList) self.range.setRange(startIndex, endIndex) - self.render({ curIndex, isSubmitHistory: false }) + self.render({ + curIndex, + isSubmitHistory: false, + isSourceHistory: true + }) }) } // 信息变动回调 @@ -1882,7 +1905,7 @@ export class Draw { this.eventBus.emit('pageSizeChange', this.pageRowList.length) } // 文档内容改变 - if (isSubmitHistory && !isInit) { + if ((isSubmitHistory || isSourceHistory) && !isInit) { if (this.listener.contentChange) { this.listener.contentChange() } diff --git a/src/editor/core/draw/interactive/Group.ts b/src/editor/core/draw/interactive/Group.ts new file mode 100644 index 0000000..1334770 --- /dev/null +++ b/src/editor/core/draw/interactive/Group.ts @@ -0,0 +1,198 @@ +import { EditorZone } from '../../../dataset/enum/Editor' +import { ElementType } from '../../../dataset/enum/Element' +import { DeepRequired } from '../../../interface/Common' +import { IEditorOption } from '../../../interface/Editor' +import { IElement, IElementFillRect } from '../../../interface/Element' +import { IPositionContext } from '../../../interface/Position' +import { IRange } from '../../../interface/Range' +import { getUUID } from '../../../utils' +import { RangeManager } from '../../range/RangeManager' +import { Draw } from '../Draw' + +export class Group { + private draw: Draw + private options: DeepRequired + private range: RangeManager + private fillRectMap: Map + + constructor(draw: Draw) { + this.draw = draw + this.options = draw.getOptions() + this.range = draw.getRange() + this.fillRectMap = new Map() + } + + public setGroup(): string | null { + if ( + this.draw.isReadonly() || + this.draw.getZone().getZone() !== EditorZone.MAIN + ) { + return null + } + const selection = this.range.getSelection() + if (!selection) return null + const groupId = getUUID() + selection.forEach(el => { + if (!Array.isArray(el.groupIds)) { + el.groupIds = [] + } + el.groupIds.push(groupId) + }) + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + return groupId + } + + public getElementListByGroupId( + elementList: IElement[], + groupId: string + ): IElement[] { + const groupElementList: IElement[] = [] + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const tdGroupElementList = this.getElementListByGroupId( + td.value, + groupId + ) + if (tdGroupElementList.length) { + groupElementList.push(...tdGroupElementList) + return groupElementList + } + } + } + } + if (element?.groupIds?.includes(groupId)) { + groupElementList.push(element) + const nextElement = elementList[e + 1] + if (!nextElement?.groupIds?.includes(groupId)) break + } + } + return groupElementList + } + + public deleteGroup(groupId: string) { + if (this.draw.isReadonly()) return + // 仅主体内容可以成组 + const elementList = this.draw.getOriginalMainElementList() + const groupElementList = this.getElementListByGroupId(elementList, groupId) + if (!groupElementList.length) return + for (let e = 0; e < groupElementList.length; e++) { + const element = groupElementList[e] + const groupIds = element.groupIds! + const groupIndex = groupIds.findIndex(id => id === groupId) + groupIds.splice(groupIndex, 1) + // 不包含成组时删除字段,减少存储及内存占用 + if (!groupIds.length) { + delete element.groupIds + } + } + this.draw.render({ + isSetCursor: false, + isCompute: false + }) + } + + public getContextByGroupId( + elementList: IElement[], + groupId: string + ): (IRange & IPositionContext) | null { + for (let e = 0; e < elementList.length; e++) { + const element = elementList[e] + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + const range = this.getContextByGroupId(td.value, groupId) + if (range) { + return { + ...range, + isTable: true, + index: e, + trIndex: r, + tdIndex: d, + tdId: td.id, + trId: tr.id, + tableId: element.tableId + } + } + } + } + } + const nextElement = elementList[e + 1] + if ( + element.groupIds?.includes(groupId) && + !nextElement?.groupIds?.includes(groupId) + ) { + return { + isTable: false, + startIndex: e, + endIndex: e + } + } + } + return null + } + + public clearFillInfo() { + this.fillRectMap.clear() + } + + public recordFillInfo( + element: IElement, + x: number, + y: number, + width: number, + height: number + ) { + const groupIds = element.groupIds + if (!groupIds) return + for (const groupId of groupIds) { + const fillRect = this.fillRectMap.get(groupId) + if (!fillRect) { + this.fillRectMap.set(groupId, { + x, + y, + width, + height + }) + } else { + fillRect.width += width + } + } + } + + public render(ctx: CanvasRenderingContext2D) { + if (!this.fillRectMap.size) return + // 当前激活组信息 + const range = this.range.getRange() + const elementList = this.draw.getElementList() + const anchorGroupIds = elementList[range.endIndex]?.groupIds + const { + group: { backgroundColor, opacity, activeOpacity, activeBackgroundColor } + } = this.options + ctx.save() + this.fillRectMap.forEach((fillRect, groupId) => { + const { x, y, width, height } = fillRect + if (anchorGroupIds?.includes(groupId)) { + ctx.globalAlpha = activeOpacity + ctx.fillStyle = activeBackgroundColor + } else { + ctx.globalAlpha = opacity + ctx.fillStyle = backgroundColor + } + ctx.fillRect(x, y, width, height) + }) + ctx.restore() + this.clearFillInfo() + } +} diff --git a/src/editor/core/event/handlers/input.ts b/src/editor/core/event/handlers/input.ts index a5451f4..a6de7ca 100644 --- a/src/editor/core/event/handlers/input.ts +++ b/src/editor/core/event/handlers/input.ts @@ -50,6 +50,8 @@ export function input(data: string, host: CanvasEvent) { (copyElement.type === SUPERSCRIPT && nextElement?.type === SUPERSCRIPT) ) { EDITOR_ELEMENT_COPY_ATTR.forEach(attr => { + // 在分组外无需复制分组信息 + if (attr === 'groupIds' && !nextElement?.groupIds) return const value = copyElement[attr] as never if (value !== undefined) { newElement[attr] = value diff --git a/src/editor/core/range/RangeManager.ts b/src/editor/core/range/RangeManager.ts index c31469d..fbbf222 100644 --- a/src/editor/core/range/RangeManager.ts +++ b/src/editor/core/range/RangeManager.ts @@ -339,6 +339,8 @@ export class RangeManager { const painter = !!this.draw.getPainterStyle() const undo = this.historyManager.isCanUndo() const redo = this.historyManager.isCanRedo() + // 组信息 + const groupIds = curElement.groupIds || null const rangeStyle: IRangeStyle = { type, undo, @@ -357,7 +359,8 @@ export class RangeManager { dashArray, level, listType, - listStyle + listStyle, + groupIds } if (rangeStyleChangeListener) { rangeStyleChangeListener(rangeStyle) @@ -396,7 +399,8 @@ export class RangeManager { dashArray: [], level: null, listType: null, - listStyle: null + listStyle: null, + groupIds: null } if (rangeStyleChangeListener) { rangeStyleChangeListener(rangeStyle) diff --git a/src/editor/core/worker/WorkerManager.ts b/src/editor/core/worker/WorkerManager.ts index 982f6c8..67d8009 100644 --- a/src/editor/core/worker/WorkerManager.ts +++ b/src/editor/core/worker/WorkerManager.ts @@ -1,17 +1,20 @@ import { Draw } from '../draw/Draw' import WordCountWorker from './works/wordCount?worker&inline' import CatalogWorker from './works/catalog?worker&inline' +import GroupWorker from './works/group?worker&inline' import { ICatalog } from '../../interface/Catalog' export class WorkerManager { private draw: Draw private wordCountWorker: Worker private catalogWorker: Worker + private groupWorker: Worker constructor(draw: Draw) { this.draw = draw this.wordCountWorker = new WordCountWorker() this.catalogWorker = new CatalogWorker() + this.groupWorker = new GroupWorker() } public getWordCount(): Promise { @@ -43,4 +46,19 @@ export class WorkerManager { this.catalogWorker.postMessage(elementList) }) } + + public getGroupIds(): Promise { + return new Promise((resolve, reject) => { + this.groupWorker.onmessage = evt => { + resolve(evt.data) + } + + this.groupWorker.onerror = evt => { + reject(evt) + } + + const elementList = this.draw.getOriginalMainElementList() + this.groupWorker.postMessage(elementList) + }) + } } diff --git a/src/editor/core/worker/works/group.ts b/src/editor/core/worker/works/group.ts new file mode 100644 index 0000000..67ae7af --- /dev/null +++ b/src/editor/core/worker/works/group.ts @@ -0,0 +1,34 @@ +import { IElement } from '../../../interface/Element' + +enum ElementType { + TABLE = 'table' +} + +function getGroupIds(elementList: IElement[]): string[] { + const groupIds: string[] = [] + for (const element of elementList) { + if (element.type === ElementType.TABLE) { + const trList = element.trList! + for (let r = 0; r < trList.length; r++) { + const tr = trList[r] + for (let d = 0; d < tr.tdList.length; d++) { + const td = tr.tdList[d] + groupIds.push(...getGroupIds(td.value)) + } + } + } + if (!element.groupIds) continue + for (const groupId of element.groupIds) { + if (!groupIds.includes(groupId)) { + groupIds.push(groupId) + } + } + } + return groupIds +} + +onmessage = evt => { + const elementList = evt.data + const groupIds = getGroupIds(elementList) + postMessage(groupIds) +} diff --git a/src/editor/dataset/constant/Element.ts b/src/editor/dataset/constant/Element.ts index 54fbaa1..947f14a 100644 --- a/src/editor/dataset/constant/Element.ts +++ b/src/editor/dataset/constant/Element.ts @@ -26,7 +26,8 @@ export const EDITOR_ELEMENT_COPY_ATTR: Array = [ 'url', 'hyperlinkId', 'dateId', - 'dateFormat' + 'dateFormat', + 'groupIds' ] export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ @@ -56,7 +57,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array = [ 'level', 'listType', 'listStyle', - 'listWrap' + 'listWrap', + 'groupIds' ] export const TABLE_CONTEXT_ATTR: Array = [ diff --git a/src/editor/dataset/constant/Group.ts b/src/editor/dataset/constant/Group.ts new file mode 100644 index 0000000..46badbd --- /dev/null +++ b/src/editor/dataset/constant/Group.ts @@ -0,0 +1,9 @@ +import { IGroup } from '../../interface/Group' + +export const defaultGroupOption: Readonly> = { + opacity: 0.1, + backgroundColor: '#E99D00', + activeOpacity: 0.5, + activeBackgroundColor: '#E99D00', + disabled: false +} diff --git a/src/editor/dataset/enum/Editor.ts b/src/editor/dataset/enum/Editor.ts index 175f4e9..94960a5 100644 --- a/src/editor/dataset/enum/Editor.ts +++ b/src/editor/dataset/enum/Editor.ts @@ -5,7 +5,8 @@ export enum EditorComponent { FOOTER = 'footer', CONTEXTMENU = 'contextmenu', POPUP = 'popup', - CATALOG = 'catalog' + CATALOG = 'catalog', + COMMENT = 'comment' } export enum EditorContext { diff --git a/src/editor/index.ts b/src/editor/index.ts index e8a8492..dbb8234 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -59,6 +59,8 @@ import { Plugin } from './core/plugin/Plugin' import { UsePlugin } from './interface/Plugin' import { EventBus } from './core/event/eventbus/EventBus' import { EventBusMap } from './interface/EventBus' +import { IGroup } from './interface/Group' +import { defaultGroupOption } from './dataset/constant/Group' export default class Editor { public command: Command @@ -109,6 +111,10 @@ export default class Editor { ...defaultPlaceholderOption, ...options.placeholder } + const groupOptions: Required = { + ...defaultGroupOption, + ...options.group + } const editorOptions: DeepRequired = { mode: EditorMode.EDIT, @@ -158,7 +164,8 @@ export default class Editor { checkbox: checkboxOptions, cursor: cursorOptions, title: titleOptions, - placeholder: placeholderOptions + placeholder: placeholderOptions, + group: groupOptions } // 数据处理 let headerElementList: IElement[] = [] diff --git a/src/editor/interface/Draw.ts b/src/editor/interface/Draw.ts index 0cf79ed..36fe120 100644 --- a/src/editor/interface/Draw.ts +++ b/src/editor/interface/Draw.ts @@ -9,6 +9,7 @@ export interface IDrawOption { isCompute?: boolean isLazy?: boolean isInit?: boolean + isSourceHistory?: boolean } export interface IDrawImagePayload { diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 57038da..3a3ba73 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -9,6 +9,7 @@ import { ICheckboxOption } from './Checkbox' import { IControlOption } from './Control' import { ICursorOption } from './Cursor' import { IFooter } from './Footer' +import { IGroup } from './Group' import { IHeader } from './Header' import { IMargin } from './Margin' import { IPageNumber } from './PageNumber' @@ -70,6 +71,7 @@ export interface IEditorOption { cursor?: ICursorOption title?: ITitleOption placeholder?: IPlaceholder + group?: IGroup } export interface IEditorResult { diff --git a/src/editor/interface/Element.ts b/src/editor/interface/Element.ts index df83d4e..d85b297 100644 --- a/src/editor/interface/Element.ts +++ b/src/editor/interface/Element.ts @@ -32,6 +32,10 @@ export interface IElementStyle { letterSpacing?: number } +export interface IElementGroup { + groupIds?: string[] +} + export interface ITitleElement { valueList?: IElement[] level?: TitleLevel @@ -103,6 +107,7 @@ export interface IBlockElement { export type IElement = IElementBasic & IElementStyle & + IElementGroup & ITable & IHyperlinkElement & ISuperscriptSubscript & diff --git a/src/editor/interface/Group.ts b/src/editor/interface/Group.ts new file mode 100644 index 0000000..c286be7 --- /dev/null +++ b/src/editor/interface/Group.ts @@ -0,0 +1,7 @@ +export interface IGroup { + opacity?: number + backgroundColor?: string + activeOpacity?: number + activeBackgroundColor?: string + disabled?: boolean +} diff --git a/src/editor/interface/Listener.ts b/src/editor/interface/Listener.ts index e0a57b1..2d473ae 100644 --- a/src/editor/interface/Listener.ts +++ b/src/editor/interface/Listener.ts @@ -29,6 +29,7 @@ export interface IRangeStyle { level: TitleLevel | null listType: ListType | null listStyle: ListStyle | null + groupIds: string[] | null } export type IRangeStyleChange = (payload: IRangeStyle) => void diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 345988b..c62cde4 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -1,4 +1,4 @@ -import { cloneProperty, deepClone, getUUID, splitText } from '.' +import { cloneProperty, deepClone, getUUID, isArrayEqual, splitText } from '.' import { ElementType, IEditorOption, @@ -375,7 +375,17 @@ export function isSameElementExceptValue( if (sourceKeys.length !== targetKeys.length) return false for (let s = 0; s < sourceKeys.length; s++) { const key = sourceKeys[s] as never + // 值不需要校验 if (key === 'value') continue + // groupIds数组需特殊校验数组是否相等 + if ( + key === 'groupIds' && + Array.isArray(source[key]) && + Array.isArray(target[key]) && + isArrayEqual(source[key], target[key]) + ) { + continue + } if (source[key] !== target[key]) { return false } diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts index f3a1532..f1ae72b 100644 --- a/src/editor/utils/index.ts +++ b/src/editor/utils/index.ts @@ -249,3 +249,10 @@ export function findScrollContainer(element: HTMLElement) { } return document.documentElement } + +export function isArrayEqual(arr1: unknown[], arr2: unknown[]): boolean { + if (arr1.length !== arr2.length) { + return false + } + return !arr1.some(item => !arr2.includes(item)) +}