Quellcode durchsuchen

add: 内容编辑新增 undo redo

liutian vor 4 Wochen
Ursprung
Commit
152d743c56

+ 191 - 71
packages/core/src/editor/content_container.js

@@ -14,10 +14,11 @@ export class ContentContainer {
     this.viewport = options.viewport
     this.scale = options.scale
     this.$t = options.$t
+    this.historyManager = options.contentEditHistoryManager
 
-    this._cancelled = false
     this.destroyed = false
     this.rendered = false
+    this.isRendering = false
     this.frameEditorList = []
     this.tool = this.pageViewer.tool || options.tool || ''
     this.color = this.pageViewer.color || ''
@@ -29,11 +30,31 @@ export class ContentContainer {
   }
 
   async init() {
+    await this.update()
+
+    document.addEventListener('keydown', this.onKeydown)
+    this.eventBus._on('toolChanged', this.onHandleTool)
+    this.render()
+  }
+
+  async update() {
     const { editPagePtr } = await this.messageHandler.sendWithPromise('InitEditPage', {
       pagePtr: this.pagePtr
     })
     this.editPagePtr = editPagePtr
     await this.initFont()
+
+    const result = await this.messageHandler.sendWithPromise('BeginEdit', {
+      editPagePtr: this.editPagePtr,
+      type: 1 | 2
+    })
+    if (result && result.code) {
+      console.log(result.message)
+      alert(result.message)
+      return false
+    }
+
+    this.tool = this.pageViewer.tool || ''
   }
 
   async initFont() {
@@ -47,80 +68,77 @@ export class ContentContainer {
   }
 
   async render () {
-    if (this._cancelled || this.contentContainer) {
+    if (this.contentContainer) {
       if (!this.initedFont) await this.initFont()
       return;
     }
+    const rendering = this.isRendering
+    this.isRendering = true
 
     const contentContainer = document.createElement('div')
     contentContainer.className = 'contentContainer'
     this.contentContainer = contentContainer
     this.pageDiv.append(this.contentContainer)
 
-    await this.init()
-    
-    this.eventBus._on('toolChanged', this.onHandleTool)
-    document.addEventListener('keydown', this.onKeydown)
-
-    const result = await this.messageHandler.sendWithPromise('BeginEdit', {
-      editPagePtr: this.editPagePtr,
-      type: 1 | 2
+    this.frameEditorList.forEach(editor => {
+      editor.removeViewer()
     })
+    this.frameEditorList.length = 0
 
-    if (result && result.code) {
-      console.log(result.message)
-      alert(result.message)
-      return false
-    }
-
-    const editAreaCount = await this.messageHandler.sendWithPromise('GetEditAreaCount', this.editPagePtr)
-    
-    for (let i = 0; i < editAreaCount; i++) {
-      const editAreaPtr = await this.messageHandler.sendWithPromise('GetEditArea', {
-        editPagePtr: this.editPagePtr,
-        index: i
-      })
-      if (!editAreaPtr) continue
+    try {
+      const editAreaCount = await this.messageHandler.sendWithPromise('GetEditAreaCount', this.editPagePtr)
 
-      const isTextPtr = await this.messageHandler.sendWithPromise('IsTextArea', editAreaPtr)
-      if (isTextPtr) {
-        const frameEditor = new TextEditor({
-          eventBus: this.eventBus,
-          contentContainer: this,
-          container: this.contentContainer,
-          pagePtr: this.pagePtr,
-          editPagePtr: this.editPagePtr,
-          editAreaPtr,
-          editAreaIndex: i,
-          viewport: this.viewport,
-          scale: this.scale,
-          messageHandler: this.messageHandler,
-          pageViewer: this.pageViewer,
-          hidden: this.tool === 'addImage' || this.tool === 'editImage'
-        })
-        this.frameEditorList.push(frameEditor)
-        continue
-      }
+      for (let i = 0; i < editAreaCount; i++) {
+        if (rendering) break
 
-      const isImagePtr = await this.messageHandler.sendWithPromise('IsImageArea', editAreaPtr)
-      if (isImagePtr) {
-        const frameEditor = new ImageEditor({
-          eventBus: this.eventBus,
-          contentContainer: this,
-          container: this.contentContainer,
-          pagePtr: this.pagePtr,
+        const editAreaPtr = await this.messageHandler.sendWithPromise('GetEditArea', {
           editPagePtr: this.editPagePtr,
-          editAreaPtr,
-          editAreaIndex: i,
-          viewport: this.viewport,
-          scale: this.scale,
-          messageHandler: this.messageHandler,
-          pageViewer: this.pageViewer,
-          $t: this.$t,
-          hidden: this.tool === 'addText' || this.tool === 'editText'
+          index: i
         })
-        this.frameEditorList.push(frameEditor)
+        if (!editAreaPtr) continue
+
+        const isTextPtr = await this.messageHandler.sendWithPromise('IsTextArea', editAreaPtr)
+        if (isTextPtr) {
+          const frameEditor = new TextEditor({
+            eventBus: this.eventBus,
+            contentContainer: this,
+            container: this.contentContainer,
+            pagePtr: this.pagePtr,
+            editPagePtr: this.editPagePtr,
+            editAreaPtr,
+            editAreaIndex: i,
+            viewport: this.pageViewer.viewport,
+            scale: this.pageViewer.scale,
+            messageHandler: this.messageHandler,
+            pageViewer: this.pageViewer,
+            hidden: this.tool === 'addImage' || this.tool === 'editImage'
+          })
+          this.frameEditorList.push(frameEditor)
+          continue
+        }
+
+        const isImagePtr = await this.messageHandler.sendWithPromise('IsImageArea', editAreaPtr)
+        if (isImagePtr) {
+          const frameEditor = new ImageEditor({
+            eventBus: this.eventBus,
+            contentContainer: this,
+            container: this.contentContainer,
+            pagePtr: this.pagePtr,
+            editPagePtr: this.editPagePtr,
+            editAreaPtr,
+            editAreaIndex: i,
+            viewport: this.pageViewer.viewport,
+            scale: this.pageViewer.scale,
+            messageHandler: this.messageHandler,
+            pageViewer: this.pageViewer,
+            $t: this.$t,
+            hidden: this.tool === 'addText' || this.tool === 'editText'
+          })
+          this.frameEditorList.push(frameEditor)
+        }
       }
+    } finally {
+      this.isRendering = false
     }
 
     if (this.tool === 'addText') {
@@ -142,10 +160,14 @@ export class ContentContainer {
     this.contentContainer.style.display = 'none'
     this.resetTools()
   }
-  
+
   cancel () {
-    this._cancelled = true
-    this.destroy()
+    if (this.destroyed || !this.rendered) return
+
+    this.contentContainer?.remove()
+    this.contentContainer = null
+
+    this.resetTools()
   }
 
   destroy () {
@@ -212,8 +234,9 @@ export class ContentContainer {
         tool: this.tool,
         color: this.color,
         container: this.pageDiv,
-        viewport: this.viewport,
-        scale: this.scale,
+        pageViewer: this.pageViewer,
+        viewport: this.pageViewer.viewport,
+        scale: this.pageViewer.scale,
         page: this.page,
         eventBus: this.eventBus,
         contentContainer: this
@@ -222,13 +245,16 @@ export class ContentContainer {
   }
 
   async addImageManager (imageData) {
-    if (!this.imageManager) {
+    if (this.imageManager) {
+      this.imageManager.init()
+    } else {
       this.imageManager = new AddImage({
         tool: this.tool,
         color: this.color,
         container: this.pageDiv,
-        viewport: this.viewport,
-        scale: this.scale,
+        pageViewer: this.pageViewer,
+        viewport: this.pageViewer.viewport,
+        scale: this.pageViewer.scale,
         page: this.page,
         eventBus: this.eventBus,
         contentContainer: this,
@@ -331,8 +357,8 @@ export class ContentContainer {
       editPagePtr: this.editPagePtr,
       editAreaPtr,
       editAreaIndex: this.frameEditorList.length,
-      viewport: this.viewport,
-      scale: this.scale,
+      viewport: this.pageViewer.viewport,
+      scale: this.pageViewer.scale,
       messageHandler: this.messageHandler,
       newAdd: true,
       pageViewer: this.pageViewer
@@ -360,8 +386,8 @@ export class ContentContainer {
       editPagePtr: this.editPagePtr,
       editAreaPtr,
       editAreaIndex: this.frameEditorList.length,
-      viewport: this.viewport,
-      scale: this.scale,
+      viewport: this.pageViewer.viewport,
+      scale: this.pageViewer.scale,
       messageHandler: this.messageHandler,
       newAdd: true,
       pageViewer: this.pageViewer,
@@ -403,4 +429,98 @@ export class ContentContainer {
       selectedEditor.remove()
     }
   }
+
+  // undo/redo
+  async handleResetOperate(op) {
+    if (op === 'undo') {
+      const res = await this.historyManager.undo()
+      // await this.updateFrameEditor(res)
+
+    } else if (op === 'redo') {
+      const res = await this.historyManager.redo()
+      // await this.updateFrameEditor(res)
+    }
+  }
+
+  // 修改后添加到undo列表
+  async handleOperateList(data) {
+    const canUndo = await this.historyManager.canUndo(data)
+    if (!canUndo) return
+    this.eventBus.dispatch('undoRedoStatusChanged')
+    this.historyManager.undoList.push(data)
+    this.historyManager.redoList.length = 0
+
+    this.eventBus.dispatch('changeOperateList', {
+      undoListLength: this.historyManager.undoList.length,
+      redoListLength: this.historyManager.redoList.length
+    })
+    this.eventBus.dispatch('isContentEditModyfied', true)
+  }
+
+  // undo/redo后,更新视图
+  async updateFrameEditor (res) {
+    const { editAreaPtr, editCharPlace } = res
+
+    const editor = this.frameEditorList.find(item => item.editAreaPtr === editAreaPtr)
+    if (editor) {
+      editor.updateCanvasAfterUndoRedo()
+      editor.type === 'text' && (editCharPlace.CharIndex !== -1 || editCharPlace.LineIndex !== -1 || editCharPlace.RunIndex !== -1 || editCharPlace.SectionIndex !== -1 ) && editor.updateCursorLine(editCharPlace)
+      return
+    }
+
+    const newFrameEditorList = []
+    const editAreaCount = await this.messageHandler.sendWithPromise('GetEditAreaCount', this.editPagePtr)
+
+    for (let i = 0; i < editAreaCount; i++) {
+      const editAreaPtr = await this.messageHandler.sendWithPromise('GetEditArea', {
+        editPagePtr: this.editPagePtr,
+        index: i
+      })
+      if (!editAreaPtr) continue
+
+      let frameEditor
+      if (await this.messageHandler.sendWithPromise('IsTextArea', editAreaPtr)) {
+        frameEditor = new TextEditor({
+          eventBus: this.eventBus,
+          contentContainer: this,
+          container: this.contentContainer,
+          pagePtr: this.pagePtr,
+          editPagePtr: this.editPagePtr,
+          editAreaPtr,
+          editAreaIndex: i,
+          viewport: this.pageViewer.viewport,
+          scale: this.pageViewer.scale,
+          messageHandler: this.messageHandler,
+          pageViewer: this.pageViewer,
+          hidden: this.tool === 'addImage',
+          isUpdate: true
+        })
+      } else if (await this.messageHandler.sendWithPromise('IsImageArea', editAreaPtr)) {
+        frameEditor = new ImageEditor({
+          eventBus: this.eventBus,
+          contentContainer: this,
+          container: this.contentContainer,
+          pagePtr: this.pagePtr,
+          editPagePtr: this.editPagePtr,
+          editAreaPtr,
+          editAreaIndex: i,
+          viewport: this.pageViewer.viewport,
+          scale: this.pageViewer.scale,
+          messageHandler: this.messageHandler,
+          pageViewer: this.pageViewer,
+          $t: this.$t,
+          hidden: this.tool === 'addText',
+          isUpdate: true
+        })
+      }
+      frameEditor && newFrameEditorList.push(frameEditor)
+    }
+
+    this.frameEditorList.forEach(async editor => {
+      await editor.updateCanvas(null, true)
+      editor.removeViewer()
+    })
+    this.frameEditorList.length = 0
+    this.frameEditorList = newFrameEditorList
+  }
 }

+ 74 - 0
packages/core/src/editor/content_edit_history_manager.js

@@ -0,0 +1,74 @@
+export class ContentEditHistoryManager {
+  constructor(pdfViewer) {
+    this.messageHandler = pdfViewer.messageHandler
+    this.eventBus = pdfViewer.eventBus
+    this.pdfViewer = pdfViewer
+
+    this.undoList = []
+    this.redoList = []
+
+    this.eventBus._on('toolModeChanged', this.handleToolMode.bind(this))
+    this.eventBus._on('toolChanged', this.handleToolMode.bind(this))
+  }
+
+  async canRedo(data) {
+    if (!data) return this.redoList.length > 0
+    
+    if (!data.editPagePtr) return false
+    const res = await this.messageHandler.sendWithPromise('CanRedo', data.editPagePtr)
+    return !!res
+  }
+
+  async canUndo(data) {
+    if (!data) return this.undoList.length > 0
+    
+    if (!data.editPagePtr) return false
+    const res = await this.messageHandler.sendWithPromise('CanUndo', data.editPagePtr)
+    return !!res
+  }
+
+  async redo() {
+    const lastRedo = this.redoList.pop()
+    if (!lastRedo) {
+      console.warn('No actions to redo')
+      return false
+    }
+    const res = await this.messageHandler.sendWithPromise('Redo', lastRedo.editPagePtr)
+
+    this.undoList.push(lastRedo)
+    this.updateView(lastRedo.pageIndex, res)
+
+    this.eventBus.dispatch('undoRedoStatusChanged')
+  }
+
+  async undo() {
+    const lastUndo = this.undoList.pop()
+    if (!lastUndo) {
+      console.warn('No actions to undo')
+      return false
+    }
+    const res = await this.messageHandler.sendWithPromise('Undo', lastUndo.editPagePtr)
+    const canRedo = await this.canRedo(lastUndo)
+
+    canRedo && this.redoList.push(lastUndo)
+    this.updateView(lastUndo.pageIndex, res)
+
+    this.eventBus.dispatch('undoRedoStatusChanged')
+  }
+
+  async updateView(pageIndex, res) {
+    await this.pdfViewer._pages[pageIndex].contentContainer.updateFrameEditor(res)
+
+    this.eventBus.dispatch('changeOperateList', {
+      undoListLength: this.undoList.length,
+      redoListLength: this.redoList.length
+    })
+  }
+
+  handleToolMode(mode) {
+    if ((mode !== 'editor' || ['editText', 'editImage', 'addText', 'addImage'].includes(mode)) && (this.undoList.length || this.redoList.length)) {
+      this.undoList.length = 0
+      this.redoList.length = 0
+    }
+  }
+}

+ 13 - 0
packages/core/src/editor/content_edit_manager.js

@@ -0,0 +1,13 @@
+export class ContentEditManager {
+  constructor(documentViewer) {
+    this.documentViewer = documentViewer
+  }
+
+  async startContentEditMode() {
+    this.documentViewer.setToolMode('editor')
+  }
+
+  async endContentEditMode() {
+    this.documentViewer.setToolMode('view')
+  }
+}

+ 172 - 26
packages/core/src/editor/image_editor.js

@@ -16,7 +16,8 @@ export class ImageEditor {
     newAdd,
     pageViewer,
     $t,
-    hidden
+    hidden,
+    isUpdate = false
   }) {
     this.eventBus = eventBus
     this.contentContainer = contentContainer
@@ -32,11 +33,11 @@ export class ImageEditor {
     this.pageViewer = pageViewer
     this.$t = $t
     this.hidden = hidden
+    this.isUpdate = isUpdate
 
     this.type = 'image'
     this.deleteSvgStr = `<rect width="30" height="30" rx="2" fill="#DDE9FF"/>
-      <path fill-rule="evenodd" clip-rule="evenodd" d="M19 6.5V9.5H24V10.5H21.5V23.5H8.5V10.5H6V9.5H11V6.5H19ZM9.5 10.5V22.5H20.5V10.5H9.5ZM18 7.5V9.5H12V7.5H18ZM13.5 13V20H12.5V13H13.5ZM17.5 20V13H16.5V20H17.5Z" fill="#333333"/>
-    `
+      <path fill-rule="evenodd" clip-rule="evenodd" d="M19 6.5V9.5H24V10.5H21.5V23.5H8.5V10.5H6V9.5H11V6.5H19ZM9.5 10.5V22.5H20.5V10.5H9.5ZM18 7.5V9.5H12V7.5H18ZM13.5 13V20H12.5V13H13.5ZM17.5 20V13H16.5V20H17.5Z" fill="#333333"/>`
     this.frame = null
     this.borderWidth = 2
     this.pointWidth = 6
@@ -53,11 +54,13 @@ export class ImageEditor {
     this.imageUrl = null
     this.cropped = false
     this.removed = false
+    this.listener = null
 
     this.start = null
     this.end = null
     this.newStart = null
     this.newEnd = null
+    this.needUndoRec = false // 执行操作后,需要添加撤销记录
 
     this.mousedown = isMobileDevice ? 'touchstart' : 'mousedown'
     this.mouseup = isMobileDevice ? 'touchend' : 'mouseup'
@@ -77,7 +80,12 @@ export class ImageEditor {
     this.eventBus._on('imagePropertyChanged', this.onHandlePropertyPanelChanged)
     this.eventBus._on('showContentEditorType', this.onShowContentEditorType)
 
-    await this.updateCanvas()
+    if (this.isUpdate) {
+      await this.updateCanvas()
+    } else {
+      await this.getRect()
+    }
+  
     this.opacity = await this.messageHandler.sendWithPromise('GetImageTransparency', this.editAreaPtr)
 
     let frameContainer = createElement(
@@ -306,6 +314,8 @@ export class ImageEditor {
 
     if (this.newAdd) {
       this.goEditing()
+      this.addUndoHistory()
+      this.newAdd = false
     }
   }
 
@@ -386,6 +396,7 @@ export class ImageEditor {
   }
 
   handleMouseDown (e) {
+    e.preventDefault()
     if (e.button === 2 || this.hidden) return // 右键点击不执行
 
     if (this.state > 0) {
@@ -411,14 +422,17 @@ export class ImageEditor {
   async handleMouseUp (e) {
     if (e && !this.frameContainer.contains(e.target) && this.state === 0) return
     if (this.contentContainer.selectedFrameIndex !== -1 && this.contentContainer.selectedFrameIndex !== this.editAreaIndex && this.state === 0) return
+    
+    document.removeEventListener(this.mousemove, this.onMousemove)
+    document.removeEventListener(this.mouseup, this.onMouseup)
+    
     if (!this.mouseDown) return
-
     this.mouseDown = false
     this.moving = false
     this.frameContainer.classList.add('selected')
 
     if (e.type === 'touchend') {
-      document.body.style.overscrollBehavior = 'auto';
+      document.querySelector('.document-container').style.overflow = 'auto'
     }
 
     if (this.state === 0 && !this.imageUrl) this.updateOriginalImageUrl()
@@ -429,6 +443,10 @@ export class ImageEditor {
       this.contentContainer.selectedFrameIndex = this.editAreaIndex
       this.container.append(this.outerLineContainer)
       this.eventBus.dispatch('changeRightPanelBtnDisabled', false)
+      this.eventBus.dispatch('contentBoxSelected', {
+        type: 'image',
+        pageNumber: this.pageViewer.pageIndex + 1,
+      })
     }
 
     if (this.mouseMoved) {
@@ -466,13 +484,23 @@ export class ImageEditor {
         rect
       })
 
+      this.needUndoRec = true
       this.updateCanvas({oldPoint, newPoint})
     }
 
-    onClickOutsideUp([this.imageContainer, this.outerLine, this.deletetButton, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
-    
-    document.removeEventListener(this.mousemove, this.onMousemove)
-    document.removeEventListener(this.mouseup, this.onMouseup)
+    const elements = [this.imageContainer, this.outerLine, this.deletetButton]
+
+    const editorPanel = document.querySelector('.editor-panel')
+    editorPanel && elements.push(editorPanel)
+    const propertyPanelButton = document.getElementById('propertyPanelButton')
+    propertyPanelButton && elements.push(propertyPanelButton)
+    const undo = document.getElementById('undo')
+    undo && elements.push(undo)
+    const redo = document.getElementById('redo')
+    redo && elements.push(redo)
+
+    this.listener && document.removeEventListener(isMobileDevice ? 'touchend' : 'mouseup', this.listener)
+    this.listener = onClickOutsideUp(elements, this.handleOutside.bind(this))
   }
 
   async handleMouseMove (e) {
@@ -481,7 +509,7 @@ export class ImageEditor {
     this.moving = true
     
     if (e.type === 'touchmove') {
-      document.body.style.overscrollBehavior = 'none';
+      document.querySelector('.document-container').style.overflow = 'hidden'
     }
 
     const { pageX, pageY } = getClickPoint(e)
@@ -664,7 +692,21 @@ export class ImageEditor {
   }
 
   async handleOutside () {
-    if (this.moving) return
+    if (this.moving) {
+      const elements = [this.imageContainer, this.outerLine, this.deletetButton]
+
+      const editorPanel = document.querySelector('.editor-panel')
+      editorPanel && elements.push(editorPanel)
+      const propertyPanelButton = document.getElementById('propertyPanelButton')
+      propertyPanelButton && elements.push(propertyPanelButton)
+      const undo = document.getElementById('undo')
+      undo && elements.push(undo)
+      const redo = document.getElementById('redo')
+      redo && elements.push(redo)
+      
+      onClickOutsideUp(elements, this.handleOutside.bind(this))
+      return
+    }
 
     this.state = 0
     this.imageUrl = null
@@ -677,8 +719,8 @@ export class ImageEditor {
       end: this.end
     })
 
-    this.frameContainer.classList.remove('editing')
-    this.frameContainer.classList.remove('selected')
+    this.frameContainer?.classList.remove('editing')
+    this.frameContainer?.classList.remove('selected')
     this.outerLineContainer.remove()
     this.outerLineContainer.append(this.deletetButton)
 
@@ -687,6 +729,11 @@ export class ImageEditor {
       if (!hasItem) {
         this.eventBus.dispatch('contentPropertyChange', { type: 'image', isOpen: false })
         this.eventBus.dispatch('changeRightPanelBtnDisabled', true)
+
+        this.eventBus.dispatch('contentBoxDeselected', {
+          type: 'image',
+          pageNumber: this.pageViewer.pageIndex + 1,
+        })
       }
     }, 1)
   }
@@ -714,6 +761,7 @@ export class ImageEditor {
     this.start = start
     this.end = end
 
+    this.oldRect = this.rect || null
     const rect = this.rectCalc(start, end)
     this.rect = rect
     this.imageRatio = this.rect.height / this.rect.width
@@ -776,7 +824,7 @@ export class ImageEditor {
       this.imgRect = imgRect
 
     } else if (oldRect) {
-      const wholeAreaRect = this.getEntireArea(oldRect, this.rect)
+      const wholeAreaRect = this.getEntireArea(this.oldRect, this.rect)
 
       const imgRect = {
         left: parseInt(wholeAreaRect.left * this.ratio),
@@ -830,6 +878,10 @@ export class ImageEditor {
   // 保存编辑
   async saveEdit () {
     await this.messageHandler.sendWithPromise('EndEdit', this.editPagePtr)
+    if (this.needUndoRec) {
+      this.addUndoHistory()
+      this.needUndoRec = false
+    }
   }
 
   // 更新outerline框的位置大小
@@ -971,8 +1023,7 @@ export class ImageEditor {
       }
     }
 
-    const oldRect = this.rect
-    await this.updateCanvas(null, oldRect)
+    await this.updateCanvas(null, true)
   }
 
   // 属性面板 修改属性
@@ -985,6 +1036,7 @@ export class ImageEditor {
           editAreaPtr: this.editAreaPtr,
           angle: props[item]
         })
+        this.needUndoRec = true
       }
 
       if (item === 'flip') {
@@ -993,14 +1045,21 @@ export class ImageEditor {
         } else if (props[item] === 'y') {
           await this.messageHandler.sendWithPromise('VerticalMirrorImage', this.editAreaPtr)
         }
+        this.needUndoRec = true
       }
 
-      if (item === 'opacity' && this.opacity !== props.opacity / 100) {
+      if (item === 'opacity') {
+        if (this.opacity * 100 === props.opacity) return
+
+        const opacity = await this.messageHandler.sendWithPromise('GetImageTransparency', this.editAreaPtr)
+        if (opacity * 100 === props.opacity) return
+
         await this.messageHandler.sendWithPromise('SetImageTransparency', {
           editAreaPtr: this.editAreaPtr,
           opacity: props.opacity / 100
         })
         this.opacity = props.opacity / 100
+        this.needUndoRec = true
       }
 
       if (item === 'tool' && props.tool === 'replace') {
@@ -1053,6 +1112,7 @@ export class ImageEditor {
           this.opacity = await this.messageHandler.sendWithPromise('GetImageTransparency', this.editAreaPtr)
           this.eventBus.dispatch('contentPropertyChange', { type: 'image', opacity: this.opacity * 100 })
 
+          this.needUndoRec = true
           this.saveEdit()
         })
         .catch((error) => {
@@ -1076,8 +1136,7 @@ export class ImageEditor {
       }
     }
 
-    const oldRect = this.rect
-    await this.updateCanvas(null, oldRect)
+    await this.updateCanvas(null, true)
     this.updateOriginalImageUrl()
   }
 
@@ -1125,14 +1184,28 @@ export class ImageEditor {
     this.container.append(this.outerLineContainer)
     if (!this.imageUrl) this.updateOriginalImageUrl()
 
-    onClickOutsideUp([this.imageContainer, this.outerLine, this.deletetButton, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
+    const elements = [this.imageContainer, this.outerLine, this.deletetButton]
+
+    const editorPanel = document.querySelector('.editor-panel')
+    editorPanel && elements.push(editorPanel)
+    const propertyPanelButton = document.getElementById('propertyPanelButton')
+    propertyPanelButton && elements.push(propertyPanelButton)
+    const undo = document.getElementById('undo')
+    undo && elements.push(undo)
+    const redo = document.getElementById('redo')
+    redo && elements.push(redo)
+    
+    onClickOutsideUp(elements, this.handleOutside.bind(this))
   }
 
   // 删除区域
   async handleDelete () {
+    await this.handleOutside()
+    
     this.removed = true
     this.outerLineContainer.remove()
     this.frameContainer.remove()
+    this.frameContainer = null
     this.contentContainer.selectedFrameIndex = -1
 
     await this.messageHandler.send('RemoveEditArea', {
@@ -1140,9 +1213,9 @@ export class ImageEditor {
       editAreaPtr: this.editAreaPtr
     })
     
-    this.contentContainer.removeEditor(this.editAreaIndex)
-    
+    this.needUndoRec = true
     this.updateCanvas()
+    this.contentContainer.removeEditor(this.editAreaIndex)
     this.eventBus.dispatch('contentPropertyChange', { type: 'image', isOpen: false })
   }
 
@@ -1150,6 +1223,18 @@ export class ImageEditor {
     this.handleDelete()
   }
 
+  removeViewer () {
+    this.state = 0
+    // await this.handleOutside()
+    
+    this.removed = true
+    this.outerLineContainer.remove()
+    this.frameContainer.remove()
+    this.frameContainer = null
+    this.contentContainer.selectedFrameIndex = -1
+    this.eventBus.dispatch('contentPropertyChange', { type: 'image', isOpen: false })
+  }
+
   // 上传图片并获取宽高
   uploadFile () {
     return new Promise((resolve, reject) => {
@@ -1160,7 +1245,7 @@ export class ImageEditor {
       fileInput.onchange = () => {
         const file = fileInput.files[0]
 
-        if (file.size > 10 * 1024 * 1024) {
+        if (file.size > 2 * 1024 * 1024) {
           reject(this.$t('editorPanel.maximum'))
           fileInput.remove()
           window.$message.error(this.$t('editorPanel.maximum'), {
@@ -1209,7 +1294,7 @@ export class ImageEditor {
     })
 
     this.imageUrl = this.imageArrayToUrl(imageArray, width, height)
-    this.eventBus.dispatch('contentPropertyChange', { type: 'image', imageUrl: this.imageUrl })
+    this.eventBus.dispatch('contentPropertyChange', { type: 'image', imageUrl: this.imageUrl, opacity: this.opacity * 100 })
   }
 
   // 更新点的位置
@@ -1403,9 +1488,70 @@ export class ImageEditor {
       if (this.state) {
         this.handleOutside()
       }
-      } else {
+    } else {
       this.hidden = false
       this.frameContainer && (this.frameContainer.style.display = 'block')
     }
   }
+
+  // 添加undo记录
+  addUndoHistory () {
+    this.contentContainer.handleOperateList({
+      editPagePtr: this.editPagePtr,
+      editAreaIndex: this.editAreaIndex,
+      editAreaPtr: this.editAreaPtr,
+      pageIndex: this.pageViewer.pageIndex
+    })
+  }
+
+  // undo/redo后 更新canvas图
+  async updateCanvasAfterUndoRedo() {
+    await this.getRect()
+
+    if (this.frameContainer && this.outerLineContainer) {
+      this.updateOutline({
+        start: this.start,
+        end: this.end
+      })
+
+      setCss(this.frameContainer, {
+        left: this.rect.left + 'px',
+        top: this.rect.top + 'px',
+        width: this.rect.width + 'px',
+        height: this.rect.height + 'px'
+      })
+    }
+
+    const wholeAreaRect = this.getEntireArea(this.oldRect, this.rect)
+
+    const imgRect = {
+      left: parseInt(wholeAreaRect.left * this.ratio),
+      top: parseInt(wholeAreaRect.top * this.ratio),
+      right: parseInt(wholeAreaRect.right * this.ratio),
+      bottom: parseInt(wholeAreaRect.bottom * this.ratio),
+      width: parseInt(wholeAreaRect.right * this.ratio) - parseInt(wholeAreaRect.left * this.ratio),
+      height: parseInt(wholeAreaRect.bottom * this.ratio) - parseInt(wholeAreaRect.top * this.ratio),
+    }
+    this.imgRect = imgRect
+
+    let { imageArray } = await this.messageHandler.sendWithPromise('PushRenderTask', {
+      pagePtr: this.pagePtr,
+      scale: this.scale * this.ratio,
+      left: this.imgRect.left,
+      right: this.imgRect.right,
+      bottom: this.imgRect.bottom,
+      top: this.imgRect.top
+    })
+    this.imageArray = imageArray
+
+    this.drawCanvas()
+    this.saveEdit()
+
+    if (this.contentContainer.selectedFrameIndex === this.editAreaIndex) {
+      this.updateOriginalImageUrl()
+      const opacity = await this.messageHandler.sendWithPromise('GetImageTransparency', this.editAreaPtr)
+      this.opacity = Math.round(opacity * 100) / 100
+      this.eventBus.dispatch('contentPropertyChange', { type: 'image', opacity: this.opacity * 100 })
+    }
+  }
 }

+ 350 - 171
packages/core/src/editor/text_editor.js

@@ -3,6 +3,7 @@ import { onClickOutsideUp, setCss, isMobileDevice } from '../ui_utils';
 import copy from 'copy-to-clipboard';
 
 export class TextEditor {
+  #editList = []
   constructor({
     eventBus,
     contentContainer,
@@ -16,7 +17,8 @@ export class TextEditor {
     messageHandler,
     newAdd,
     pageViewer,
-    hidden
+    hidden,
+    isUpdate
   }) {
     this.eventBus = eventBus
     this.contentContainer = contentContainer
@@ -31,9 +33,9 @@ export class TextEditor {
     this.newAdd = newAdd
     this.pageViewer = pageViewer
     this.hidden = hidden
-    
-    this.type = 'text'
+    this.isUpdate = isUpdate
 
+    this.type = 'text'
     this.frame = null
     this.canvas = null
     this.borderWidth = 2
@@ -47,6 +49,7 @@ export class TextEditor {
     }
     this.selectedRects = null
     this.selectedCharRange = null
+    this.entireCharRange = null
     this.state = 0 // 0 未选中;1 已选中;2 编辑状态
     this.mouseDown = false
     this.moving = false
@@ -62,6 +65,7 @@ export class TextEditor {
     this.startPoint = null
     this.endPoint = null
     this.composing = false // 组合输入状态
+    this.needUndoRec = false // 执行操作后,需要添加撤销记录
 
     this.isFirefox = navigator.userAgent.indexOf('Firefox') > -1
     this.mousedown = isMobileDevice ? 'touchstart' : 'mousedown'
@@ -80,6 +84,7 @@ export class TextEditor {
     this.onCompositionstart = this.handleCompositionstart.bind(this)
     this.onCompositionend = this.handleCompositionend.bind(this)
     this.onShowContentEditorType = this.handleShow.bind(this)
+    this.onHandlePopup = this.handlePopup.bind(this)
 
     this.render()
   }
@@ -87,6 +92,7 @@ export class TextEditor {
   async render () {
     this.eventBus._on('textPropertyChanged', this.onHandlePropertyPanelChanged)
     this.eventBus._on('showContentEditorType', this.onShowContentEditorType)
+    this.eventBus._on('contentEditorPopupClicked', this.onHandlePopup)
 
     await this.getRect()
     this.canvas = document.createElement('canvas')
@@ -137,20 +143,25 @@ export class TextEditor {
         height: '100%',
         resize: 'none',
         opacity: 0,
-        touchAction: 'none',
-        zIndex: 1
+        touchAction: 'none'
       }
     )
+    textarea.name = 'text-editor'
     this.textContainer.append(textarea)
     this.textarea = textarea
     let text = await this.getText()
-    this.textarea.innerHTML = text
+    this.textarea.value = text
 
-    this.frameContainer.append(this.textContainer)
+    this.frameContainer?.append(this.textContainer)
 
     // this.textContainer.addEventListener('click', this.onClick)
     this.textContainer.addEventListener(this.mousedown, this.onMousedown)
     this.textContainer.addEventListener(this.mouseup, this.onMouseup)
+    isMobileDevice && this.textarea.addEventListener('contextmenu', (event) => event.preventDefault())
+
+    this.outerLineContainer = document.createElement('div')
+    this.outerLineContainer.className = 'outline-container'
+    this.outerLineContainer.style.position = 'absolute'
 
     const outerLine = createSvg('svg', {
       class: 'outerline'
@@ -301,13 +312,18 @@ export class TextEditor {
     this.outerLine.append(this.rightRect)
     this.outerLine.append(this.topRightRect)
     this.outerLine.append(this.topRect)
+    this.outerLineContainer.append(this.outerLine)
 
     this.outerLine.addEventListener(this.mousedown, this.onMousedown)
     this.outerLine.addEventListener(this.mouseup, this.onMouseup)
 
     if (this.newAdd) {
       this.goEditing()
+      this.addUndoHistory()
+      this.newAdd = false
     }
+    
+    this.isUpdate && await this.updateCanvas()
   }
 
   getActualRect (viewport, s, frame) {
@@ -387,6 +403,7 @@ export class TextEditor {
   }
 
   handleMouseDown (e) {
+    isMobileDevice && e.preventDefault()
     if (e.button === 2 || this.hidden) return // 右键点击不执行
 
     if (this.state === 1) {
@@ -460,9 +477,10 @@ export class TextEditor {
     if (!this.mouseDown) return
     this.mouseDown = false
     this.moving = false
+    this.textContainer.removeEventListener(this.mousemove, this.onMousemove)
 
     if (e.type === 'touchend') {
-      document.body.style.overscrollBehavior = 'auto';
+      document.querySelector('.document-container').style.overflow = 'auto'
     }
 
     let flag = true
@@ -474,6 +492,10 @@ export class TextEditor {
 
       this.contentContainer.selectedFrameIndex = this.editAreaIndex
       this.eventBus.dispatch('changeRightPanelBtnDisabled', false)
+      this.eventBus.dispatch('contentBoxSelected', {
+        type: 'text',
+        pageNumber: this.pageViewer.pageIndex + 1,
+      })
     }
 
     if (this.state === 1) {
@@ -494,7 +516,7 @@ export class TextEditor {
         this.end = this.newEnd
       }
       
-      this.container.append(this.outerLine)
+      this.container.append(this.outerLineContainer)
 
       if (this.mouseMoved) {
         const { start, end } = this.getInitialPoint(this.start, this.end)
@@ -510,7 +532,8 @@ export class TextEditor {
           editAreaPtr: this.editAreaPtr,
           rect
         })
-        this.updateCanvas({oldPoint, newPoint})
+        this.needUndoRec = true
+        await this.updateCanvas({oldPoint, newPoint})
 
       } else if (!this.mouseMoved && flag) {
         this.state = 2
@@ -518,14 +541,17 @@ export class TextEditor {
           this.eventBus.dispatch('contentPropertyChange', { type: 'text', isOpen: true })
         }
       }
+
+      this.showPopup()
     }
     
     if (this.state === 2) {
-      this.outerLine.remove()
+      this.outerLineContainer.remove()
       this.frameContainer.classList.add('editing')
 
       let endPoint
       if (isMobileDevice) {
+        this.textarea.focus()
         const offsetX = e.changedTouches[0].clientX - this.pageViewer.div.getBoundingClientRect().left
         const offsetY = e.changedTouches[0].clientY - this.pageViewer.div.getBoundingClientRect().top
 
@@ -545,13 +571,13 @@ export class TextEditor {
       
       if (this.mouseMoved) {
         endPoint = this.endPoint
-        this.cursor.style.display = 'block'
       }
       if (!this.startPoint) {
         this.startPoint = endPoint
       }
       const { start, end } = await this.getCharsRange(this.startPoint, endPoint)
       this.activeCharPlace = end
+      this.getTextStyle()
 
       if (!this.cursor) {
         const cursor = createSvg(
@@ -569,6 +595,8 @@ export class TextEditor {
         )
         this.cursor = cursor
         this.textContainer.append(this.cursor)
+      } else {
+        this.cursor.style.display = 'block'
       }
       this.updateCursorLine()
 
@@ -581,24 +609,36 @@ export class TextEditor {
         this.clearSelectText()
       }
 
-      if (!this.selectedRects) {
-        this.getTextStyle()
-      }
-
       this.textarea.focus()
       this.textarea.addEventListener('blur', this.onBlur)
       this.textarea.addEventListener('keydown', this.onKeydown)
       this.textarea.addEventListener(this.textInput, this.onTextInput)
-      if (this.isFirefox) {
+      if (this.isFirefox || isMobileDevice) {
         this.textarea.addEventListener('compositionstart', this.onCompositionstart)
         this.textarea.addEventListener('compositionend', this.onCompositionend)
       }
+      onClickOutsideUp([this.textContainer], this.handleTextOutside.bind(this))
+
+      this.hidePopup()
     }
 
     this.frameContainer.classList.add('selected')
 
     if (this.state === 1 && !this.mouseMoved) {
-      onClickOutsideUp([this.textContainer, this.outerLine, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
+      const elements = [this.textContainer, this.outerLine]
+
+      const editorPanel = document.querySelector('.editor-panel')
+      editorPanel && elements.push(editorPanel)
+      const propertyPanelButton = document.getElementById('propertyPanelButton')
+      propertyPanelButton && elements.push(propertyPanelButton)
+      const undo = document.getElementById('undo')
+      undo && elements.push(undo)
+      const redo = document.getElementById('redo')
+      redo && elements.push(redo)
+      const contentEditorPopup = document.querySelector('.content-editor-popup')
+      contentEditorPopup && elements.push(contentEditorPopup)
+      
+      onClickOutsideUp(elements, this.handleOutside.bind(this))
     }
   }
 
@@ -606,9 +646,10 @@ export class TextEditor {
     if (this.contentContainer.selectedFrameIndex !== -1 && this.contentContainer.selectedFrameIndex !== this.editAreaIndex && this.state === 0) return
 
     this.moving = true
+    this.hidePopup()
     
     if (e.type === 'touchmove') {
-      document.body.style.overscrollBehavior = 'none';
+      document.querySelector('.document-container').style.overflow = 'hidden'
     }
 
     if (this.state === 1) {
@@ -779,23 +820,40 @@ export class TextEditor {
     // console.log('blur')
   }
 
+  handleTextOutside () {
+    this.cursor && (this.cursor.style.display = 'none')
+  }
+
   async handleOutside () {
     if (this.moving) {
-      onClickOutsideUp([this.textContainer, this.outerLine, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
+      const elements = [this.textContainer, this.outerLine]
+
+      const editorPanel = document.querySelector('.editor-panel')
+      editorPanel && elements.push(editorPanel)
+      const propertyPanelButton = document.getElementById('propertyPanelButton')
+      propertyPanelButton && elements.push(propertyPanelButton)
+      const undo = document.getElementById('undo')
+      undo && elements.push(undo)
+      const redo = document.getElementById('redo')
+      redo && elements.push(redo)
+      const contentEditorPopup = document.querySelector('.content-editor-popup')
+      contentEditorPopup && elements.push(contentEditorPopup)
+      
+      onClickOutsideUp(elements, this.handleOutside.bind(this))
       return
     }
     
     this.state = this.state === 2 ? 1 : this.state === 1 ? 0 : this.state
 
-    this.frameContainer.classList.remove('editing')
+    this.frameContainer?.classList.remove('editing')
 
     this.clearSelectText()
     this.cursor?.remove()
     this.cursor = null
 
     if (this.state === 0) {
-      this.frameContainer.classList.remove('selected')
-      this.outerLine.remove()
+      this.frameContainer?.classList.remove('selected')
+      this.outerLineContainer?.remove()
 
       if (!this.removed) this.contentContainer.selectedFrameIndex = -1
 
@@ -804,8 +862,15 @@ export class TextEditor {
         if (!hasItem) {
           this.eventBus.dispatch('contentPropertyChange', { type: 'text', isOpen: false })
           this.eventBus.dispatch('changeRightPanelBtnDisabled', true)
+
+          this.eventBus.dispatch('contentBoxDeselected', {
+            type: 'text',
+            pageNumber: this.pageViewer.pageIndex + 1,
+          })
         }
       }, 1)
+      
+      this.hidePopup()
     }
 
     if (this.state === 1) {
@@ -818,15 +883,31 @@ export class TextEditor {
       this.textContainer.removeEventListener(this.mousemove, this.onMousemove)
 
       if (!this.hidden) {
-        this.container.append(this.outerLine)
-        onClickOutsideUp([this.textContainer, this.outerLine, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
+        this.container.append(this.outerLineContainer)
+
+        const elements = [this.textContainer, this.outerLine]
+
+        const editorPanel = document.querySelector('.editor-panel')
+        editorPanel && elements.push(editorPanel)
+        const propertyPanelButton = document.getElementById('propertyPanelButton')
+        propertyPanelButton && elements.push(propertyPanelButton)
+        const undo = document.getElementById('undo')
+        undo && elements.push(undo)
+        const redo = document.getElementById('redo')
+        redo && elements.push(redo)
+        const contentEditorPopup = document.querySelector('.content-editor-popup')
+        contentEditorPopup && elements.push(contentEditorPopup)
+        
+        onClickOutsideUp(elements, this.handleOutside.bind(this))
+
+        this.showPopup()
       }
     }
 
     this.textarea.removeEventListener('blur', this.onBlur)
     this.textarea.removeEventListener('keydown', this.onKeydown)
     this.textarea.removeEventListener(this.textInput, this.onTextInput)
-    if (this.isFirefox) {
+    if (this.isFirefox || isMobileDevice) {
       this.textarea.removeEventListener('compositionstart', this.onCompositionstart)
       this.textarea.removeEventListener('compositionend', this.onCompositionend)
     }
@@ -840,7 +921,7 @@ export class TextEditor {
 
     this.isToolKey = isToolKey
 
-    if (keyCode === 8 || keyCode === 46) { // 8 delete键,46 backspace键
+    if (keyCode === 8 || keyCode === 46) { // 8 backspace键,46 delete键
       if (this.activeCharPlace.SectionIndex === 0
         && this.activeCharPlace.LineIndex === 0
         && this.activeCharPlace.RunIndex === 0
@@ -858,8 +939,8 @@ export class TextEditor {
         newChar = await this.getCharPlace('DeleteChar')
       }
       this.activeCharPlace = newChar
-      const oldRect = this.rect
-      await this.updateCanvas(null, oldRect)
+      this.needUndoRec = true
+      await this.updateCanvas(null, true)
       this.clearSelectText()
       this.updateCursorLine()
       return
@@ -869,8 +950,8 @@ export class TextEditor {
       const newChar = await this.getCharPlace('DeleteChars')
       this.activeCharPlace = newChar
       this.selectedCharRange = null
-      const oldRect = this.rect
-      await this.updateCanvas(null, oldRect)
+      this.needUndoRec = true
+      await this.updateCanvas(null, true)
       this.updateCursorLine()
     }
 
@@ -911,24 +992,33 @@ export class TextEditor {
 
     let data = e.data
 
-    if (this.isFirefox) {
+    if (this.isFirefox || isMobileDevice) {
       if (e.inputType === 'insertLineBreak') {
         data = '\n'
       } else if (this.composing || data === null) {
         return
       }
     }
+    this.#editList.push(data)
+    if (this.#editList.length > 1) return
+    this.handleInsertText()
+  }
 
+  async handleInsertText() {
     const newChar = await this.messageHandler.sendWithPromise('InsertText', {
       editAreaPtr: this.editAreaPtr,
       char: this.activeCharPlace,
-      text: data
+      text: this.#editList[0]
     })
     this.activeCharPlace = newChar
 
-    this.saveEdit()
+    this.addUndoHistory()
     this.updateCanvas()
     this.updateCursorLine()
+    this.#editList.length && this.#editList.shift()
+    if (this.#editList.length > 0) {
+      await this.handleInsertText()
+    }
   }
 
   // 兼容FireFox 监听中文输入
@@ -943,7 +1033,7 @@ export class TextEditor {
     })
     this.activeCharPlace = newChar
 
-    this.saveEdit()
+    this.needUndoRec = true
     this.updateCanvas()
     this.updateCursorLine()
 
@@ -964,6 +1054,7 @@ export class TextEditor {
     this.start = start
     this.end = end
 
+    this.oldRect = this.rect || null
     const rect = this.rectCalc(start, end)
     this.rect = rect
   }
@@ -972,7 +1063,7 @@ export class TextEditor {
   async updateCanvas(whole, oldRect) {
     if (!this.removed) await this.getRect()
 
-    if (this.frameContainer && this.outerLine) {
+    if (this.frameContainer && this.outerLineContainer) {
       this.updateOutline({
         start: this.start,
         end: this.end
@@ -1028,7 +1119,7 @@ export class TextEditor {
       this.canvas.height = this.rect.height * this.ratio
 
     } else if (oldRect) {
-      const wholeAreaRect = this.getEntireArea(oldRect, this.rect)
+      const wholeAreaRect = this.getEntireArea(this.oldRect, this.rect)
 
       const imgRect = {
         left: parseInt(wholeAreaRect.left * this.ratio),
@@ -1086,13 +1177,16 @@ export class TextEditor {
   }
 
   // 更新光标位置
-  async updateCursorLine () {
+  async updateCursorLine (char) {
+    char && (this.activeCharPlace = char)
+
     let cursorPoints = await this.messageHandler.sendWithPromise('GetTextCursorPoints', {
       pagePtr: this.pagePtr,
       editAreaPtr: this.editAreaPtr,
       char: this.activeCharPlace
     })
     
+    if (!this.cursor) return
     this.cursor.innerHTML = ''
     const cursorLine = createSvg(
       "line",
@@ -1115,22 +1209,26 @@ export class TextEditor {
       char: this.activeCharPlace
     })
 
+    const fontFamily = await this.messageHandler.sendWithPromise('GetBaseFontName', {
+      editAreaPtr: this.editAreaPtr,
+      char: this.activeCharPlace
+    })
+
     const style = {
       color: textStyle.color,
       opacity: Math.round(textStyle.Transparency * 100),
       fontStyle: textStyle.IsBold && textStyle.IsItalic ? 3 : !textStyle.IsBold && !textStyle.IsItalic ? 0 : textStyle.IsBold ? 1 : 2,
-      fontSize: Math.round(textStyle.FontSize)
+      fontSize: Math.round(textStyle.FontSize),
+      fontFamily,
     }
+    this.textStyle = style
 
-    const fontName = await this.messageHandler.sendWithPromise('GetBaseFontName', {
+    const alignType = await this.messageHandler.sendWithPromise('GetTextSectionAlignType', {
       editAreaPtr: this.editAreaPtr,
       char: this.activeCharPlace
     })
-    style.fontFamily = fontName
-
-    this.textStyle = style
 
-    this.eventBus.dispatch('contentPropertyChange', { type: 'text', ...style })
+    this.eventBus.dispatch('contentPropertyChange', { type: 'text', ...style, alignType })
   }
 
   // 某个操作之后,获取光标所在字符的位置
@@ -1161,6 +1259,10 @@ export class TextEditor {
   // 保存编辑
   saveEdit () {
     this.messageHandler.sendWithPromise('EndEdit', this.editPagePtr)
+    if (this.needUndoRec) {
+      this.addUndoHistory()
+      this.needUndoRec = false
+    }
   }
 
   // 获取区域内的文本
@@ -1217,7 +1319,7 @@ export class TextEditor {
   }
 
   // 获取选中区域里文本的矩形rect
-  async getCharsRect (startPoint, endPoint) {
+  async getCharsRect (startPoint, endPoint, isUpdate) {
     if (
       startPoint.SectionIndex === endPoint.SectionIndex &&
       startPoint.LineIndex === endPoint.LineIndex &&
@@ -1252,8 +1354,11 @@ export class TextEditor {
         this.scale,
         rect
       )
-      this.start = start
-      this.end = end
+
+      if (!isUpdate) {
+        this.start = start
+        this.end = end
+      }
 
       const selectedRect = this.rectCalc(start, end, 0)
       selectedRectList.push(selectedRect)
@@ -1339,107 +1444,20 @@ export class TextEditor {
     this.selectedCharRange = null
   }
 
-  async setTextStyle(props) {
-    for (const item in props) {
-
-      if (item === 'alignType') {
-        if (!this.selectedCharRange) {
-          await this.messageHandler.sendWithPromise('SetTextAligningSection', {
-            editAreaPtr: this.editAreaPtr,
-            alignType: props.alignType,
-            char: activeCharPlace = {
-              SectionIndex: 0,
-              LineIndex: 0,
-              RunIndex: 0,
-              CharIndex: -1
-            }
-          })
-        } else {
-          await this.messageHandler.sendWithPromise('SetTextAligningRange', {
-            editAreaPtr: this.editAreaPtr,
-            alignType: props.alignType,
-            start: this.selectedCharRange.start,
-            end: this.selectedCharRange.end,
-          })
-        }
-      }
-
-      if (!this.selectedCharRange && item !== 'alignType') {
-        await this.selectAllText()
-      }
-      
-      if (item === 'color') {
-        await this.messageHandler.sendWithPromise('SetCharsFontColor', {
-          editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
-          color: this.hexToRgb(props.color)
-        })
-      }
-
-      if (item === 'opacity') {
-        await this.messageHandler.sendWithPromise('SetCharsFontTransparency', {
-          editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
-          opacity: props.opacity / 100
-        })
-      }
-
-      if (item === 'fontSize') {
-        await this.messageHandler.sendWithPromise('SetCharsFontSize', {
-          editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
-          fontSize: props.fontSize
-        })
-      }
-
-      if (item === 'fontFamily') {
-        await this.messageHandler.sendWithPromise('SetFontFromNativeTrueTypeFont', {
-          editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
-          fontFamily: props.fontFamily
-        })
-      }
-
-      if (item === 'fontStyle') {
-        const fontStyle = props.fontStyle
-
-        await this.setCharsFontStyle('ClearCharsFontBold')
-        await this.setCharsFontStyle('ClearCharsFontItalic')
-
-        if (fontStyle === 1) {
-          await this.setCharsFontStyle('SetCharsFontBold')
-        } else if (fontStyle === 2) {
-          await this.setCharsFontStyle('SetCharsFontItalic')
-        } else if (fontStyle === 3) {
-          await this.setCharsFontStyle('SetCharsFontBold')
-          await this.setCharsFontStyle('SetCharsFontItalic')
-        }
-      }
-    }
-
-    const oldRect = this.rect
-    await this.updateCanvas(null, oldRect)
-  }
-
   // 属性面板 修改属性
   async handlePropertyPanelChanged (props) {
     if (this.state === 0) return
 
     let changed = false
 
-    if (!this.selectedCharRange && this.state === 1) {
-      await this.selectAllText()
-    }
+    await this.getEntireCharPlace()
+    const charRange = this.selectedCharRange || this.entireCharRange
 
     for (const item in props) {
       if (props[item] === this.textStyle[item]) continue
 
       if (item === 'alignType') {
-        if (!this.selectedCharRange) {
+        if (!this.selectedCharRange && this.state === 2) {
           await this.messageHandler.sendWithPromise('SetTextAligningSection', {
             editAreaPtr: this.editAreaPtr,
             alignType: props.alignType,
@@ -1449,21 +1467,17 @@ export class TextEditor {
           await this.messageHandler.sendWithPromise('SetTextAligningRange', {
             editAreaPtr: this.editAreaPtr,
             alignType: props.alignType,
-            start: this.selectedCharRange.start,
-            end: this.selectedCharRange.end,
+            start: charRange.start,
+            end: charRange.end,
           })
         }
       }
-
-      if (!this.selectedCharRange && item !== 'alignType' && this.state === 2) {
-        await this.selectAllText()
-      }
       
       if (item === 'color') {
         await this.messageHandler.sendWithPromise('SetCharsFontColor', {
           editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
+          start: charRange.start,
+          end: charRange.end,
           color: this.hexToRgb(props.color)
         })
       }
@@ -1471,8 +1485,8 @@ export class TextEditor {
       if (item === 'opacity') {
         await this.messageHandler.sendWithPromise('SetCharsFontTransparency', {
           editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
+          start: charRange.start,
+          end: charRange.end,
           opacity: props.opacity / 100
         })
       }
@@ -1480,8 +1494,8 @@ export class TextEditor {
       if (item === 'fontSize') {
         await this.messageHandler.sendWithPromise('SetCharsFontSize', {
           editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
+          start: charRange.start,
+          end: charRange.end,
           fontSize: props.fontSize
         })
       }
@@ -1489,8 +1503,8 @@ export class TextEditor {
       if (item === 'fontFamily') {
         await this.messageHandler.sendWithPromise('SetFontFromNativeTrueTypeFont', {
           editAreaPtr: this.editAreaPtr,
-          start: this.selectedCharRange.start,
-          end: this.selectedCharRange.end,
+          start: charRange.start,
+          end: charRange.end,
           fontFamily: props.fontFamily
         })
       }
@@ -1499,7 +1513,9 @@ export class TextEditor {
         const fontStyle = props.fontStyle
 
         await this.setCharsFontStyle('ClearCharsFontBold')
+        await this.refreshSelecedRange()
         await this.setCharsFontStyle('ClearCharsFontItalic')
+        await this.refreshSelecedRange()
 
         if (fontStyle === 1) {
           await this.setCharsFontStyle('SetCharsFontBold')
@@ -1513,26 +1529,15 @@ export class TextEditor {
 
       this.textStyle[item] = props[item]
       changed = true
+      this.needUndoRec = true
     }
 
     if (!changed) return
 
-    const oldRect = this.rect
-    await this.updateCanvas(null, oldRect)
+    await this.updateCanvas(null, true)
 
     if (this.selectedCharRange && this.state === 2) {
-      const { start, end } = await this.messageHandler.sendWithPromise('RefreshRange', {
-        editAreaPtr: this.editAreaPtr,
-        start: this.selectedCharRange.start,
-        end: this.selectedCharRange.end,
-      })
-      this.selectedCharRange = { start, end }
-
-      if (this.activeCharPlace.CharIndex === start.CharIndex) {
-        this.activeCharPlace = start
-      } else {
-        this.activeCharPlace = end
-      }
+      await this.refreshSelecedRange()
     }
     
     if (this.state === 2) {
@@ -1543,10 +1548,11 @@ export class TextEditor {
 
   // 设置文本样式
   async setCharsFontStyle (action) {
+    const charRange = this.selectedCharRange || this.entireCharRange
     await this.messageHandler.sendWithPromise('SetCharsFontStyle', {
       editAreaPtr: this.editAreaPtr,
-      start: this.selectedCharRange.start,
-      end: this.selectedCharRange.end,
+      start: charRange.start,
+      end: charRange.end,
       fontStyle: action
     })
   }
@@ -1638,18 +1644,35 @@ export class TextEditor {
     this.textarea.addEventListener('blur', this.onBlur)
     this.textarea.addEventListener('keydown', this.onKeydown)
     this.textarea.addEventListener(this.textInput, this.onTextInput)
-    if (this.isFirefox) {
+    if (this.isFirefox || isMobileDevice) {
       this.textarea.addEventListener('compositionstart', this.onCompositionstart)
       this.textarea.addEventListener('compositionend', this.onCompositionend)
     }
 
-    onClickOutsideUp([this.textContainer, this.outerLine, document.querySelector('.editor-panel'), document.getElementById('propertyPanelButton')], this.handleOutside.bind(this))
+    const elements = [this.textContainer, this.outerLine]
+
+    const editorPanel = document.querySelector('.editor-panel')
+    editorPanel && elements.push(editorPanel)
+    const propertyPanelButton = document.getElementById('propertyPanelButton')
+    propertyPanelButton && elements.push(propertyPanelButton)
+    const undo = document.getElementById('undo')
+    undo && elements.push(undo)
+    const redo = document.getElementById('redo')
+    redo && elements.push(redo)
+    const contentEditorPopup = document.querySelector('.content-editor-popup')
+    contentEditorPopup && elements.push(contentEditorPopup)
+    
+    onClickOutsideUp(elements, this.handleOutside.bind(this))
   }
 
   // 删除区域
   async remove () {
-    await this.outerLine.remove()
+    this.state = 0
+    await this.handleOutside()
+
+    await this.outerLineContainer.remove()
     this.frameContainer.remove()
+    this.frameContainer = null
     this.contentContainer.selectedFrameIndex = -1
     this.removed = true
 
@@ -1662,9 +1685,26 @@ export class TextEditor {
     //   editAreaPtr: this.editAreaPtr
     // })
 
+    this.needUndoRec = true
     this.updateCanvas()
     this.contentContainer.removeEditor(this.editAreaIndex)
     this.eventBus.dispatch('contentPropertyChange', { type: 'text', isOpen: false })
+
+    this.eventBus._off('textPropertyChanged', this.onHandlePropertyPanelChanged)
+    this.eventBus._off('showContentEditorType', this.onShowContentEditorType)
+    this.eventBus._off('contentEditorPopupClicked', this.onHandlePopup)
+  }
+  
+  removeViewer () {
+    this.state = 0
+    // await this.handleOutside()
+    
+    this.outerLineContainer?.remove()
+    this.frameContainer?.remove()
+    this.frameContainer = null
+    this.contentContainer.selectedFrameIndex = -1
+    this.removed = true
+    this.eventBus.dispatch('contentPropertyChange', { type: 'text', isOpen: false })
   }
 
   // 复制区域对象
@@ -1679,9 +1719,16 @@ export class TextEditor {
     })
   }
 
+  // 获取整个区域的文本信息
+  async getEntireCharPlace () {
+    const { start, end } = await this.messageHandler.sendWithPromise('GetBeginAndEndCharPlace', this.editAreaPtr)
+    this.entireCharRange = { start, end }
+    return { start, end }
+  }
+
   // 选中整个区域的文本
   async selectAllText () {
-    const { start, end } = await this.messageHandler.sendWithPromise('GetBeginAndEndCharPlace', this.editAreaPtr)
+    const { start, end } = await this.getEntireCharPlace()
     this.selectedCharRange = { start, end }
     if (this.state === 2) this.selectedRectList = await this.getCharsRect(start, end)
   }
@@ -1699,4 +1746,136 @@ export class TextEditor {
       this.frameContainer && (this.frameContainer.style.display = 'block')
     }
   }
+
+  // 添加undo记录
+  addUndoHistory () {
+    this.contentContainer.handleOperateList({
+      editPagePtr: this.editPagePtr,
+      editAreaIndex: this.editAreaIndex,
+      editAreaPtr: this.editAreaPtr,
+      pageIndex: this.pageViewer.pageIndex
+    })
+  }
+
+  // undo/redo后 更新canvas图
+  async updateCanvasAfterUndoRedo() {
+    await this.getRect()
+
+    if (this.frameContainer && this.outerLineContainer) {
+      this.updateOutline({
+        start: this.start,
+        end: this.end
+      })
+
+      setCss(this.frameContainer, {
+        left: this.rect.left + 'px',
+        top: this.rect.top + 'px',
+        width: this.rect.width + 'px',
+        height: this.rect.height + 'px'
+      })
+    }
+
+    const wholeAreaRect = this.getEntireArea(this.oldRect, this.rect)
+
+    const imgRect = {
+      left: parseInt(wholeAreaRect.left * this.ratio),
+      top: parseInt(wholeAreaRect.top * this.ratio),
+      right: parseInt(wholeAreaRect.right * this.ratio),
+      bottom: parseInt(wholeAreaRect.bottom * this.ratio),
+      width: parseInt(wholeAreaRect.right * this.ratio) - parseInt(wholeAreaRect.left * this.ratio),
+      height: parseInt(wholeAreaRect.bottom * this.ratio) - parseInt(wholeAreaRect.top * this.ratio),
+    }
+    this.imgRect = imgRect
+
+    this.canvas.width = this.rect.width * this.ratio
+    this.canvas.height = this.rect.height * this.ratio
+
+    let { imageArray } = await this.messageHandler.sendWithPromise('PushRenderTask', {
+      pagePtr: this.pagePtr,
+      scale: this.scale * this.ratio,
+      left: this.imgRect.left,
+      right: this.imgRect.right,
+      bottom: this.imgRect.bottom,
+      top: this.imgRect.top
+    })
+    this.imageArray = imageArray
+
+    this.drawCanvas()
+    this.saveEdit()
+    if (this.contentContainer.selectedFrameIndex === this.editAreaIndex) {
+      this.getTextStyle()
+    }
+  }
+
+  // 修改属性后,刷新选中范围的数据
+  async refreshSelecedRange() {
+    const charRange = this.selectedCharRange || this.entireCharRange
+    const { start, end } = await this.messageHandler.sendWithPromise('RefreshRange', {
+      editAreaPtr: this.editAreaPtr,
+      start: charRange.start,
+      end: charRange.end,
+    })
+
+    if (this.selectedCharRange) this.selectedCharRange = { start, end }
+    else this.entireCharRange = { start, end }
+
+    if (this.activeCharPlace.CharIndex === start.CharIndex) this.activeCharPlace = start
+    else this.activeCharPlace = end
+  }
+
+  // 计算坐标并显示文本悬浮窗
+  async showPopup() {
+    const pageRect = this.container.getBoundingClientRect()
+    const rect = {
+      left: this.rect.left + pageRect.left,
+      top: this.rect.top + pageRect.top,
+      right: this.rect.width + this.rect.left + pageRect.left,
+      bottom: this.rect.height + this.rect.top + pageRect.top
+    }
+    this.eventBus.dispatch('showContentEditorPopup', { rect })
+    this.isPopupShow = true
+
+    const textRects = []
+    const { start, end } = this.selectedCharRange || await this.getEntireCharPlace()
+    const oriTextRects = await this.getCharsRect(start, end, true)
+
+    for (let i = 0; i < oriTextRects.length; i++) {
+      const textRect = oriTextRects[i]
+      const { left, top, width, height } = textRect
+      textRects.push({
+        left,
+        top,
+        right: left + width,
+        bottom: top + height
+      })
+    }
+
+    const selectedText = await this.getText()
+    this.eventBus.dispatch('contentSelected', {
+      selectedText,
+      pageNumber: this.pageViewer.pageIndex + 1,
+      textRects
+    })
+  }
+
+  hidePopup() {
+    if (!this.isPopupShow) return
+    this.isPopupShow = false
+    this.eventBus.dispatch('showContentEditorPopup')
+  }
+
+  async handlePopup(data) {
+    if (this.state === 0) return
+
+    switch (data) {
+      case 'copy':
+        const copyText = await this.getText()
+        copy(copyText)
+        break;
+
+      case 'delete':
+        this.remove()
+        break;
+    }
+  }
 }

+ 18 - 1
packages/core/src/index.js

@@ -20,6 +20,7 @@ import { InkSign } from "./ink_sign"
 import MessageHandler from "./message_handler"
 import JSZip from 'jszip'
 import Outline from './Outline'
+import { ContentEditManager } from "./editor/content_edit_manager.js"
 
 GlobalWorkerOptions.workerSrc = './lib/pdf.worker.min.js'
 const CMAP_URL = './cmaps/'
@@ -2222,7 +2223,9 @@ class ComPDFKitViewer {
         pdfThumbnailViewer: this.pdfThumbnailViewer,
         eventBus: this.eventBus
       });
-      this.pdfSidebar.onToggled = this.forceRendering.bind(this);
+      this.pdfSidebar.onToggled = this.forceRendering.bind(this)
+
+      this.contentEditManager = new ContentEditManager(this)
     }
   }
 
@@ -3009,6 +3012,20 @@ class ComPDFKitViewer {
     }
     return pages
   }
+  
+  // 内容编辑 - undo/redo
+  async resetOperate(data) {
+    const { operation, pageNumber } = data
+    this.pdfViewer._pages[pageNumber - 1].contentContainer.handleResetOperate(operation)
+  }
+
+  getContentEditManager() {
+    return this.contentEditManager
+  }
+
+  getContentEditHistoryManager() {
+    return this.pdfViewer.contentEditHistoryManager
+  }
 
   getMinRadioSize(width, height, pageSize) {
     const radio = Math.min(

+ 32 - 14
packages/core/src/pdf_page_view.js

@@ -165,6 +165,7 @@ class PDFPageView {
     this.eventBus._on('search', this.handleSearch.bind(this))
 
     this.mode = null
+    this.contentEditHistoryManager = options.contentEditHistoryManager
 
     container?.style.setProperty(
       "--scale-factor",
@@ -262,7 +263,7 @@ class PDFPageView {
       if (['editText', 'editImage', 'addText', 'addImage'].includes(tool) && this.toolMode === 'annotation') {
         this.contentContainer.resetTools()
         this.contentContainer.tool = tool
-        await this.contentContainer.render()
+        await this.contentContainer.init()
       } else if (this.mode === 'editor') {
         this.contentContainer.resetTools()
       } else {
@@ -280,7 +281,7 @@ class PDFPageView {
     this.toolMode = mode
     if (this.contentContainer) {
       if (mode === 'editor') {
-        await this.contentContainer.render()
+        await this.contentContainer.init()
       } else {
         this.contentContainer.destroy()
       }
@@ -437,6 +438,17 @@ class PDFPageView {
     this.annotationEditorLayer.render(this.viewport, "display");
   }
 
+  selectAnnotation(annotation) {
+    annotation.name && (this.annotationStore.toSelectAnnotationName = annotation.name)
+    if (!this.compdfAnnotationLayer) return
+    const annotationsArray = this.compdfAnnotationLayer.annotationsArray
+    const annotationIndex = annotationsArray.findIndex(item => item.annotation.name === this.annotationStore.toSelectAnnotationName)
+    if (annotationIndex > -1) {
+      this.compdfAnnotationLayer.annotationsArray[annotationIndex].selectAnnotation()
+      this.annotationStore.toSelectAnnotationName = null
+    }
+  }
+
   renderPage() {
     if (this.renderingState !== RenderingStates.INITIAL) {
       console.error("Must be in new state before drawing");
@@ -560,8 +572,9 @@ class PDFPageView {
               pagePtr: this.pagesPtr[this.pageIndex].pagePtr,
               messageHandler: this.messageHandler,
               $t: this.$t,
-              tool: this.tool
-            })
+              contentEditHistoryManager: this.contentEditHistoryManager
+            });
+            (this.mode === 'editor' || ['editText', 'editImage', 'addText', 'addImage'].includes(this.tool)) && await this.contentContainer.init()
           }
           if (this.mode === 'editor' || ['editText', 'editImage', 'addText', 'addImage'].includes(this.tool)) {
             await this.contentContainer.render()
@@ -942,8 +955,7 @@ class PDFPageView {
       this.annotationEditorLayer = null;
     }
     if (this.contentContainer) {
-      this.contentContainer.cancel();
-      this.contentContainer = null;
+      this.contentContainer.cancel()
     }
 
     if (this.textSelection) {
@@ -1053,10 +1065,10 @@ class PDFPageView {
         selected: this.selected,
         pagePtr: this.pagesPtr[this.pageIndex].pagePtr,
         messageHandler: this.messageHandler,
-        $t: this.$t
+        $t: this.$t,
+        contentEditHistoryManager: this.contentEditHistoryManager
       })
-
-      await this.contentContainer.render()
+      await this.contentContainer.init()
     }
 
     this.contentContainer.addTextEditor(data)
@@ -1078,10 +1090,10 @@ class PDFPageView {
         selected: this.selected,
         pagePtr: this.pagesPtr[this.pageIndex].pagePtr,
         messageHandler: this.messageHandler,
-        $t: this.$t
+        $t: this.$t,
+        contentEditHistoryManager: this.contentEditHistoryManager
       })
-
-      await this.contentContainer.render()
+      await this.contentContainer.init()
     }
 
     return this.contentContainer.frameEditorList
@@ -1216,12 +1228,18 @@ class PDFPageView {
               pagePtr: this.pagesPtr[this.pageIndex].pagePtr,
               messageHandler: this.messageHandler,
               $t: this.$t,
-              tool: this.tool
-            })
+              contentEditHistoryManager: this.contentEditHistoryManager
+            });
+
+            (this.mode === 'editor' || ['editText', 'editImage', 'addText', 'addImage'].includes(this.tool)) && await this.contentContainer.init()
           }
           if (this.mode === 'editor' || ['editText', 'editImage', 'addText', 'addImage'].includes(this.tool)) {
             await this.contentContainer.render()
           }
+          if (this.contentContainer && this.contentContainer.pagePtr !== this.pagesPtr[this.pageIndex].pagePtr) {
+            this.contentContainer.pagePtr = this.pagesPtr[this.pageIndex].pagePtr
+            await this.contentContainer.update()
+          }
 
           if (!this.textSelection) {
             this.textSelection = new TextSelection({

+ 19 - 6
packages/core/src/pdf_viewer.js

@@ -37,6 +37,7 @@ import {
 } from "./ui_utils.js";
 
 import { PDFPageView } from "./pdf_page_view.js";
+import { ContentEditHistoryManager } from "./editor/content_edit_history_manager.js";
 
 const DEFAULT_CACHE_SIZE = 3;
 const ENABLE_PERMISSIONS_CLASS = "enablePermissions";
@@ -180,6 +181,7 @@ class PDFViewer {
     this.doc = options.doc;
     this.messageHandler = options.messageHandler;
     this._fontFile = null;
+    this.contentEditHistoryManager = null;
 
     if (
       this.pageColors &&
@@ -622,16 +624,20 @@ class PDFViewer {
         // see issue 15795.
         this.viewer.style.setProperty("--scale-factor", viewport.scale);
 
+        this.contentEditHistoryManager = new ContentEditHistoryManager(this)
+
         for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
+          const pageIndex = pageNum - 1;
+
           const pageView = new PDFPageView({
             container: viewerElement,
             eventBus: this.eventBus,
             id: pageNum,
-            pageIndex: pageNum - 1,
+            pageIndex,
             scale,
             messageHandler: this.messageHandler,
             annotationStore: this.annotationStore,
-            annotations: annotations && annotations[pageNum - 1] || null,
+            annotations: annotations && annotations[pageIndex] || null,
             annotationsAll: annotations,
             pagesPtr,
             defaultViewport: viewport.clone(),
@@ -652,7 +658,8 @@ class PDFViewer {
             l10n: this.l10n,
             $t: this.$t,
             doc: this.doc,
-            messageHandler: this.messageHandler
+            messageHandler: this.messageHandler,
+            contentEditHistoryManager: this.contentEditHistoryManager,
           });
           this._pages.push(pageView);
         }
@@ -772,9 +779,11 @@ class PDFViewer {
   }
 
   renderAnnotation(annotation, show) {
-    if (!this._pages[annotation.pageIndex]) return
-    if (this._pages[annotation.pageIndex].compdfAnnotationLayer) {
-      this._pages[annotation.pageIndex].compdfAnnotationLayer.renderAnnotation(annotation, show);
+    const page = this._pages[annotation.pageIndex]
+    if (!page) return
+    if (page.compdfAnnotationLayer) {
+      page.textSelection && page.textSelection.cleanSelection()
+      page.compdfAnnotationLayer.renderAnnotation(annotation, show);
     }
   }
 
@@ -794,6 +803,7 @@ class PDFViewer {
     this._scrollMode = ScrollMode.VERTICAL;
     this._previousScrollMode = ScrollMode.UNKNOWN;
     this._spreadMode = SpreadMode.NONE;
+    this.contentEditHistoryManager = null;
 
     this.#scrollModePageState = {
       previousPageNumber: 1,
@@ -1465,6 +1475,9 @@ class PDFViewer {
    */
   _cancelRendering() {
     for (const pageView of this._pages) {
+      pageView.contentContainer?.destroy();
+      pageView.contentContainer = null;
+
       pageView.cancelRendering();
     }
   }

+ 74 - 19
packages/core/src/worker/compdfkit_worker.js

@@ -696,7 +696,8 @@ class CPDFWorker {
         rect.left,
         rect.right,
         rect.bottom,
-        rect.top
+        rect.top,
+        true
       )
     })
 
@@ -858,7 +859,7 @@ class CPDFWorker {
         editAreaPtr,
         char.SectionIndex, char.LineIndex, char.RunIndex, char.CharIndex,
         textPtr,
-        false
+        true
       )
       return EditCharPlace
     })
@@ -932,7 +933,8 @@ class CPDFWorker {
         editAreaPtr,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
         end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
-        color.r, color.g, color.b
+        color.r, color.g, color.b,
+        true
       )
     })
 
@@ -942,7 +944,8 @@ class CPDFWorker {
         editAreaPtr,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
         end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
-        opacity
+        opacity,
+        true
       )
     })
 
@@ -951,7 +954,8 @@ class CPDFWorker {
       Module._SetTextAligningSection(
         editAreaPtr,
         alignType,
-        char.SectionIndex, char.LineIndex, char.RunIndex, char.CharIndex
+        char.SectionIndex, char.LineIndex, char.RunIndex, char.CharIndex,
+        true
       )
     })
 
@@ -961,7 +965,8 @@ class CPDFWorker {
         editAreaPtr,
         alignType,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
-        end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex
+        end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
+        true
       )
     })
 
@@ -972,6 +977,7 @@ class CPDFWorker {
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
         end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
         fontSize,
+        true,
         true
       )
     })
@@ -984,7 +990,8 @@ class CPDFWorker {
         editAreaPtr,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
         end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
-        font
+        font,
+        true
       )
     })
 
@@ -1012,7 +1019,8 @@ class CPDFWorker {
       Module[action](
         editAreaPtr,
         start.SectionIndex, start.LineIndex, start.RunIndex, start.CharIndex,
-        end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex
+        end.SectionIndex, end.LineIndex, end.RunIndex, end.CharIndex,
+        true
       )
     })
 
@@ -1034,7 +1042,8 @@ class CPDFWorker {
         fontData.opacity,
         fontData.isBold,
         fontData.italic,
-        alignType
+        alignType,
+        true
       )
     })
 
@@ -1042,7 +1051,8 @@ class CPDFWorker {
       const { editPagePtr, editAreaIndex } = data
       Module._RemoveEditAreaByIndex(
         editPagePtr,
-        editAreaIndex
+        editAreaIndex,
+        true
       )
     })
 
@@ -1050,7 +1060,8 @@ class CPDFWorker {
       const { editPagePtr, editAreaPtr } = data
       Module._RemoveEditArea(
         editPagePtr,
-        editAreaPtr
+        editAreaPtr,
+        true
       )
     })
 
@@ -1356,7 +1367,8 @@ class CPDFWorker {
         rect.bottom,
         rect.top,
         0,
-        imageData.length
+        imageData.length,
+        true
       )
     })
 
@@ -1377,26 +1389,27 @@ class CPDFWorker {
         rect.bottom,
         rect.top,
         0,
-        imageData.length
+        imageData.length,
+        true
       )
     })
 
     messageHandler.on('RotateImage', (data) => {
       const { editAreaPtr, angle } = data
-      return Module._RotateImage(editAreaPtr, angle)
+      return Module._RotateImage(editAreaPtr, angle, true)
     })
 
     messageHandler.on('HorizontalMirrorImage', (editAreaPtr) => {
-      return Module._HorizontalMirrorImage(editAreaPtr)
+      return Module._HorizontalMirrorImage(editAreaPtr, true)
     })
 
     messageHandler.on('VerticalMirrorImage', (editAreaPtr) => {
-      return Module._VerticalMirrorImage(editAreaPtr)
+      return Module._VerticalMirrorImage(editAreaPtr, true)
     })
 
     messageHandler.on('SetImageTransparency', (data) => {
       const { editAreaPtr, opacity } = data
-      return Module._SetImageTransparency(editAreaPtr, opacity)
+      return Module._SetImageTransparency(editAreaPtr, opacity, true)
     })
 
     messageHandler.on('GetImageTransparency', (editAreaPtr) => {
@@ -1424,7 +1437,8 @@ class CPDFWorker {
         rect.left,
         rect.right,
         rect.bottom,
-        rect.top
+        rect.top,
+        true
       )
     })
 
@@ -1452,7 +1466,48 @@ class CPDFWorker {
         rect.left,
         rect.right,
         rect.bottom,
-        rect.top
+        rect.top,
+        true
+      )
+    })
+
+
+
+    messageHandler.on('CanRedo', (editPagePtr) => {
+      const res = Module._CanRedo(editPagePtr)
+      return res
+    })
+
+    messageHandler.on('CanUndo', (editPagePtr) => {
+      const res = Module._CanUndo(editPagePtr)
+      return res
+    })
+
+    messageHandler.on('Redo', (editPagePtr) => {
+      EditCharPlace = {}
+      const editAreaPtr = Module._Redo(editPagePtr)
+
+      return {
+        editAreaPtr,
+        editCharPlace: EditCharPlace
+      }
+    })
+
+    messageHandler.on('Undo', (editPagePtr) => {
+      EditCharPlace = {}
+      const editAreaPtr = Module._Undo(editPagePtr)
+
+      return {
+        editAreaPtr,
+        editCharPlace: EditCharPlace
+      }
+    })
+
+    messageHandler.on('GetTextSectionAlignType', async (data) => {
+      const { editAreaPtr, char } = data
+      return Module._GetTextSectionAlignType(
+        editAreaPtr,
+        char.SectionIndex, char.LineIndex, char.RunIndex, char.CharIndex,
       )
     })
   }

+ 2 - 2
packages/webview/locales/de.json

@@ -55,8 +55,8 @@
     "addText": "Text hinzufügen",
     "editImage": "Bild bearbeiten",
     "addImage": "Bild einfügen",
-    "undo": "Rückgängig",
-    "redo": "Redo"
+    "undo": "Rückgängig machen",
+    "redo": "Wiederholen"
   },
 
   "nextPage": "Nächste Seite",

+ 1 - 1
packages/webview/locales/fr.json

@@ -55,7 +55,7 @@
     "addText": "Ajouter un texte",
     "editImage": "Éditer l'image",
     "addImage": "Insérer une image",
-    "undo": "Annuler",
+    "undo": "Défaire",
     "redo": "Refaire"
   },
 

+ 2 - 2
packages/webview/locales/it.json

@@ -55,8 +55,8 @@
     "addText": "Aggiungere testo",
     "editImage": "Modifica immagine",
     "addImage": "Inserire immagine",
-    "undo": "Annullare",
-    "redo": "Ripristinare"
+    "undo": "Disfare",
+    "redo": "Rifare"
   },
 
   "nextPage": "Pagina successiva",

+ 1 - 1
packages/webview/src/components/Annotate/Annotate.vue

@@ -90,7 +90,7 @@
     const markups = ['highlight', 'underline', 'squiggly', 'strikeout']
     return markups.includes(activeTool.value)
   })
-  
+
   const showColors = ['highlight', 'underline', 'squiggly', 'strikeout', 'square', 'circle', 'arrow', 'line', 'ink']
 
   const shapeActive = computed(() => {

+ 0 - 7
packages/webview/src/components/ContentEditorToolBar/ContentEditorToolBar.vue

@@ -6,13 +6,6 @@
     <Button class="operate" :class="{ active: activeTool === 'addImage' }" @click="changeActiveTool('addImage')" :title="$t('header.addImage')">
       <AddImage />{{ $t('header.addImage') }}<input v-if="isMobileDevice" type="file" ref="editorImageInput" accept=".png, .jpg, .jpeg" style="display: none;" @change="handleFile">
     </Button>
-    <!-- <div class="divider pc"></div>
-    <Button class="history" :title="$t('header.undo')">
-      <EditBack />
-    </Button>
-    <Button class="history" :title="$t('header.redo')">
-      <EditGo />
-    </Button> -->
   </div>
 </template>
 

+ 1 - 1
packages/webview/src/components/Dialogs/DeletePageDialog.vue

@@ -3,7 +3,7 @@
     <Dialog :show="show" :dialogName="dialogName" :close="false">
       <!-- content -->
       <Warning />
-      <p>{{ $t('documentEditor.deleteConfirmText') }}{{ locale === 'en' ? ` ${pageStr}?` : '' }}</p>
+      <p>{{ $t('documentEditor.deleteConfirmText') }}{{ locale === 'zh-CN' ? '' : ` ${pageStr}?` }}</p>
 
       <!-- footer -->
       <template #footer>

+ 4 - 0
packages/webview/src/components/HeaderItems/HeaderItems.vue

@@ -2,12 +2,14 @@
   <div v-if="toolMode !== 'compare' && toolMode !== 'document'" class="header-items">
     <template v-for="(item, index) in items" :key="`${item.type}-${item.dataElement || index}`">
       <CustomButton v-if="item.name === 'customButton' && !item.hidden && !item.dropItem" :item="item" />
+      <UndoRedo v-else-if="item.type === 'undoRedoButton' && ['editText', 'editImage', 'addText', 'addImage'].includes(activeTool) && !item.hidden && item.className === 'mobile' && !item.dropItem" class="mobile" device="mobile" />
       <ToggleElementButton v-else-if="item.type === 'toggleElementButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
       <ToolButton v-else-if="item.type === 'toolButton' && !item.hidden" :item="item" :data-element="item.dataElement" />
       <FullScreenButton v-else-if="item.type === 'fullScreenButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
       <HandButton v-else-if="item.type === 'handToolButton' && !item.hidden" :item="item" :class="{ disabled: !load }" :data-element="item.dataElement" />
       <ZoomOverlay v-else-if="item.type === 'zoomOverlay' && !item.hidden" :data-element="item.dataElement" />
       <ThemeMode v-else-if="item.type === 'themeMode' && !item.hidden" :data-element="item.dataElement" />
+      <UndoRedo v-else-if="item.type === 'undoRedoButton' && ['editText', 'editImage', 'addText', 'addImage'].includes(activeTool) && !item.hidden && item.className === 'pc' && !item.dropItem" class="pc" device="pc" />
       <ViewRotationControls v-else-if="item.type === 'viewRotationControls' && !item.hidden" :data-element="item.dataElement" />
       <div v-else-if="['spacer', 'divider'].includes(item.type)" :class="item.type"></div>
     </template>
@@ -157,6 +159,8 @@ const changeToolMode = (mode) => {
   }
 }
 
+const activeTool = computed(() => useDocument.getActiveTool)
+
 const openSignCreatePanel = () => {
   useViewer.toggleElement('signCreatePanel')
   useViewer.setActiceToolMode('sign')

+ 88 - 0
packages/webview/src/components/UndoRedo/UndoRedo.vue

@@ -0,0 +1,88 @@
+<template>
+  <div class="undo-redo-container">
+    <Button class="history" id="undo" :class="{ disabled: !undoListLength }" :title="$t('header.undo')" @click="reset('undo')">
+      <EditBack />
+    </Button>
+    <Button class="history" id="redo" :class="{ disabled: !redoListLength }" :title="$t('header.redo')" @click="reset('redo')">
+      <EditGo />
+    </Button>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted } from 'vue'
+import { useViewerStore } from '@/stores/modules/viewer'
+import core from '@/core'
+import { useDocumentStore } from '@/stores/modules/document';
+const { resetOperate } = core
+const { device } = defineProps(['device'])
+
+const useViewer = useViewerStore()
+const useDocument = useDocumentStore()
+
+const undoListLength = ref(0)
+const redoListLength = ref(0)
+const currentPage = computed(() => useViewer.getCurrentPage)
+const activeTool = computed(() => useDocument.getActiveTool)
+
+const reset = (operation) => {
+  resetOperate({
+    operation,
+    pageNumber: currentPage.value
+  })
+}
+
+const changeListLength = (data) => {
+  undoListLength.value = data.undoListLength
+  redoListLength.value = data.redoListLength
+}
+core.addEvent('changeOperateList', changeListLength)
+
+// 监听键盘事件
+const handleKeyDown = (event) => {
+  const key = event.key.toLowerCase()
+
+  // 检查撤销(Ctrl/Cmd + Z)
+  if ((event.ctrlKey || event.metaKey) && key === 'z' && !event.shiftKey) {
+    undoListLength.value && reset('undo') // 执行撤销操作
+  }
+
+  // 检查重做(Ctrl + Y/Cmd + Shift + Z)
+  if ((event.ctrlKey && key === 'y') || (event.metaKey && event.shiftKey && key === 'z')) {
+    redoListLength.value && reset('redo') // 执行重做操作
+  }
+}
+
+// 组件挂载时开始监听
+onMounted(() => {
+  if (device === 'pc') {
+    document.addEventListener('keydown', handleKeyDown)
+  }
+})
+
+// 组件卸载时移除监听
+onUnmounted(() => {
+  if (device === 'pc') {
+    document.removeEventListener('keydown', handleKeyDown)
+  }
+})
+</script>
+
+<style>
+.undo-redo-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+@media screen and (min-width: 768px) {
+  .undo-redo-container.mobile {
+    display: none;
+  }
+}
+
+@media screen and (max-width: 767.9px) {
+  .undo-redo-container.pc {
+    display: none;
+  }
+}
+</style>

+ 2 - 0
packages/webview/src/core/index.js

@@ -61,6 +61,7 @@ import addEditorImage from './addEditorImage'
 import flattenPdf from './flattenPdf'
 import getPageWidth from './getPageWidth'
 import getPageHeight from './getPageHeight'
+import resetOperate from './resetOperate'
 
 export default {
   getDocumentViewer,
@@ -129,4 +130,5 @@ export default {
   flattenPdf,
   getPageWidth,
   getPageHeight,
+  resetOperate,
 }

+ 3 - 0
packages/webview/src/core/resetOperate.js

@@ -0,0 +1,3 @@
+import core from '@/core'
+
+export default (op) => core.getDocumentViewer().resetOperate(op)

+ 12 - 0
packages/webview/src/stores/modules/viewer.js

@@ -50,6 +50,12 @@ export const useViewerStore = defineStore({
       signPanelTab: 'trackpad'
     },
     headers: [
+      {
+        type: 'undoRedoButton',
+        dataElement: 'undoRedoButton',
+        element: 'undoRedoButton',
+        className: 'mobile'
+      },
       {
         type: 'toggleElementButton',
         img: 'icon-header-sidebar-line',
@@ -105,6 +111,12 @@ export const useViewerStore = defineStore({
         dataElement: 'themeMode',
         element: 'themeMode'
       },
+      {
+        type: 'undoRedoButton',
+        dataElement: 'undoRedoButton',
+        element: 'undoRedoButton',
+        className: 'pc'
+      },
       {
         type: 'stickyNoteButton',
         dataElement: 'stickyNoteButton',