feat: add element group

pr675
Hufe921 3 years ago committed by Hufe
parent f19077b2ce
commit 3e183aefa5

@ -761,3 +761,33 @@ Usage:
```javascript
instance.command.executeSetHTML(payload: Partial<IEditorHTML)
```
## executeSetGroup
Feature: Set group
Usage:
```javascript
instance.command.executeSetGroup()
```
## executeDeleteGroup
Feature: Delete group
Usage:
```javascript
instance.command.executeDeleteGroup(groupId: string)
```
## executeLocationGroup
Feature: Positioning group position
Usage:
```javascript
instance.command.executeLocationGroup(groupId: string)
```

@ -151,10 +151,20 @@ const {
## getLocale
功能:Get current locale
Feature: Get current locale
用法:
Usage:
```javascript
const locale = await instance.command.getLocale()
```
## getGroupIds
Feature: Get all group ids
Usage:
```javascript
const groupIds = await instance.command.getGroupIds()
```

@ -61,6 +61,7 @@ interface IEditorOption {
cursor?: ICursorOption // Cursor style. {width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // Title configuration.{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // Placeholder text
group?: IGroup // Group option. {opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean}
}
```

@ -41,6 +41,8 @@ interface IElement {
};
rowMargin?: number;
letterSpacing?: number;
// groupIds
groupIds?: string[];
// table
colgroup?: {
width: number;

@ -763,3 +763,33 @@ instance.command.executeWordTool()
```javascript
instance.command.executeSetHTML(payload: Partial<IEditorHTML)
```
## executeSetGroup
功能:设置成组
用法:
```javascript
instance.command.executeSetGroup()
```
## executeDeleteGroup
功能:删除成组
用法:
```javascript
instance.command.executeDeleteGroup(groupId: string)
```
## executeLocationGroup
功能:定位成组位置
用法:
```javascript
instance.command.executeLocationGroup(groupId: string)
```

@ -158,3 +158,13 @@ const {
```javascript
const locale = await instance.command.getLocale()
```
## getGroupIds
功能:获取所有成组 id
用法:
```javascript
const groupIds = await instance.command.getGroupIds()
```

@ -61,6 +61,7 @@ interface IEditorOption {
cursor?: ICursorOption // 光标样式。{width?: number; color?: string; dragWidth?: number; dragColor?: string;}
title?: ITitleOption // 标题配置。{ defaultFirstSize?: number; defaultSecondSize?: number; defaultThirdSize?: number defaultFourthSize?: number; defaultFifthSize?: number; defaultSixthSize?: number;}
placeholder?: IPlaceholder // 编辑器空白占位文本
group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean}
}
```

@ -41,6 +41,8 @@ interface IElement {
};
rowMargin?: number;
letterSpacing?: number;
// 组信息-可用于批注等其他成组使用场景
groupIds?: string[];
// 表格
colgroup?: {
width: number;

@ -76,6 +76,9 @@ export class Command {
public executeLocationCatalog: CommandAdapt['locationCatalog']
public executeWordTool: CommandAdapt['wordTool']
public executeSetHTML: CommandAdapt['setHTML']
public executeSetGroup: CommandAdapt['setGroup']
public executeDeleteGroup: CommandAdapt['deleteGroup']
public executeLocationGroup: CommandAdapt['locationGroup']
public getCatalog: CommandAdapt['getCatalog']
public getImage: CommandAdapt['getImage']
public getValue: CommandAdapt['getValue']
@ -89,6 +92,7 @@ export class Command {
public getPaperMargin: CommandAdapt['getPaperMargin']
public getSearchNavigateInfo: CommandAdapt['getSearchNavigateInfo']
public getLocale: CommandAdapt['getLocale']
public getGroupIds: CommandAdapt['getGroupIds']
constructor(adapt: CommandAdapt) {
// 全局命令
@ -173,7 +177,9 @@ export class Command {
this.executeLocationCatalog = adapt.locationCatalog.bind(adapt)
this.executeWordTool = adapt.wordTool.bind(adapt)
this.executeSetHTML = adapt.setHTML.bind(adapt)
this.executeSetGroup = adapt.setGroup.bind(adapt)
this.executeDeleteGroup = adapt.deleteGroup.bind(adapt)
this.executeLocationGroup = adapt.locationGroup.bind(adapt)
// 获取
this.getImage = adapt.getImage.bind(adapt)
this.getValue = adapt.getValue.bind(adapt)
@ -188,5 +194,6 @@ export class Command {
this.getPaperMargin = adapt.getPaperMargin.bind(adapt)
this.getSearchNavigateInfo = adapt.getSearchNavigateInfo.bind(adapt)
this.getLocale = adapt.getLocale.bind(adapt)
this.getGroupIds = adapt.getGroupIds.bind(adapt)
}
}

@ -321,7 +321,10 @@ export class CommandAdapt {
selection.forEach(el => {
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<string[]> {
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
})
}
}

@ -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()
}

@ -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<IEditorOption>
private range: RangeManager
private fillRectMap: Map<string, IElementFillRect>
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()
}
}

@ -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

@ -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)

@ -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<number> {
@ -43,4 +46,19 @@ export class WorkerManager {
this.catalogWorker.postMessage(elementList)
})
}
public getGroupIds(): Promise<string[]> {
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)
})
}
}

@ -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 = <IElement[]>evt.data
const groupIds = getGroupIds(elementList)
postMessage(groupIds)
}

@ -26,7 +26,8 @@ export const EDITOR_ELEMENT_COPY_ATTR: Array<keyof IElement> = [
'url',
'hyperlinkId',
'dateId',
'dateFormat'
'dateFormat',
'groupIds'
]
export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
@ -56,7 +57,8 @@ export const EDITOR_ELEMENT_ZIP_ATTR: Array<keyof IElement> = [
'level',
'listType',
'listStyle',
'listWrap'
'listWrap',
'groupIds'
]
export const TABLE_CONTEXT_ATTR: Array<keyof IElement> = [

@ -0,0 +1,9 @@
import { IGroup } from '../../interface/Group'
export const defaultGroupOption: Readonly<Required<IGroup>> = {
opacity: 0.1,
backgroundColor: '#E99D00',
activeOpacity: 0.5,
activeBackgroundColor: '#E99D00',
disabled: false
}

@ -5,7 +5,8 @@ export enum EditorComponent {
FOOTER = 'footer',
CONTEXTMENU = 'contextmenu',
POPUP = 'popup',
CATALOG = 'catalog'
CATALOG = 'catalog',
COMMENT = 'comment'
}
export enum EditorContext {

@ -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<IGroup> = {
...defaultGroupOption,
...options.group
}
const editorOptions: DeepRequired<IEditorOption> = {
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[] = []

@ -9,6 +9,7 @@ export interface IDrawOption {
isCompute?: boolean
isLazy?: boolean
isInit?: boolean
isSourceHistory?: boolean
}
export interface IDrawImagePayload {

@ -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 {

@ -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 &

@ -0,0 +1,7 @@
export interface IGroup {
opacity?: number
backgroundColor?: string
activeOpacity?: number
activeBackgroundColor?: string
disabled?: boolean
}

@ -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

@ -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
}

@ -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))
}

Loading…
Cancel
Save