From 83270794b35baa20dc2ffbc9ce9615adf62b95f2 Mon Sep 17 00:00:00 2001
From: hunghg255
Date: Sat, 9 Nov 2024 21:56:11 +0700
Subject: [PATCH] feat: flip image
---
docs/guide/bubble-menu.md | 1 +
playground/src/App.tsx | 2 +-
src/components/icons/icons.ts | 4 ++
src/components/menus/bubble.ts | 42 ++++++++++++++++++-
src/extensions/Image/Image.ts | 34 ++++++++++++---
src/extensions/Image/components/ImageView.tsx | 11 ++++-
src/locales/en.ts | 2 +
src/locales/pt-br.ts | 2 +
src/locales/vi.ts | 2 +
src/locales/zh-cn.ts | 2 +
src/styles/editor.scss | 11 +++--
11 files changed, 99 insertions(+), 14 deletions(-)
diff --git a/docs/guide/bubble-menu.md b/docs/guide/bubble-menu.md
index 33984be..52d4354 100644
--- a/docs/guide/bubble-menu.md
+++ b/docs/guide/bubble-menu.md
@@ -30,6 +30,7 @@ The system provides the following default bubble menus:
| ContentMenu | Provides general content-related operations like copy, paste, delete, etc. | floatingMenuConfig |
| BubbleMenuImageGif | Provides general content-related operations like copy, paste, delete, image gif etc. | imageGifConfig |
| BubbleMenuMermaid | Provides general content-related operations like copy, paste, delete, mermaid etc. | mermaidConfig |
+| BubbleMenuTwitter | Provides general content-related operations like copy, paste, delete, twitter etc. | twitterConfig |
## Disabling the Bubble Menu
diff --git a/playground/src/App.tsx b/playground/src/App.tsx
index a0a834d..6b3e470 100644
--- a/playground/src/App.tsx
+++ b/playground/src/App.tsx
@@ -177,7 +177,7 @@ const extensions = [
Twitter,
]
-const DEFAULT = ``
+const DEFAULT = `
`
function debounce(func: any, wait: number) {
let timeout: NodeJS.Timeout
diff --git a/src/components/icons/icons.ts b/src/components/icons/icons.ts
index 1eadd01..ddca778 100644
--- a/src/components/icons/icons.ts
+++ b/src/components/icons/icons.ts
@@ -18,6 +18,8 @@ import {
CropIcon,
Eraser,
Eye,
+ FlipHorizontal,
+ FlipVertical,
Frame,
GripVertical,
Heading1,
@@ -201,4 +203,6 @@ export const icons = {
Crop: CropIcon,
Mermaid,
Twitter,
+ FlipX: FlipVertical,
+ FlipY: FlipHorizontal,
} as any
diff --git a/src/components/menus/bubble.ts b/src/components/menus/bubble.ts
index 4830235..43b1f91 100644
--- a/src/components/menus/bubble.ts
+++ b/src/components/menus/bubble.ts
@@ -106,7 +106,6 @@ function imageGifSizeMenus(editor: Editor): BubbleMenuItem[] {
componentProps: {
tooltip: localeActions.t(`editor.${size.replace('-', '.')}.tooltip` as any),
icon: icons[i],
- // @ts-expect-error
action: () => editor.commands.updateImageGif({ width: IMAGE_SIZE[size] }),
isActive: () => editor.isActive('image', { width: IMAGE_SIZE[size] }),
},
@@ -146,7 +145,6 @@ function imageGifAlignMenus(editor: Editor): BubbleMenuItem[] {
componentProps: {
tooltip: localeActions.t(`editor.textalign.${k}.tooltip`),
icon: iconMap[k],
- // @ts-expect-error
action: () => editor.commands?.setAlignImageGif?.(k),
isActive: () => editor.isActive({ align: k }) || false,
disabled: false,
@@ -195,6 +193,46 @@ function videoSizeMenus(editor: Editor): BubbleMenuItem[] {
}
export function getBubbleImage(editor: Editor): BubbleMenuItem[] {
return [
+ {
+ type: 'flipX',
+ component: ActionButton,
+ componentProps: {
+ editor,
+ tooltip: localeActions.t('editor.tooltip.flipX'),
+ icon: 'FlipX',
+ action: () => {
+ const image = editor.getAttributes('image')
+ const { flipX } = image as any
+ editor
+ .chain()
+ .focus(undefined, { scrollIntoView: false })
+ .updateImage({
+ flipX: !flipX,
+ })
+ .run()
+ },
+ },
+ },
+ {
+ type: 'flipY',
+ component: ActionButton,
+ componentProps: {
+ editor,
+ tooltip: localeActions.t('editor.tooltip.flipY'),
+ icon: 'FlipY',
+ action: () => {
+ const image = editor.getAttributes('image')
+ const { flipY } = image as any
+ editor
+ .chain()
+ .focus(undefined, { scrollIntoView: false })
+ .updateImage({
+ flipY: !flipY,
+ })
+ .run()
+ },
+ },
+ },
...imageSizeMenus(editor),
...imageAlignMenus(editor),
{
diff --git a/src/extensions/Image/Image.ts b/src/extensions/Image/Image.ts
index d00f287..968afbc 100644
--- a/src/extensions/Image/Image.ts
+++ b/src/extensions/Image/Image.ts
@@ -19,8 +19,12 @@ export interface SetImageAttrsOptions {
width?: number | string | null
/** The alignment of the image. */
align?: 'left' | 'center' | 'right'
-
+ /** Whether the image is inline. */
inline?: boolean
+ /** image FlipX */
+ flipX?: boolean
+ /** image FlipY */
+ flipY?: boolean
}
const DEFAULT_OPTIONS: any = {
@@ -94,6 +98,12 @@ export const Image = TiptapImage.extend({
addAttributes() {
return {
...this.parent?.(),
+ flipX: {
+ default: false,
+ },
+ flipY: {
+ default: false,
+ },
width: {
default: null,
parseHTML: (element) => {
@@ -152,22 +162,25 @@ export const Image = TiptapImage.extend({
}
},
renderHTML({ HTMLAttributes }) {
- const { align, inline } = HTMLAttributes
+ const { flipX, flipY, align, inline } = HTMLAttributes
+
+ const transformStyle
+ = flipX || flipY ? `transform: rotateX(${flipX ? '180' : '0'}deg) rotateY(${flipY ? '180' : '0'}deg);` : ''
- const style = align ? `text-align: ${align};` : ''
+ const textAlignStyle = align ? `text-align: ${align};` : ''
return [
- inline ? 'span' : 'div', // Parent element
+ inline ? 'span' : 'div',
{
- style,
+ style: textAlignStyle,
class: 'image',
},
[
'img',
mergeAttributes(
- // Always render the `height="auto"`
{
height: 'auto',
+ style: transformStyle,
},
this.options.HTMLAttributes,
HTMLAttributes,
@@ -184,6 +197,9 @@ export const Image = TiptapImage.extend({
const width = img?.getAttribute('width')
+ const flipX = img?.getAttribute('flipx') || false
+ const flipY = img?.getAttribute('flipy') || false
+
return {
src: img?.getAttribute('src'),
alt: img?.getAttribute('alt'),
@@ -191,6 +207,8 @@ export const Image = TiptapImage.extend({
width: width ? Number.parseInt(width as string, 10) : null,
align: img?.getAttribute('align') || element?.style?.textAlign || null,
inline: img?.getAttribute('inline') || false,
+ flipX: flipX === 'true',
+ flipY: flipY === 'true',
}
},
},
@@ -200,6 +218,8 @@ export const Image = TiptapImage.extend({
const img = element.querySelector('img')
const width = img?.getAttribute('width')
+ const flipX = img?.getAttribute('flipx') || false
+ const flipY = img?.getAttribute('flipy') || false
return {
src: img?.getAttribute('src'),
@@ -208,6 +228,8 @@ export const Image = TiptapImage.extend({
width: width ? Number.parseInt(width as string, 10) : null,
align: img?.getAttribute('align') || element.style.textAlign || null,
inline: img?.getAttribute('inline') || false,
+ flipX: flipX === 'true',
+ flipY: flipY === 'true',
}
},
},
diff --git a/src/extensions/Image/components/ImageView.tsx b/src/extensions/Image/components/ImageView.tsx
index ac9abb4..abfdc43 100644
--- a/src/extensions/Image/components/ImageView.tsx
+++ b/src/extensions/Image/components/ImageView.tsx
@@ -49,16 +49,25 @@ function ImageView(props: any) {
const { align, inline } = props?.node?.attrs
const imgAttrs = useMemo(() => {
- const { src, alt, width: w, height: h } = props?.node?.attrs
+ const { src, alt, width: w, height: h, flipX, flipY } = props?.node?.attrs
const width = isNumber(w) ? `${w}px` : w
const height = isNumber(h) ? `${h}px` : h
+ const transformStyles: any = []
+
+ if (flipX)
+ transformStyles.push('rotateX(180deg)')
+ if (flipY)
+ transformStyles.push('rotateY(180deg)')
+ const transform = transformStyles.join(' ')
+
return {
src: src || undefined,
alt: alt || undefined,
style: {
width: width || undefined,
height: height || undefined,
+ transform: transform || 'none',
},
}
}, [props?.node?.attrs])
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 3077fe6..dad44d4 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -163,6 +163,8 @@ const locale = {
'editor.replace.caseSensitive': 'Case Sensitive',
'editor.mermaid.tooltip': 'Mermaid',
'editor.twitter.tooltip': 'Twitter',
+ 'editor.tooltip.flipX': 'Flip Horizontal',
+ 'editor.tooltip.flipY': 'Flip Vertical',
}
export default locale
diff --git a/src/locales/pt-br.ts b/src/locales/pt-br.ts
index f86297f..cc0a91a 100644
--- a/src/locales/pt-br.ts
+++ b/src/locales/pt-br.ts
@@ -164,6 +164,8 @@ const locale = {
'editor.replace.caseSensitive': 'Sensível a maiúsculas e minúsculas',
'editor.mermaid.tooltip': 'Mermaid',
'editor.twitter.tooltip': 'Twitter',
+ 'editor.tooltip.flipX': 'Inverter Horizontal',
+ 'editor.tooltip.flipY': 'Inverter Vertical',
}
export default locale
diff --git a/src/locales/vi.ts b/src/locales/vi.ts
index 978561a..08ce501 100644
--- a/src/locales/vi.ts
+++ b/src/locales/vi.ts
@@ -164,6 +164,8 @@ const locale = {
'editor.replace.caseSensitive': 'Phân biệt chữ hoa chữ thường',
'editor.mermaid.tooltip': 'Mermaid',
'editor.twitter.tooltip': 'Twitter',
+ 'editor.tooltip.flipX': 'Lật Ngang',
+ 'editor.tooltip.flipY': 'Lật Dọc',
}
export default locale
diff --git a/src/locales/zh-cn.ts b/src/locales/zh-cn.ts
index d9be739..32e9993 100644
--- a/src/locales/zh-cn.ts
+++ b/src/locales/zh-cn.ts
@@ -164,6 +164,8 @@ const locale = {
'editor.replace.caseSensitive': '区分大小写',
'editor.mermaid.tooltip': 'Mermaid',
'editor.twitter.tooltip': 'Twitter',
+ 'editor.tooltip.flipX': '水平翻转',
+ 'editor.tooltip.flipY': '垂直翻转',
}
export default locale
diff --git a/src/styles/editor.scss b/src/styles/editor.scss
index f14aa3a..04307fe 100644
--- a/src/styles/editor.scss
+++ b/src/styles/editor.scss
@@ -146,7 +146,7 @@
&--focused:hover,
&--resizing:hover {
- outline-color: transparent;
+ outline-color: hsl(var(--primary));
}
&__placeholder {
@@ -164,13 +164,16 @@
}
}
+ .image-view__body--focused {
+ outline-color: hsl(var(--primary)) !important;
+ }
+
&.focus {
img {
@apply richtext-outline-primary richtext-outline-2 richtext-outline;
}
}
-
img {
display: inline;
vertical-align: baseline;
@@ -188,7 +191,7 @@
height: 100%;
@apply richtext-border !important;
@apply richtext-border-border !important;
- border-style: dashed;
+
&__handler {
position: absolute;
@@ -199,7 +202,7 @@
height: 12px;
border: 1px solid #fff;
border-radius: 2px;
- @apply richtext-bg-blue-500;
+ background-color: hsl(var(--primary));
&--tl {
top: -6px;