diff --git a/src/assets/images/signature.svg b/src/assets/images/signature.svg
new file mode 100644
index 0000000..57a007f
--- /dev/null
+++ b/src/assets/images/signature.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/trash.svg b/src/assets/images/trash.svg
new file mode 100644
index 0000000..c9852d1
--- /dev/null
+++ b/src/assets/images/trash.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/signature/Signature.ts b/src/components/signature/Signature.ts
new file mode 100644
index 0000000..ad9d33c
--- /dev/null
+++ b/src/components/signature/Signature.ts
@@ -0,0 +1,186 @@
+import { EditorComponent, EDITOR_COMPONENT } from '../../editor'
+import './signature.css'
+
+export interface ISignatureConfirm {
+ value: string;
+ width: number;
+ height: number;
+}
+
+export interface ISignatureOptions {
+ width?: number;
+ height?: number;
+ onClose?: () => void;
+ onCancel?: () => void;
+ onConfirm?: (payload: ISignatureConfirm) => void;
+}
+
+export class Signature {
+ private x: number
+ private y: number
+ private isDrawing: boolean
+ private canvasWidth: number
+ private canvasHeight: number
+ private options: ISignatureOptions
+ private mask: HTMLDivElement
+ private container: HTMLDivElement
+ private trashContainer: HTMLDivElement
+ private canvas: HTMLCanvasElement
+ private ctx: CanvasRenderingContext2D
+
+ constructor(options: ISignatureOptions) {
+ this.x = 0
+ this.y = 0
+ this.isDrawing = false
+ this.options = options
+ this.canvasWidth = options.width || 390
+ this.canvasHeight = options.height || 180
+ const { mask, container, trashContainer, canvas } = this._render()
+ this.mask = mask
+ this.container = container
+ this.trashContainer = trashContainer
+ this.canvas = canvas
+ this.ctx = canvas.getContext('2d')
+ this.ctx.lineCap = 'round'
+ this._bindEvent()
+ }
+
+ private _render() {
+ const { onClose, onCancel, onConfirm } = this.options
+ // 渲染遮罩层
+ const mask = document.createElement('div')
+ mask.classList.add('signature-mask')
+ mask.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
+ document.body.append(mask)
+ // 渲染容器
+ const container = document.createElement('div')
+ container.classList.add('signature-container')
+ container.setAttribute(EDITOR_COMPONENT, EditorComponent.COMPONENT)
+ // 弹窗
+ const signatureContainer = document.createElement('div')
+ signatureContainer.classList.add('signature')
+ container.append(signatureContainer)
+ // 标题容器
+ const titleContainer = document.createElement('div')
+ titleContainer.classList.add('signature-title')
+ // 标题&关闭按钮
+ const titleSpan = document.createElement('span')
+ titleSpan.append(document.createTextNode('插入签名'))
+ const titleClose = document.createElement('i')
+ titleClose.onclick = () => {
+ if (onClose) {
+ onClose()
+ }
+ this._dispose()
+ }
+ titleContainer.append(titleSpan)
+ titleContainer.append(titleClose)
+ signatureContainer.append(titleContainer)
+ // 操作区
+ const operationContainer = document.createElement('div')
+ operationContainer.classList.add('signature-operation')
+ const trashContainer = document.createElement('div')
+ trashContainer.classList.add('signature-operation__trash')
+ const trashIcon = document.createElement('i')
+ const trashLabel = document.createElement('span')
+ trashLabel.innerText = '清空画布'
+ trashContainer.append(trashIcon)
+ trashContainer.append(trashLabel)
+ operationContainer.append(trashContainer)
+ signatureContainer.append(operationContainer)
+ // 绘图区
+ const canvasContainer = document.createElement('div')
+ canvasContainer.classList.add('signature-canvas')
+ const canvas = document.createElement('canvas')
+ canvas.width = this.canvasWidth
+ canvas.height = this.canvasHeight
+ canvas.style.width = `${this.canvasWidth}px`
+ canvas.style.height = `${this.canvasHeight}px`
+ canvasContainer.append(canvas)
+ signatureContainer.append(canvasContainer)
+ // 按钮容器
+ const menuContainer = document.createElement('div')
+ menuContainer.classList.add('signature-menu')
+ // 取消按钮
+ const cancelBtn = document.createElement('button')
+ cancelBtn.classList.add('signature-menu__cancel')
+ cancelBtn.append(document.createTextNode('取消'))
+ cancelBtn.type = 'default'
+ cancelBtn.onclick = () => {
+ if (onCancel) {
+ onCancel()
+ }
+ this._dispose()
+ }
+ menuContainer.append(cancelBtn)
+ // 确认按钮
+ const confirmBtn = document.createElement('button')
+ confirmBtn.append(document.createTextNode('确定'))
+ confirmBtn.type = 'primary'
+ confirmBtn.onclick = () => {
+ if (onConfirm) {
+ onConfirm({
+ width: this.canvasWidth,
+ height: this.canvasHeight,
+ value: this._toDataURL()
+ })
+ }
+ this._dispose()
+ }
+ menuContainer.append(confirmBtn)
+ signatureContainer.append(menuContainer)
+ // 渲染
+ document.body.append(container)
+ this.container = container
+ this.mask = mask
+ return {
+ mask,
+ canvas,
+ container,
+ trashContainer
+ }
+ }
+
+ private _bindEvent() {
+ this.trashContainer.onclick = this._clearCanvas.bind(this)
+ this.canvas.onmousedown = this._startDraw.bind(this)
+ this.canvas.onmousemove = this._draw.bind(this)
+ this.container.onmouseup = this._stopDraw.bind(this)
+ }
+
+ private _clearCanvas() {
+ this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+ }
+
+ private _startDraw(evt: MouseEvent) {
+ this.isDrawing = true
+ this.x = evt.offsetX
+ this.y = evt.offsetY
+ this.ctx.lineWidth = 5
+ }
+
+ private _draw(evt: MouseEvent) {
+ if (!this.isDrawing) return
+ const { offsetX, offsetY } = evt
+ this.ctx.beginPath()
+ this.ctx.moveTo(this.x, this.y)
+ this.ctx.lineTo(offsetX, offsetY)
+ this.ctx.stroke()
+ this.x = offsetX
+ this.y = offsetY
+ }
+
+ private _stopDraw() {
+ this.isDrawing = false
+ }
+
+ private _toDataURL() {
+ return this.canvas.toDataURL()
+ }
+
+ private _dispose() {
+ this.mask.remove()
+ this.container.remove()
+ }
+
+}
\ No newline at end of file
diff --git a/src/components/signature/signature.css b/src/components/signature/signature.css
new file mode 100644
index 0000000..e7121d8
--- /dev/null
+++ b/src/components/signature/signature.css
@@ -0,0 +1,122 @@
+.signature-mask {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ opacity: .5;
+ background: #000000;
+ z-index: 99;
+}
+
+.signature-container {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ overflow: auto;
+ z-index: 999;
+ margin: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.signature {
+ position: absolute;
+ padding: 0 30px 30px;
+ background: #ffffff;
+ box-shadow: 0 2px 12px 0 rgb(56 56 56 / 20%);
+ border: 1px solid #e2e6ed;
+ border-radius: 2px;
+}
+
+.signature-title {
+ position: relative;
+ border-bottom: 1px solid #e2e6ed;
+ margin-bottom: 15px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.signature-title i {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+ display: inline-block;
+ background: url(../../assets/images/close.svg);
+}
+
+.signature-operation__trash {
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ color: #3d4757;
+ user-select: none;
+}
+
+.signature-operation__trash:hover {
+ color: #6e7175;
+}
+
+.signature-operation__trash i {
+ width: 24px;
+ height: 24px;
+ display: inline-block;
+ background: url(../../assets/images/trash.svg) no-repeat;
+}
+
+.signature-operation__trash span {
+ font-size: 12px;
+ margin-left: 5px;
+}
+
+.signature-canvas {
+ margin: 15px 0;
+ border: 1px solid #e9e9e9;
+ user-select: none;
+}
+
+.signature-canvas canvas {
+ background: #fbfbfb;
+}
+
+.signature-menu {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.signature-menu button {
+ position: relative;
+ display: inline-block;
+ border: 1px solid #e2e6ed;
+ border-radius: 2px;
+ background: #ffffff;
+ line-height: 22px;
+ padding: 0 16px;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.signature-menu button:hover {
+ background: rgba(25, 55, 88, .04);
+}
+
+.signature-menu__cancel {
+ margin-right: 16px;
+}
+
+.signature-menu button[type='primary'] {
+ color: #ffffff;
+ background: #4991f2;
+ border-color: #4991f2;
+}
+
+.signature-menu button[type='primary']:hover {
+ background: #5b9cf3;
+ border-color: #5b9cf3;
+}
\ No newline at end of file
diff --git a/src/editor/index.ts b/src/editor/index.ts
index 9d22e92..3ccf63e 100644
--- a/src/editor/index.ts
+++ b/src/editor/index.ts
@@ -116,7 +116,8 @@ export {
EditorComponent,
EDITOR_COMPONENT,
PageMode,
- ImageDisplay
+ ImageDisplay,
+ Command
}
// 对外类型
diff --git a/src/main.ts b/src/main.ts
index 0d12c2f..67b249d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,10 @@
import { data, options } from './mock'
import './style.css'
import prism from 'prismjs'
-import Editor, { ControlType, EditorMode, ElementType, IElement, PageMode } from './editor'
+import Editor, { Command, ControlType, EditorMode, ElementType, IElement, PageMode } from './editor'
import { Dialog } from './components/dialog/Dialog'
import { formatPrismToken } from './utils/prism'
+import { Signature } from './components/signature/Signature'
window.onload = function () {
@@ -865,4 +866,29 @@ window.onload = function () {
console.log('elementList: ', payload)
}
+ // 9. 右键菜单注册
+ instance.register.contextMenuList([
+ {
+ name: '签名',
+ icon: 'signature',
+ when: (payload) => {
+ return !payload.isReadonly && payload.editorTextFocus
+ },
+ callback: (command: Command) => {
+ new Signature({
+ onConfirm(payload) {
+ const { value, width, height } = payload
+ if (!value || !width || !height) return
+ command.executeInsertElementList([{
+ value,
+ width,
+ height,
+ type: ElementType.IMAGE
+ }])
+ }
+ })
+ }
+ }
+ ])
+
}
\ No newline at end of file
diff --git a/src/style.css b/src/style.css
index ac0ed6f..5b83959 100644
--- a/src/style.css
+++ b/src/style.css
@@ -699,4 +699,8 @@ ul {
.footer .paper-size .options {
right: 0;
left: unset;
+}
+
+.contextmenu-signature {
+ background-image: url('./assets/images/signature.svg');
}
\ No newline at end of file