Commit 1f83ff59 by Яков

update

parent 98ec316b
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.1.10", "version": "1.1.11",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -1147,6 +1147,7 @@ const QEditor = ({ ...@@ -1147,6 +1147,7 @@ const QEditor = ({
src: file.path, src: file.path,
width: Math.round(realWidth), width: Math.round(realWidth),
height: Math.round(realHeight), height: Math.round(realHeight),
align: 'center',
points: [] points: []
}, },
}) })
......
...@@ -31,6 +31,48 @@ const Audio = Node.create({ ...@@ -31,6 +31,48 @@ const Audio = Node.create({
]; ];
}, },
addNodeView() {
return ({ editor, node }) => {
const container = document.createElement('div')
container.style.position = 'relative'
const audio = document.createElement('audio')
audio.src = node.attrs.src
audio.controls = true
const closeBtn = document.createElement('button')
closeBtn.textContent = '×'
closeBtn.className = 'audio-delete-btn'
closeBtn.style.cssText = `
position: absolute;
top: -6px;
right: -6px;
z-index: 10;
background: white;
border: 1px solid #ccc;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
line-height: 1;
cursor: pointer;
`
closeBtn.addEventListener('click', function () {
const pos = editor.view.posAtDOM(container, 0)
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
})
container.appendChild(closeBtn)
container.appendChild(audio)
return {
dom: container,
}
}
},
addCommands() { addCommands() {
return { return {
addVoiceMessage: (options) => ({ chain }) => { addVoiceMessage: (options) => ({ chain }) => {
......
...@@ -7,7 +7,7 @@ const Iframe = Node.create({ ...@@ -7,7 +7,7 @@ const Iframe = Node.create({
draggable: true, draggable: true,
atom: true, atom: true,
addAttributes() { addAttributes () {
return { return {
src: { src: {
default: null default: null
...@@ -24,7 +24,7 @@ const Iframe = Node.create({ ...@@ -24,7 +24,7 @@ const Iframe = Node.create({
} }
}, },
parseHTML() { parseHTML () {
return [ return [
{ {
tag: 'iframe' tag: 'iframe'
...@@ -32,12 +32,12 @@ const Iframe = Node.create({ ...@@ -32,12 +32,12 @@ const Iframe = Node.create({
] ]
}, },
renderHTML({ HTMLAttributes }) { renderHTML ({HTMLAttributes}) {
return ['iframe', mergeAttributes(HTMLAttributes)] return ['iframe', mergeAttributes(HTMLAttributes)]
}, },
addNodeView() { addNodeView () {
return ({ editor, node, ...a }) => { return ({editor, node, ...a}) => {
const container = document.createElement('div') const container = document.createElement('div')
const iframe = document.createElement('iframe') const iframe = document.createElement('iframe')
...@@ -48,8 +48,12 @@ const Iframe = Node.create({ ...@@ -48,8 +48,12 @@ const Iframe = Node.create({
const closeBtn = document.createElement('button') const closeBtn = document.createElement('button')
closeBtn.textContent = 'X' closeBtn.textContent = 'X'
closeBtn.classList.add('closeBtn') closeBtn.classList.add('closeBtn')
closeBtn.addEventListener('click', function () { closeBtn.addEventListener('click', function () {
container.remove() const pos = editor.view.posAtDOM(container, 0)
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}) })
// if (editor.isEditable) { // if (editor.isEditable) {
...@@ -64,12 +68,12 @@ const Iframe = Node.create({ ...@@ -64,12 +68,12 @@ const Iframe = Node.create({
} }
}, },
addCommands() { addCommands () {
return { return {
setIframe: setIframe:
(options) => (options) =>
({ tr, dispatch }) => { ({tr, dispatch}) => {
const { selection } = tr const {selection} = tr
const node = this.type.create(options) const node = this.type.create(options)
if (dispatch) { if (dispatch) {
......
...@@ -402,6 +402,58 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -402,6 +402,58 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
}} }}
/> />
<Button
size="default"
shape={'circle'}
type={node.attrs.alt?.length > 0 ? 'primary' : 'default'}
onClick={(e) => {
e.stopPropagation();
setTempAlt(node.attrs.alt || '');
setAltModalVisible(true);
}}
style={{
position: 'absolute',
top: 4,
right: '30px',
zIndex: 15,
}}
>
<FontSizeOutlined />
</Button>
{selected && (
<Button
type="text"
danger
size="small"
onClick={(e) => {
e.stopPropagation()
const pos = getPos?.()
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}
}}
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 30,
backgroundColor: 'white',
border: '1px solid #d9d9d9',
borderRadius: '50%',
width: 20,
height: 20,
fontSize: 12,
lineHeight: 1,
padding: '0px 0px 2px 0px',
cursor: 'pointer'
}}
>
×
</Button>
)}
{(selected || isResizing) && ( {(selected || isResizing) && (
<Fragment> <Fragment>
{['nw', 'ne', 'sw', 'se'].map(dir => ( {['nw', 'ne', 'sw', 'se'].map(dir => (
...@@ -479,25 +531,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -479,25 +531,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
> >
Align Align
</button> </button>
<Button
size="default"
shape={'circle'}
type={node.attrs.alt?.length > 0 ? 'primary' : 'default'}
onClick={(e) => {
e.stopPropagation();
setTempAlt(node.attrs.alt || '');
setAltModalVisible(true);
}}
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 15,
}}
>
<FontSizeOutlined />
</Button>
</Fragment> </Fragment>
)} )}
<Modal <Modal
......
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { TextSelection } from 'prosemirror-state'
// React компонент NodeView // React компонент NodeView
export const ToggleBlockComponent = ({ node, updateAttributes }) => { export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => {
const open = node.attrs.open const open = node.attrs.open
const title = node.attrs.title const title = node.attrs.title
...@@ -11,7 +12,7 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => { ...@@ -11,7 +12,7 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
const [inputWidth, setInputWidth] = useState('100px') const [inputWidth, setInputWidth] = useState('100px')
const toggle = () => { const toggle = () => {
updateAttributes({ open: !open }) updateAttributes({open: ! open})
} }
useEffect(() => { useEffect(() => {
...@@ -23,6 +24,40 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => { ...@@ -23,6 +24,40 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
return ( return (
<NodeViewWrapper className="toggle-block" data-open={open}> <NodeViewWrapper className="toggle-block" data-open={open}>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}
} catch (err) {
console.warn('Ошибка удаления toggleBlock:', err)
}
}}
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 10,
background: 'white',
border: '1px solid #ccc',
borderRadius: '50%',
width: 20,
height: 20,
fontSize: 12,
lineHeight: 1,
cursor: 'pointer',
padding: 0,
}}
>
×
</button>
<div className="toggle-block-inner"> <div className="toggle-block-inner">
<div className="toggle-header-wrapper"> <div className="toggle-header-wrapper">
<span <span
...@@ -40,17 +75,56 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => { ...@@ -40,17 +75,56 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
onChange={(e) => updateAttributes({ title: e.target.value })} onChange={(e) => updateAttributes({ title: e.target.value })}
placeholder="Заголовок..." placeholder="Заголовок..."
style={{ width: inputWidth }} style={{ width: inputWidth }}
onFocus={(e) => {
if (title.trim() === 'Заголовок') {
// выделить весь текст
setTimeout(() => e.target.select(), 0)
}
}}
/> />
</div> </div>
</div> </div>
<div <div
className="toggle-body" className="toggle-body"
data-collapsed={!open}
style={{ style={{
maxHeight: open ? '1000px' : '0', maxHeight: open ? '1000px' : '0',
}} }}
> >
<div className="toggle-body-wrapper"> <div
<NodeViewContent className="toggle-content" /> className="toggle-body-wrapper"
onClick={(e) => {
e.stopPropagation()
try {
const pos = getPos?.()
if (typeof pos !== 'number') return
const doc = editor.state.doc
const toggleNode = doc.nodeAt(pos)
if (!toggleNode || toggleNode.childCount === 0) return
const firstBlock = toggleNode.child(0)
if (
firstBlock.type.name === 'paragraph' &&
firstBlock.textContent.trim() === 'Введите подробности...'
) {
const from = pos + 2 // +1 = paragraph, +1 = start of text inside it
const to = from + firstBlock.textContent.length
const tr = editor.state.tr.setSelection(
TextSelection.create(doc, from, to)
)
editor.view.dispatch(tr)
editor.view.focus()
}
} catch (err) {
console.warn('Ошибка выделения toggleBlock:', err)
}
}}
>
<NodeViewContent className="toggle-content"/>
</div> </div>
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
...@@ -63,14 +137,14 @@ const ToggleBlock = Node.create({ ...@@ -63,14 +137,14 @@ const ToggleBlock = Node.create({
group: 'block', group: 'block',
content: 'block+', content: 'block+',
addAttributes() { addAttributes () {
return { return {
title: { default: 'Заголовок' }, title: {default: 'Заголовок'},
open: { default: false }, open: {default: false},
} }
}, },
parseHTML() { parseHTML () {
return [{ return [{
tag: 'div.toggle-block', tag: 'div.toggle-block',
getAttrs: (element) => { getAttrs: (element) => {
...@@ -83,12 +157,12 @@ const ToggleBlock = Node.create({ ...@@ -83,12 +157,12 @@ const ToggleBlock = Node.create({
titleEl.parentNode.removeChild(titleEl) titleEl.parentNode.removeChild(titleEl)
} }
return { title } return {title}
} }
}] }]
}, },
renderHTML({ HTMLAttributes }) { renderHTML ({HTMLAttributes}) {
return [ return [
'div', 'div',
{ {
...@@ -96,15 +170,15 @@ const ToggleBlock = Node.create({ ...@@ -96,15 +170,15 @@ const ToggleBlock = Node.create({
}, },
[ [
'div', 'div',
{ class: 'toggle-block-inner' }, {class: 'toggle-block-inner'},
['span', { class: 'toggle-button ' }], ['span', {class: 'toggle-button '}],
['span', { class: 'toggle-header' }, HTMLAttributes.title || 'Заголовок'], ['span', {class: 'toggle-header'}, HTMLAttributes.title || 'Заголовок'],
], ],
['div', { class: 'toggle-body' }, ['div', { class: 'toggle-content' }, 0]], ['div', {class: 'toggle-body'}, ['div', {class: 'toggle-content'}, 0]],
] ]
}, },
addNodeView() { addNodeView () {
return ReactNodeViewRenderer(ToggleBlockComponent) return ReactNodeViewRenderer(ToggleBlockComponent)
}, },
}) })
......
...@@ -49,8 +49,12 @@ const Video = Node.create({ ...@@ -49,8 +49,12 @@ const Video = Node.create({
closeBtn.textContent = 'X' closeBtn.textContent = 'X'
closeBtn.classList.add('closeBtn') closeBtn.classList.add('closeBtn')
closeBtn.addEventListener('click', function () { closeBtn.addEventListener('click', function () {
container.remove() const pos = editor.view.posAtDOM(container, 0)
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}) })
container.append(closeBtn, video) container.append(closeBtn, video)
return { return {
......
...@@ -1102,14 +1102,14 @@ body{ ...@@ -1102,14 +1102,14 @@ body{
display: flex; display: flex;
justify-content: end; justify-content: end;
border-radius: 50%; border-radius: 50%;
border: none; border: 1px solid rgb(217, 217, 217);
background-color: #2677e3; background-color: white;
color: #fff; color: #ff4d4f;
font-size: 0.5rem; font-size: 0.5rem;
padding: 4px 6px; padding: 4px 6px;
top: 10px; top: 4px;
cursor: pointer; cursor: pointer;
right: 8px; right: 4px;
z-index: 9; z-index: 9;
} }
...@@ -1149,20 +1149,18 @@ body{ ...@@ -1149,20 +1149,18 @@ body{
} }
.toggle-body { .toggle-body {
overflow: hidden; overflow: hidden;
transition: max-height 0.3s ease; transition: max-height 0.5s ease, opacity 0.5s ease;
&[data-collapsed="true"] {
max-height: 0 !important;
opacity: 0;
}
} }
.toggle-block[data-open="false"] .toggle-body { .toggle-block[data-open="false"] .toggle-body {
max-height: 0; max-height: 0;
padding: 0; padding: 0;
border: none; border: none;
} }
.toggle-block[data-open="true"] .toggle-body {
max-height: 1000px;
border: 1px dashed #D9D9D9;
border-radius: 6px;
padding: 10px;
background: #FAFAFA;
}
.toggle-block { .toggle-block {
margin-bottom: 12px; margin-bottom: 12px;
} }
...@@ -1205,12 +1203,21 @@ body{ ...@@ -1205,12 +1203,21 @@ body{
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYxMDlfNDAzMjYpIj4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNi43NDc1NiA0LjIxMzMzQzYuODc3NDUgNC4wNjQzMSA3LjEyNDkxIDQuMDY0MzEgNy4yNTM0MiA0LjIxMzMzTDExLjc0MjcgOS40MTkzOEMxMS45MDk0IDkuNjEzNTIgMTEuNzU5MSA5Ljg5NzkgMTEuNDg5OCA5Ljg5NzlIMi41MTAyNkMyLjI0MDk5IDkuODk3OSAyLjA5MDcyIDkuNjEzNTIgMi4yNTczMyA5LjQxOTM4TDYuNzQ3NTYgNC4yMTMzM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF82MTA5XzQwMzI2Ij4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=); background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYxMDlfNDAzMjYpIj4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNi43NDc1NiA0LjIxMzMzQzYuODc3NDUgNC4wNjQzMSA3LjEyNDkxIDQuMDY0MzEgNy4yNTM0MiA0LjIxMzMzTDExLjc0MjcgOS40MTkzOEMxMS45MDk0IDkuNjEzNTIgMTEuNzU5MSA5Ljg5NzkgMTEuNDg5OCA5Ljg5NzlIMi41MTAyNkMyLjI0MDk5IDkuODk3OSAyLjA5MDcyIDkuNjEzNTIgMi4yNTczMyA5LjQxOTM4TDYuNzQ3NTYgNC4yMTMzM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF82MTA5XzQwMzI2Ij4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 4px 3px; background-position: 4px 3px;
transform: rotate(0deg); transform: rotate(180deg);
background-color: white; background-color: white;
cursor: pointer; cursor: pointer;
&.open { &.open {
transform: rotate(180deg) !important; transform: rotate(0deg) !important;
} }
} }
.toggle-block {
position: relative;
}
.toggle-body-wrapper {
padding: 12px;
background: #f7f7f7;
border: 1px solid #d9d9d9;
border-radius: 4px;
transition: background 0.3s ease, border 0.3s ease;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment