feat(rich-text-editor): [rich-text-editor] add image drag adjustment (#1504)

* feat(rich-text-editor): [rich-text-editor] add image drag adjustment to rich-text-editor

* fix(rich-text-editor/image-view): change script to uniform writing rules

* fix(rich-text-editor/image-view): initialize image size to 400 and optimize insertion of  images
This commit is contained in:
星河 2024-03-30 10:16:41 +08:00 committed by GitHub
parent ec9407697c
commit e4199366ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 380 additions and 15 deletions

View File

@ -633,4 +633,59 @@
border-radius: 8px;
}
}
.tiny-image {
&__node__view {
display: inline-block;
margin: 0 5px;
}
&__view {
position: relative;
box-sizing: border-box;
border: 1px solid transparent;
display: inline-block;
max-width: 100%;
}
&__handle {
display: inline-block;
width: 10px;
height: 10px;
background: var(--ti-base-color-brand-7);
position: absolute;
}
&__resize {
border: 2px solid var(--ti-base-color-brand-7);
left: 0;
position: absolute;
top: 0;
z-index: 1;
&__tl {
left: -5px;
top: -5px;
cursor: nwse-resize;
}
&__tr {
right: -5px;
top: -5px;
cursor: nesw-resize;
}
&__bl {
left: -5px;
bottom: -5px;
cursor: nesw-resize;
}
&__br {
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
}
}
}

View File

@ -205,8 +205,8 @@
}
.isActive {
background-color: rgba(32, 122, 183, 0.5);
border-color: rgba(32, 122, 183, 0.5);
background-color: rgb(32 122 183 / 50%);
border-color: rgb(32 122 183 / 50%);
}
}
}
@ -274,9 +274,8 @@
input {
position: absolute;
width: 100%;
height: 100%;
left: 0px;
left: 0;
opacity: 0;
width: 0;
}
@ -286,15 +285,15 @@
.line-height-button {
position: relative;
.line-height-options {
position: absolute;
padding: 0.15rem;
background-color: var(--ti-rich-text-editor-options-bg-color);
left: 0;
display: none;
border-radius: var(--ti-rich-text-editor-options-border-radius);
box-shadow: var(--ti-rich-text-editor-options-box-shadow);
z-index: 999;
.line-height-options {
position: absolute;
padding: 0.15rem;
background-color: var(--ti-rich-text-editor-options-bg-color);
left: 0;
display: none;
border-radius: var(--ti-rich-text-editor-options-border-radius);
box-shadow: var(--ti-rich-text-editor-options-box-shadow);
z-index: 999;
button {
color: black;
@ -419,7 +418,6 @@
img {
max-width: 100%;
height: auto;
}
blockquote {
@ -636,4 +634,59 @@
border-radius: 8px;
}
}
.tiny-image {
&__node__view {
display: inline-block;
margin: 0 5px;
}
&__view {
position: relative;
box-sizing: border-box;
border: 1px solid transparent;
display: inline-block;
max-width: 100%;
}
&__handle {
display: inline-block;
width: 10px;
height: 10px;
background: var(--ti-base-color-brand-7);
position: absolute;
}
&__resize {
border: 2px solid var(--ti-base-color-brand-7);
left: 0;
position: absolute;
top: 0;
z-index: 1;
&__tl {
left: -5px;
top: -5px;
cursor: nwse-resize;
}
&__tr {
right: -5px;
top: -5px;
cursor: nesw-resize;
}
&__bl {
left: -5px;
bottom: -5px;
cursor: nesw-resize;
}
&__br {
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
}
}
}

View File

@ -0,0 +1,55 @@
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import TiptapImage from '@tiptap/extension-image'
import ImageView from '../image-view.vue'
const Image = TiptapImage.extend({
inline() {
return true
},
group() {
return 'inline'
},
addAttributes() {
return {
...this.parent?.(),
width: {
default: 500,
parseHTML(element) {
const width = element.style.width || element.getAttribute('width')
return width
},
renderHTML: (attributes) => {
const { width } = attributes
return {
width
}
}
},
height: {
default: 500,
parseHTML(element) {
const height = element.style.height || element.getAttribute('height')
return height
},
renderHTML: (attributes) => {
const { height } = attributes
return {
height
}
}
}
}
},
addNodeView() {
return VueNodeViewRenderer(ImageView)
},
parseHTML() {
return [
{
tag: 'img[src]'
}
]
}
})
export default Image

View File

@ -0,0 +1,201 @@
<template>
<NodeViewWrapper class="tiny-image__node__view" :node="node">
<div class="tiny-image__view">
<img
class="image-view-content"
:src="src"
:alt="alt"
:title="title"
:width="width"
:height="height"
@click="handleSelectImg"
ref="imgRef"
/>
<div
class="tiny-image__resize"
v-show="selectedVal"
v-if="editor.isEditable"
:style="{
width: resizerState.w + 'px',
height: resizerState.h + 'px',
left: resizerState.x + 'px',
top: resizerState.y + 'px'
}"
>
<span
v-for="direction in resizeDirection"
:key="direction"
:class="'tiny-image__resize__' + direction"
class="tiny-image__handle"
@mousedown="handleMouseDown($event, direction)"
></span>
</div>
</div>
</NodeViewWrapper>
</template>
<script lang="ts">
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3'
import { defineComponent, hooks } from '@opentiny/vue-common'
export default defineComponent({
name: 'ImageView',
props: nodeViewProps,
components: {
NodeViewWrapper
},
setup(props) {
const enum ResizeDirection {
TOP_LEFT = 'tl',
TOP_RIGHT = 'tr',
BOTTOM_LEFT = 'bl',
BOTTOM_RIGHT = 'br'
}
interface ImageSize {
minWidth: number
minHeight: number
maxWidth: number
maxHeight: number
}
const { node, editor, getPos, updateAttributes } = props
const resizeDirection = [
ResizeDirection.TOP_LEFT,
ResizeDirection.TOP_RIGHT,
ResizeDirection.BOTTOM_LEFT,
ResizeDirection.BOTTOM_RIGHT
]
const resizerState = hooks.reactive({
x: 0,
y: 0,
w: 0,
h: 0,
dir: ''
})
const imageSize = hooks.ref<ImageSize>({
minWidth: 50,
minHeight: 50,
maxWidth: 1000,
maxHeight: 10000
})
const width = hooks.computed<number>(() => props.node.attrs.width)
const height = hooks.computed<number>(() => props.node.attrs.height)
const src = hooks.computed<string>(() => props.node.attrs.src)
const title = hooks.computed<string>(() => props.node.attrs.title)
const alt = hooks.computed<string>(() => props.node.attrs.alt)
const handleSelectImg = () => {
editor.commands.setNodeSelection(getPos())
}
const selectedVal = hooks.computed(() => props.selected || isResizing.value)
const initResizerState = () => {
const { width, height } = node.attrs
const maxWidth = editor.view.dom.getBoundingClientRect().width
resizerState.w = width
resizerState.h = height
imageSize.value.maxWidth = Math.floor(maxWidth)
}
let isResizing = hooks.ref(false)
const imgRef = hooks.ref<HTMLImageElement | null>(null)
const lastX = hooks.ref(0)
const lastY = hooks.ref(0)
const handleMouseDown = (e: MouseEvent, direction: ResizeDirection) => {
e.stopPropagation()
e.preventDefault()
resizerState.dir = direction
lastX.value = e.clientX
lastY.value = e.clientY
isResizing.value = true
window.addEventListener('mousemove', handleMouseMove, true)
window.addEventListener('mouseup', handleMouseUp, true)
}
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.value || !imgRef.value) return
e.preventDefault()
e.stopPropagation()
const x = e.clientX
const y = e.clientY
const dx = x - lastX.value
const dy = y - lastY.value
let newWidth = resizerState.w + (resizerState.dir.includes('r') ? dx : -dx)
let newHeight = resizerState.h + (resizerState.dir.includes('b') ? dy : -dy)
newWidth = Math.max(imageSize.value.minWidth, Math.min(newWidth, imageSize.value.maxWidth))
newHeight = Math.max(imageSize.value.minHeight, Math.min(newHeight, imageSize.value.maxHeight))
resizerState.w = newWidth
resizerState.h = newHeight
lastX.value = x
lastY.value = y
updateAttributes({
width: newWidth,
height: newHeight
})
}
const handleMouseUp = () => {
isResizing.value = false
handleSelectImg()
window.removeEventListener('mousemove', handleMouseMove, true)
window.removeEventListener('mouseup', handleMouseUp, true)
}
const resizeObserver = new ResizeObserver((target) => {
const { width } = target[0].contentRect
imageSize.value.maxWidth = Math.floor(width - 20)
changeSize()
})
const changeSize = () => {
if (imageSize.value.maxWidth <= width.value) {
updateAttributes({
width: imageSize.value.maxWidth
})
resizerState.w = imageSize.value.maxWidth
}
}
hooks.onMounted(() => {
initResizerState()
resizeObserver.observe(editor.view.dom)
})
hooks.onBeforeUnmount(() => {
resizeObserver.disconnect()
})
return {
imgRef,
isResizing,
resizerState,
handleMouseDown,
handleMouseMove,
handleMouseUp,
node,
editor,
resizeDirection,
handleSelectImg,
selectedVal,
width,
height,
src,
title,
alt
}
}
})
</script>

View File

@ -311,7 +311,8 @@ import Paragraph from '@tiptap/extension-paragraph'
import { mergeAttributes } from '@tiptap/core'
// image
import Image from '@tiptap/extension-image'
// import Image from '@tiptap/extension-image'
import Image from './extensions/image'
// -- HeighLight
import Highlight from '@tiptap/extension-highlight'