Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(image): add right-click menu to save and copy image #196

Merged
merged 1 commit into from
Jan 10, 2025

Conversation

qwangry
Copy link
Contributor

@qwangry qwangry commented Jan 5, 2025

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #124

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • New Features

    • Added an image bar component to the Quill editor.
    • Introduced context menu for images with download and copy functionality.
    • Enhanced image interaction with dynamic toolbar positioning.
  • Style

    • Added comprehensive styling for the image bar component.
    • Implemented responsive design with CSS variables and flex layout.

Copy link

coderabbitai bot commented Jan 5, 2025

Walkthrough

This pull request introduces a new image bar feature for the Quill editor, enhancing image interaction capabilities. The changes include creating a new ImageBar class in image-bar.ts, adding corresponding styling in imageBar.scss, and updating the BlotFormatter to integrate the new image bar functionality. The implementation allows users to interact with images through a context menu, providing options to download and copy images directly from the editor.

Changes

File Change Summary
packages/fluent-editor/src/assets/imageBar.scss Added .ql-image-bar CSS class with comprehensive styling for image bar components, including flex layout, hover effects, and icon styling.
packages/fluent-editor/src/assets/style.scss Added import for imageBar stylesheet.
packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts Added imageBar property and updated constructor to handle image bar context menu interactions.
packages/fluent-editor/src/modules/custom-image/image-bar.ts Created new ImageBar class to manage image toolbar with download and copy functionality.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • kagol

Poem

🐰 A rabbit's tale of image delight,
Toolbar dancing with pixels so bright,
Click and copy, download with glee,
Quill's new magic, for you and for me!
Image bar hopping, a feature so neat! 🖼️


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

Walkthrough

This pull request introduces a new feature that adds a right-click menu to images, allowing users to save or copy images directly from the editor. This enhancement improves user interaction with images within the editor.

Changes

File Summary
packages/fluent-editor/src/assets/imageBar.scss Added styles for the new image bar component, including positioning, transitions, and hover effects.
packages/fluent-editor/src/assets/style.scss Imported the new imageBar styles into the main stylesheet.
packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts Integrated the ImageBar component to handle right-click events on images, creating and destroying the image bar as needed.
packages/fluent-editor/src/modules/custom-image/image-bar.ts Implemented the ImageBar class, which manages the creation, positioning, and interaction of the image bar for downloading and copying images.

}
else if (operate === 'copy') {
const imageUrl = this.image.src
const response = await fetch(imageUrl)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using fetch to retrieve the image data and then writing it to the clipboard could lead to potential security issues if the image source is not trusted. Ensure that the image source is validated or sanitized to prevent any malicious content from being executed.

@qwangry
Copy link
Contributor Author

qwangry commented Jan 5, 2025

  • 不知道该如何设置 复制图标 css 的 content,暂时写的svg
  • 复制svg格式的图片,再粘贴进编辑器,无法显示,这个需要考虑吗

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
packages/fluent-editor/src/modules/custom-image/image-bar.ts (4)

1-6: Use explicit typings for maintainability and clarity.

Both quill: any and image: any could benefit from more specific types for better readability and IDE assistance.


22-43: Add cleanup logic for event listeners.

Events are attached to imageDownload and imageCopy but there is no direct removal of these listeners on destruction. Consider removing them explicitly in destroy() to avoid memory leaks in large or long-lived sessions.


84-94: Ensure toolbar remains visible within the viewport.

If the image is near the right edge or bottom of the screen, the requested position could extend beyond the viewport. Consider clamping the position to ensure the toolbar is fully visible.


96-112: Favor built-in typing for style properties.

When iterating over the rules object, consider refining the property type. Using more specific CSS styling patterns (like Partial<CSSStyleDeclaration>) might help reduce type mismatches.

packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts (2)

22-22: Add docstring for imageBar.

A short docstring clarifying when and how the imageBar is used would make code navigation smoother for new contributors.


158-163: Consolidate destruction logic.

When hiding the formatter (this.hide()), consider integrating the imageBar teardown there as well to avoid multiple method calls in onClick. This can reduce code duplication.

packages/fluent-editor/src/assets/imageBar.scss (2)

18-26: Consider a single utility class for vertical dividers.

You might define a global .vertical-divider and reuse it across multiple components instead of repeatedly declaring a .ql-split block.


50-53: Ensure color transitions extend across variants.

For consistent user experience, unify the transition property set across .ql-image-preview, .ql-image-download, and .ql-image-copy.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b910a9b and 6ccb52a.

📒 Files selected for processing (4)
  • packages/fluent-editor/src/assets/imageBar.scss (1 hunks)
  • packages/fluent-editor/src/assets/style.scss (1 hunks)
  • packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts (4 hunks)
  • packages/fluent-editor/src/modules/custom-image/image-bar.ts (1 hunks)
🔇 Additional comments (7)
packages/fluent-editor/src/modules/custom-image/image-bar.ts (1)

7-20: Consider adding a guard for initialization consistency.

You might want to validate that the passed target is indeed an <img> element. This helps avoid errors in cases where the context menu is triggered on a non-image element.

packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts (3)

7-7: Inline import is acceptable.

The import for ImageBar looks good. It’s straightforward and aligned with standard ES6 practices.


44-44: Great approach for context menu integration.

Using an event listener on contextmenu ensures minimal intrusion into other editor functionality. Good job.


164-179: Validate presence of closest method.

Element.closest is not supported in some older browsers, so if there's a need for IE11 or lower support, consider a polyfill.

packages/fluent-editor/src/assets/style.scss (1)

22-22: Confirm that the import path matches repo structure.

Ensure @import './imageBar'; does not conflict with names or paths in different modules.

packages/fluent-editor/src/assets/imageBar.scss (2)

1-16: Good use of CSS variables for theming.

Leveraging var(--devui-base-bg, #fff) etc. offers flexibility for theming. Nice approach to dynamic styling support.


127-130: Coordinate hover styling with the design system.

If there's a design standard or pattern library, confirm that the hover color #aaaaaf or the variable used is consistent throughout.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/fluent-editor/src/modules/custom-image/image-bar.ts (3)

11-17: Move SVG content to a separate asset file

The SVG content should be moved to a separate asset file and imported, improving maintainability and reusability.

Consider creating an assets/icons directory and moving the SVG there, then importing it:

import copyIcon from '../assets/icons/copy-icon.svg'

this.template = [
  `<a class="ql-image-download"><i class="icon-download"></i></a>`,
  `<a class="ql-image-copy">${copyIcon}</a>`,
].join('')

89-99: Improve positioning logic

  1. Extract magic numbers into named constants
  2. Add window resize handling
  3. Consider removing IE11 compatibility if not required
+ private static readonly TOOLBAR_WIDTH = 92
+
+ private handleResize = () => {
+   this.setPosition()
+ }
+
  setPosition() {
    if (this.domNode && this.image) {
      const parent = this.quill.root.parentNode
      const containerRect = parent.getBoundingClientRect()
      const imageRect = this.image.getBoundingClientRect()
      this.css(this.domNode, {
-       left: `${imageRect.left + imageRect.width - containerRect.left - 92}px`,
+       left: `${imageRect.left + imageRect.width - containerRect.left - ImageBar.TOOLBAR_WIDTH}px`,
        top: `${imageRect.top - containerRect.top}px`,
      })
    }
  }
+
+ createImageBar() {
+   // ... existing code ...
+   window.addEventListener('resize', this.handleResize)
+ }
+
+ destroy() {
+   // ... existing code ...
+   window.removeEventListener('resize', this.handleResize)
+ }

101-117: Consider modernizing the CSS application method

The current implementation includes IE11 compatibility code. Consider using modern approaches if IE11 support is not required.

- css(domNode, rules) {
-   if (typeof rules === 'object') {
-     for (const prop in rules) {
-       if (prop) {
-         if (Array.isArray(rules[prop])) {
-           // 兼容IE11浏览器
-           rules[prop].forEach((val) => {
-             domNode.style[prop] = val
-           })
-         }
-         else {
-           domNode.style[prop] = rules[prop]
-         }
-       }
-     }
-   }
- }
+ css(domNode: HTMLElement, rules: Record<string, string>) {
+   Object.assign(domNode.style, rules)
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ccb52a and 644adf1.

📒 Files selected for processing (1)
  • packages/fluent-editor/src/modules/custom-image/image-bar.ts (1 hunks)

Comment on lines +45 to +53
destroy() {
if (this.domNode) {
this.domNode.remove()
this.domNode = null
this.image = null
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve cleanup process

The destroy method should remove event listeners and handle edge cases more robustly.

+ private handleDownload: (event: MouseEvent) => void
+ private handleCopy: (event: MouseEvent) => void
+
  destroy() {
    if (this.domNode) {
+     const imageDownload = this.domNode.querySelector('a.ql-image-download')
+     const imageCopy = this.domNode.querySelector('a.ql-image-copy')
+     
+     if (imageDownload && this.handleDownload) {
+       imageDownload.removeEventListener('click', this.handleDownload)
+     }
+     if (imageCopy && this.handleCopy) {
+       imageCopy.removeEventListener('click', this.handleCopy)
+     }
+
      this.domNode.remove()
      this.domNode = null
      this.image = null
    }
  }

Committable suggestion skipped: line range outside the PR's diff.

@qwangry qwangry force-pushed the wangry/feat-image branch from b356cf5 to 1e56bf0 Compare January 5, 2025 08:40
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/fluent-editor/src/modules/custom-image/image-bar.ts (2)

13-19: Move SVG to assets and clean up commented code.

  1. The SVG content should be moved to a separate assets file for better maintainability.
  2. Remove the commented preview functionality if it's not planned for implementation.

99-127: Improve positioning logic.

  1. The hard-coded value 92 in positioning calculation should be derived from actual element dimensions.
  2. Add window resize event handling to update the position when the window is resized.
 setPosition() {
   if (this.domNode && this.image) {
+    const updatePosition = () => {
       const parent = this.quill.root.parentNode
       const containerRect = parent.getBoundingClientRect()
       const imageRect = this.image.getBoundingClientRect()
+      const barWidth = this.domNode.offsetWidth
       this.css(this.domNode, {
-        left: `${imageRect.left + imageRect.width - containerRect.left - 92}px`,
+        left: `${imageRect.left + imageRect.width - containerRect.left - barWidth}px`,
         top: `${imageRect.top - containerRect.top}px`,
       })
+    }
+    updatePosition()
+    window.addEventListener('resize', updatePosition)
+    // Clean up the event listener in destroy method
   }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 644adf1 and 1e56bf0.

📒 Files selected for processing (4)
  • packages/fluent-editor/src/assets/imageBar.scss (1 hunks)
  • packages/fluent-editor/src/assets/style.scss (1 hunks)
  • packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts (4 hunks)
  • packages/fluent-editor/src/modules/custom-image/image-bar.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/fluent-editor/src/assets/style.scss
  • packages/fluent-editor/src/assets/imageBar.scss
🔇 Additional comments (6)
packages/fluent-editor/src/modules/custom-image/image-bar.ts (4)

1-8: LGTM! Well-structured class with proper TypeScript types.

The class properties are properly typed, making the code type-safe and maintainable.


24-45: Clean up event listeners and translate comments to English.

Previous review comments about event listener cleanup and English comments are still applicable.


47-53: Improve cleanup process.

Previous review comments about improving the cleanup process are still applicable.


55-97: Improve image operations implementation and security.

Previous review comments about improving image operations implementation are still applicable.

Let's verify the image sources that can be handled:

#!/bin/bash
# Search for image source patterns in the codebase
rg -g '!*.{jpg,png,svg}' -A 2 'src\s*=|<img.*src=|imageUrl\s*='
packages/fluent-editor/src/modules/custom-image/BlotFormatter.ts (2)

7-7: LGTM! Clean import and property declaration.

The ImageBar integration is properly set up with correct typing.

Also applies to: 22-22


158-162: LGTM! Proper cleanup of ImageBar instance.

The click handler correctly destroys and nullifies the ImageBar instance.

Comment on lines +164 to +179
onContextmenu = (event) => {
this.hide()
event.preventDefault()
const target = event.target
const imageDom = target.closest('img')

if (imageDom) {
if (this.imageBar) {
this.imageBar.destroy()
}
this.imageBar = new ImageBar(this.quill, imageDom)
}
else if (this.imageBar && !target.closest('.ql-image-bar')) {
this.imageBar.destroy()
this.imageBar = null
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for edge cases.

The context menu handler should include error handling for potential edge cases:

  1. Invalid target element
  2. ImageBar creation failure
 onContextmenu = (event) => {
   this.hide()
   event.preventDefault()
-  const target = event.target
-  const imageDom = target.closest('img')
+  try {
+    const target = event.target
+    if (!target || !(target instanceof Element)) {
+      return
+    }
+    const imageDom = target.closest('img')

     if (imageDom) {
       if (this.imageBar) {
         this.imageBar.destroy()
       }
-      this.imageBar = new ImageBar(this.quill, imageDom)
+      try {
+        this.imageBar = new ImageBar(this.quill, imageDom)
+      } catch (error) {
+        console.error('Failed to create ImageBar:', error)
+      }
     }
     else if (this.imageBar && !target.closest('.ql-image-bar')) {
       this.imageBar.destroy()
       this.imageBar = null
     }
+  } catch (error) {
+    console.error('Error handling context menu:', error)
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onContextmenu = (event) => {
this.hide()
event.preventDefault()
const target = event.target
const imageDom = target.closest('img')
if (imageDom) {
if (this.imageBar) {
this.imageBar.destroy()
}
this.imageBar = new ImageBar(this.quill, imageDom)
}
else if (this.imageBar && !target.closest('.ql-image-bar')) {
this.imageBar.destroy()
this.imageBar = null
}
onContextmenu = (event) => {
this.hide()
event.preventDefault()
try {
const target = event.target
if (!target || !(target instanceof Element)) {
return
}
const imageDom = target.closest('img')
if (imageDom) {
if (this.imageBar) {
this.imageBar.destroy()
}
try {
this.imageBar = new ImageBar(this.quill, imageDom)
} catch (error) {
console.error('Failed to create ImageBar:', error)
}
}
else if (this.imageBar && !target.closest('.ql-image-bar')) {
this.imageBar.destroy()
this.imageBar = null
}
} catch (error) {
console.error('Error handling context menu:', error)
}
}

@@ -39,6 +41,7 @@
// disable native image resizing on firefox
document.execCommand('enableObjectResizing', false, 'false') // eslint-disable-next-line-line no-undef
this.quill.root.addEventListener('click', this.onClick)
this.quill.root.addEventListener('contextmenu', event => this.onContextmenu(event))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add event listener cleanup.

The context menu event listener should be cleaned up when the BlotFormatter is destroyed.

 constructor(quill: any, options: any = {}) {
   // ... existing code ...
   this.quill.root.addEventListener('click', this.onClick)
-  this.quill.root.addEventListener('contextmenu', event => this.onContextmenu(event))
+  this.onContextmenu = this.onContextmenu.bind(this)
+  this.quill.root.addEventListener('contextmenu', this.onContextmenu)
   // ... existing code ...
 }

+destroy() {
+  this.quill.root.removeEventListener('contextmenu', this.onContextmenu)
+  if (this.imageBar) {
+    this.imageBar.destroy()
+    this.imageBar = null
+  }
+}

Committable suggestion skipped: line range outside the PR's diff.

@kagol kagol added the enhancement New feature or request label Jan 5, 2025
@kagol
Copy link
Member

kagol commented Jan 10, 2025

@qwangry 放在右键菜单里有点太隐藏了,建议 hover 时显示,这个工具栏后续可以扩展,比如做大图预览、删除图片等,不过可以后续优化,我先合入。

@kagol kagol merged commit b424b90 into opentiny:main Jan 10, 2025
2 checks passed
@kagol
Copy link
Member

kagol commented Jan 10, 2025

@qwangry 下次提交 PR 可以在 PR 描述里的 issue 号前面加上 clsoe,这样 PR 合入之后,issue 会自动关闭。

Issue Number: #124
=>
Issue Number: close #124

@qwangry
Copy link
Contributor Author

qwangry commented Jan 10, 2025

@qwangry 下次提交 PR 可以在 PR 描述里的 issue 号前面加上 clsoe,这样 PR 合入之后,issue 会自动关闭。

Issue Number: #124
=>
Issue Number: close #124

好的,get了

@qwangry
Copy link
Contributor Author

qwangry commented Jan 10, 2025

@qwangry 放在右键菜单里有点太隐藏了,建议 hover 时显示,这个工具栏后续可以扩展,比如做大图预览、删除图片等,不过可以后续优化,我先合入。

@kagol 那个图标库可以更新吗(就是在css中设置content显示图标那个文件),我看目前只有file-bar的下载和删除图标,感觉可以加上复制和预览图标,这样比较统一

@kagol
Copy link
Member

kagol commented Jan 10, 2025

@qwangry 放在右键菜单里有点太隐藏了,建议 hover 时显示,这个工具栏后续可以扩展,比如做大图预览、删除图片等,不过可以后续优化,我先合入。

@kagol 那个图标库可以更新吗(就是在css中设置content显示图标那个文件),我看目前只有file-bar的下载和删除图标,感觉可以加上复制和预览图标,这样比较统一

那个是 iconfont,扩展性不好,后续改成基于 svg 的图标库。你先用 svg 没问题的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request v3
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants