forked from opentiny/tiny-vue
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:
parent
ec9407697c
commit
e4199366ed
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue