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: flip image #108

Merged
merged 3 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guide/bubble-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
"homepage": "https://reactjs-tiptap-editor.vercel.app/",
"repository": {
"type": "git",
"url": "https://github.com/hunghg255/reactjs-tiptap-editor.git"
"url": "git+https://github.com/hunghg255/reactjs-tiptap-editor.git"
},
"bugs": "https://github.com/hunghg255/reactjs-tiptap-editor/issues"
}
2 changes: 1 addition & 1 deletion playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const extensions = [
Twitter,
]

const DEFAULT = ``
const DEFAULT = `<p dir="auto"></p><p dir="auto"></p><p dir="auto"></p><p dir="auto"><div style="text-align: center;" class="image"><img height="auto" style="transform: rotateX(0deg) rotateY(180deg);" src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729198819038/684c0adb-b189-4af8-b9d8-d26e4097ce27.png?auto=compress,format&amp;format=webp" flipx="false" flipy="true" align="center" inline="false"></div></p><p dir="auto"></p><p dir="auto"></p><p dir="auto"></p>`

function debounce(func: any, wait: number) {
let timeout: NodeJS.Timeout
Expand Down
935 changes: 594 additions & 341 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/components/icons/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
CropIcon,
Eraser,
Eye,
FlipHorizontal,
FlipVertical,
Frame,
GripVertical,
Heading1,
Expand Down Expand Up @@ -201,4 +203,6 @@ export const icons = {
Crop: CropIcon,
Mermaid,
Twitter,
FlipX: FlipVertical,
FlipY: FlipHorizontal,
} as any
42 changes: 40 additions & 2 deletions src/components/menus/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] }),
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
{
Expand Down
34 changes: 28 additions & 6 deletions src/extensions/Image/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -94,6 +98,12 @@ export const Image = TiptapImage.extend<IImageOptions>({
addAttributes() {
return {
...this.parent?.(),
flipX: {
default: false,
},
flipY: {
default: false,
},
width: {
default: null,
parseHTML: (element) => {
Expand Down Expand Up @@ -152,22 +162,25 @@ export const Image = TiptapImage.extend<IImageOptions>({
}
},
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,
Expand All @@ -184,13 +197,18 @@ export const Image = TiptapImage.extend<IImageOptions>({

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'),
caption: img?.getAttribute('caption'),
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',
}
},
},
Expand All @@ -200,6 +218,8 @@ export const Image = TiptapImage.extend<IImageOptions>({
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'),
Expand All @@ -208,6 +228,8 @@ export const Image = TiptapImage.extend<IImageOptions>({
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',
}
},
},
Expand Down
11 changes: 10 additions & 1 deletion src/extensions/Image/components/ImageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/locales/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/locales/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/locales/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 7 additions & 4 deletions src/styles/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@

&--focused:hover,
&--resizing:hover {
outline-color: transparent;
outline-color: hsl(var(--primary));
}

&__placeholder {
Expand All @@ -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;
Expand All @@ -188,7 +191,7 @@
height: 100%;
@apply richtext-border !important;
@apply richtext-border-border !important;
border-style: dashed;


&__handler {
position: absolute;
Expand All @@ -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;
Expand Down
Loading