diff --git a/docs/en/guide/i18n.md b/docs/en/guide/i18n.md index c45e804..14d2ef5 100644 --- a/docs/en/guide/i18n.md +++ b/docs/en/guide/i18n.md @@ -96,5 +96,9 @@ interface ILang { pageBreak: { displayName: string } + zone: { + headerTip: string + footerTip: string + } } ``` diff --git a/docs/en/guide/option.md b/docs/en/guide/option.md index ed812b1..847db93 100644 --- a/docs/en/guide/option.md +++ b/docs/en/guide/option.md @@ -67,6 +67,7 @@ interface IEditorOption { placeholder?: IPlaceholder // Placeholder text group?: IGroup // Group option. {opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean} pageBreak?: IPageBreak // PageBreak option。{font?:string; fontSize?:number; lineDash?:number[];} + zone?: IZoneOption // Zone option。{tipDisabled?:boolean;} } ``` diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index e01a27d..6814619 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -96,5 +96,9 @@ interface ILang { pageBreak: { displayName: string } + zone: { + headerTip: string + footerTip: string + } } ``` diff --git a/docs/guide/option.md b/docs/guide/option.md index 4a16309..6aeb3a7 100644 --- a/docs/guide/option.md +++ b/docs/guide/option.md @@ -67,6 +67,7 @@ interface IEditorOption { placeholder?: IPlaceholder // 编辑器空白占位文本 group?: IGroup // 成组配置。{opacity?:number; backgroundColor?:string; activeOpacity?:number; activeBackgroundColor?:string; disabled?:boolean} pageBreak?: IPageBreak // 分页符配置。{font?:string; fontSize?:number; lineDash?:number[];} + zone?: IZoneOption // 编辑器区域配置。{tipDisabled?:boolean;} } ``` diff --git a/src/editor/assets/css/zone/zone.css b/src/editor/assets/css/zone/zone.css index a5738bc..a8c2fd3 100644 --- a/src/editor/assets/css/zone/zone.css +++ b/src/editor/assets/css/zone/zone.css @@ -31,4 +31,31 @@ .ce-zone-indicator-border__right { border-right: 2px dashed rgb(238, 238, 238); +} + +.ce-zone-tip { + display: none; + align-items: center; + height: 30px; + white-space: nowrap; + position: fixed; + opacity: .9; + background-color: #000000; + padding: 0 5px; + border-radius: 4px; + z-index: 9; + transition: all .3s; + outline: none; + user-select: none; + pointer-events: none; + transform: translate(10px, 10px); +} + +.ce-zone-tip.show { + display: flex; +} + +.ce-zone-tip span { + color: #ffffff; + font-size: 12px; } \ No newline at end of file diff --git a/src/editor/core/i18n/lang/en.json b/src/editor/core/i18n/lang/en.json index 1a76564..740da22 100644 --- a/src/editor/core/i18n/lang/en.json +++ b/src/editor/core/i18n/lang/en.json @@ -76,5 +76,9 @@ }, "pageBreak": { "displayName": "Page Break" + }, + "zone": { + "headerTip": "Double click to edit header", + "footerTip": "Double click to edit footer" } } diff --git a/src/editor/core/i18n/lang/zh-CN.json b/src/editor/core/i18n/lang/zh-CN.json index 97589d6..f9ddce7 100644 --- a/src/editor/core/i18n/lang/zh-CN.json +++ b/src/editor/core/i18n/lang/zh-CN.json @@ -76,5 +76,9 @@ }, "pageBreak": { "displayName": "分页符" + }, + "zone": { + "headerTip": "双击编辑页眉", + "footerTip": "双击编辑页脚" } } diff --git a/src/editor/core/position/Position.ts b/src/editor/core/position/Position.ts index ee55afe..02a8cd2 100644 --- a/src/editor/core/position/Position.ts +++ b/src/editor/core/position/Position.ts @@ -288,7 +288,7 @@ export class Position { positionList = this.getOriginalPositionList() } const zoneManager = this.draw.getZone() - const curPageNo = this.draw.getPageNo() + const curPageNo = payload.pageNo ?? this.draw.getPageNo() const isMainActive = zoneManager.isMainActive() const positionNo = isMainActive ? curPageNo : 0 for (let j = 0; j < positionList.length; j++) { diff --git a/src/editor/core/zone/Zone.ts b/src/editor/core/zone/Zone.ts index ad8cc05..a31e19c 100644 --- a/src/editor/core/zone/Zone.ts +++ b/src/editor/core/zone/Zone.ts @@ -4,6 +4,7 @@ import { IEditorOption } from '../../interface/Editor' import { nextTick } from '../../utils' import { Draw } from '../draw/Draw' import { I18n } from '../i18n/I18n' +import { ZoneTip } from './ZoneTip' export class Zone { private readonly INDICATOR_PADDING = 2 @@ -24,6 +25,10 @@ export class Zone { this.container = draw.getContainer() this.currentZone = EditorZone.MAIN this.indicatorContainer = null + // 区域提示 + if (!this.options.zone.tipDisabled) { + new ZoneTip(draw) + } } public isHeaderActive(): boolean { diff --git a/src/editor/core/zone/ZoneTip.ts b/src/editor/core/zone/ZoneTip.ts new file mode 100644 index 0000000..5635a38 --- /dev/null +++ b/src/editor/core/zone/ZoneTip.ts @@ -0,0 +1,94 @@ +import { EDITOR_PREFIX } from '../../dataset/constant/Editor' +import { EditorZone } from '../../dataset/enum/Editor' +import { throttle } from '../../utils' +import { Draw } from '../draw/Draw' +import { I18n } from '../i18n/I18n' +import { Position } from '../position/Position' + +export class ZoneTip { + private position: Position + private i18n: I18n + private container: HTMLDivElement + private pageContainer: HTMLDivElement + + private isDisableMouseMove: boolean + private tipContainer: HTMLDivElement + private tipContent: HTMLSpanElement + private currentMoveZone: EditorZone | undefined + + constructor(draw: Draw) { + this.position = draw.getPosition() + this.i18n = draw.getI18n() + this.container = draw.getContainer() + this.pageContainer = draw.getPageContainer() + + const { tipContainer, tipContent } = this._drawZoneTip() + this.tipContainer = tipContainer + this.tipContent = tipContent + this.isDisableMouseMove = true + this.currentMoveZone = EditorZone.MAIN + + this._watchMouseMoveZoneChange() + } + + private _watchMouseMoveZoneChange() { + this.pageContainer.addEventListener( + 'mousemove', + throttle((evt: MouseEvent) => { + if (this.isDisableMouseMove) return + const pageNo = Number((evt.target).dataset.index) + if (Number.isNaN(pageNo)) { + this._updateZoneTip(false) + } else { + const positionInfo = this.position.getPositionByXY({ + x: evt.offsetX, + y: evt.offsetY, + pageNo + }) + this.currentMoveZone = positionInfo.zone + this._updateZoneTip( + positionInfo.zone === EditorZone.HEADER || + positionInfo.zone === EditorZone.FOOTER, + evt.x, + evt.y + ) + } + }, 250) + ) + // mouseenter后mousemove有效,避免因节流导致的mouseleave后继续执行逻辑 + this.pageContainer.addEventListener('mouseenter', () => { + this.isDisableMouseMove = false + }) + this.pageContainer.addEventListener('mouseleave', () => { + this.isDisableMouseMove = true + this._updateZoneTip(false) + }) + } + + private _drawZoneTip() { + const tipContainer = document.createElement('div') + tipContainer.classList.add(`${EDITOR_PREFIX}-zone-tip`) + const tipContent = document.createElement('span') + tipContainer.append(tipContent) + this.container.append(tipContainer) + return { + tipContainer, + tipContent + } + } + + private _updateZoneTip(visible: boolean, left?: number, top?: number) { + if (visible) { + this.tipContainer.classList.add('show') + this.tipContainer.style.left = `${left}px` + this.tipContainer.style.top = `${top}px` + this.tipContent.innerText = this.i18n.t( + `zone.${ + this.currentMoveZone === EditorZone.HEADER ? 'headerTip' : 'footerTip' + }` + ) + } else { + this.tipContainer.classList.remove('show') + } + } +} diff --git a/src/editor/dataset/constant/Zone.ts b/src/editor/dataset/constant/Zone.ts new file mode 100644 index 0000000..4cb3d30 --- /dev/null +++ b/src/editor/dataset/constant/Zone.ts @@ -0,0 +1,5 @@ +import { IZoneOption } from '../../interface/Zone' + +export const defaultZoneOption: Readonly> = { + tipDisabled: true +} diff --git a/src/editor/index.ts b/src/editor/index.ts index 7871a30..6572ebb 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -73,6 +73,8 @@ import { LETTER_CLASS } from './dataset/constant/Common' import { INTERNAL_CONTEXT_MENU_KEY } from './dataset/constant/ContextMenu' import { IRange } from './interface/Range' import { deepClone, splitText } from './utils' +import { IZoneOption } from './interface/Zone' +import { defaultZoneOption } from './dataset/constant/Zone' export default class Editor { public command: Command @@ -132,6 +134,10 @@ export default class Editor { ...defaultPageBreakOption, ...options.pageBreak } + const zoneOptions: Required = { + ...defaultZoneOption, + ...options.zone + } const editorOptions: DeepRequired = { mode: EditorMode.EDIT, @@ -187,7 +193,8 @@ export default class Editor { title: titleOptions, placeholder: placeholderOptions, group: groupOptions, - pageBreak: pageBreakOptions + pageBreak: pageBreakOptions, + zone: zoneOptions } // 数据处理 data = deepClone(data) diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 6599955..8e547ae 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -18,6 +18,7 @@ import { IPageNumber } from './PageNumber' import { IPlaceholder } from './Placeholder' import { ITitleOption } from './Title' import { IWatermark } from './Watermark' +import { IZoneOption } from './Zone' export interface IEditorData { header?: IElement[] @@ -79,6 +80,7 @@ export interface IEditorOption { placeholder?: IPlaceholder group?: IGroup pageBreak?: IPageBreak + zone?: IZoneOption } export interface IEditorResult { diff --git a/src/editor/interface/Position.ts b/src/editor/interface/Position.ts index 06f2558..7d9f761 100644 --- a/src/editor/interface/Position.ts +++ b/src/editor/interface/Position.ts @@ -24,6 +24,7 @@ export interface ICurrentPosition { export interface IGetPositionByXYPayload { x: number y: number + pageNo?: number isTable?: boolean td?: ITd tablePosition?: IElementPosition diff --git a/src/editor/interface/Zone.ts b/src/editor/interface/Zone.ts new file mode 100644 index 0000000..bec8627 --- /dev/null +++ b/src/editor/interface/Zone.ts @@ -0,0 +1,3 @@ +export interface IZoneOption { + tipDisabled?: boolean +} diff --git a/src/editor/utils/index.ts b/src/editor/utils/index.ts index 6654506..8f2242f 100644 --- a/src/editor/utils/index.ts +++ b/src/editor/utils/index.ts @@ -12,6 +12,25 @@ export function debounce(func: Function, delay: number) { } } +export function throttle(func: Function, delay: number) { + let lastExecTime = 0 + let timer: number + return function (this: any, ...args: any[]) { + const currentTime = Date.now() + if (currentTime - lastExecTime >= delay) { + window.clearTimeout(timer) + func.apply(this, args) + lastExecTime = currentTime + } else { + window.clearTimeout(timer) + timer = window.setTimeout(() => { + func.apply(this, args) + lastExecTime = currentTime + }, delay) + } + } +} + export function deepClone(obj: T): T { if (!obj || typeof obj !== 'object') { return obj