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;