Commit 1f83ff59 by Яков

update

parent 98ec316b
{
"name": "react-ag-qeditor",
"version": "1.1.10",
"version": "1.1.11",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......
......@@ -1147,6 +1147,7 @@ const QEditor = ({
src: file.path,
width: Math.round(realWidth),
height: Math.round(realHeight),
align: 'center',
points: []
},
})
......
......@@ -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() {
return {
addVoiceMessage: (options) => ({ chain }) => {
......
import { Node, mergeAttributes } from '@tiptap/core'
const Iframe = Node.create({
name: 'iframe',
group: 'block',
selectable: false,
draggable: true,
atom: true,
name: 'iframe',
group: 'block',
selectable: false,
draggable: true,
atom: true,
addAttributes() {
return {
src: {
default: null
},
frameborder: {
default: 0
},
allowfullscreen: {
default: true,
parseHTML: () => {
// console.log(this)
addAttributes () {
return {
src: {
default: null
},
frameborder: {
default: 0
},
allowfullscreen: {
default: true,
parseHTML: () => {
// console.log(this)
}
}
}
}
}
},
},
parseHTML() {
return [
{
tag: 'iframe'
}
]
},
parseHTML () {
return [
{
tag: 'iframe'
}
]
},
renderHTML({ HTMLAttributes }) {
return ['iframe', mergeAttributes(HTMLAttributes)]
},
renderHTML ({HTMLAttributes}) {
return ['iframe', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return ({ editor, node, ...a }) => {
const container = document.createElement('div')
addNodeView () {
return ({editor, node, ...a}) => {
const container = document.createElement('div')
const iframe = document.createElement('iframe')
iframe.src = node.attrs.src
iframe.allowfullscreen = node.attrs.allowfullscreen
iframe.classList.add('customIframe')
const iframe = document.createElement('iframe')
iframe.src = node.attrs.src
iframe.allowfullscreen = node.attrs.allowfullscreen
iframe.classList.add('customIframe')
const closeBtn = document.createElement('button')
closeBtn.textContent = 'X'
closeBtn.classList.add('closeBtn')
closeBtn.addEventListener('click', function () {
container.remove()
})
const closeBtn = document.createElement('button')
closeBtn.textContent = 'X'
closeBtn.classList.add('closeBtn')
// if (editor.isEditable) {
// container.classList.add('pointer-events-none');
// }
closeBtn.addEventListener('click', function () {
const pos = editor.view.posAtDOM(container, 0)
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
})
container.append(closeBtn, iframe)
// if (editor.isEditable) {
// container.classList.add('pointer-events-none');
// }
return {
dom: container
}
}
},
container.append(closeBtn, iframe)
return {
dom: container
}
}
},
addCommands() {
return {
setIframe:
(options) =>
({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
addCommands () {
return {
setIframe:
(options) =>
({tr, dispatch}) => {
const {selection} = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
}
}
}
}
})
export default Iframe
......@@ -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) && (
<Fragment>
{['nw', 'ne', 'sw', 'se'].map(dir => (
......@@ -479,25 +531,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
>
Align
</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>
)}
<Modal
......
import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import React, { useEffect, useRef, useState } from 'react'
import { TextSelection } from 'prosemirror-state'
// React компонент NodeView
export const ToggleBlockComponent = ({ node, updateAttributes }) => {
export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => {
const open = node.attrs.open
const title = node.attrs.title
......@@ -11,7 +12,7 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
const [inputWidth, setInputWidth] = useState('100px')
const toggle = () => {
updateAttributes({ open: !open })
updateAttributes({open: ! open})
}
useEffect(() => {
......@@ -23,15 +24,49 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
return (
<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-header-wrapper">
<span
className="toggle-header-measurer"
ref={measurerRef}
aria-hidden="true"
>
{title || 'Заголовок'}
</span>
<span
className="toggle-header-measurer"
ref={measurerRef}
aria-hidden="true"
>
{title || 'Заголовок'}
</span>
<button className={"toggle-button " + (open ? 'open' : '')} onClick={toggle}></button>
<input
type="text"
......@@ -40,17 +75,56 @@ export const ToggleBlockComponent = ({ node, updateAttributes }) => {
onChange={(e) => updateAttributes({ title: e.target.value })}
placeholder="Заголовок..."
style={{ width: inputWidth }}
onFocus={(e) => {
if (title.trim() === 'Заголовок') {
// выделить весь текст
setTimeout(() => e.target.select(), 0)
}
}}
/>
</div>
</div>
<div
className="toggle-body"
data-collapsed={!open}
style={{
maxHeight: open ? '1000px' : '0',
}}
>
<div className="toggle-body-wrapper">
<NodeViewContent className="toggle-content" />
<div
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>
</NodeViewWrapper>
......@@ -63,14 +137,14 @@ const ToggleBlock = Node.create({
group: 'block',
content: 'block+',
addAttributes() {
addAttributes () {
return {
title: { default: 'Заголовок' },
open: { default: false },
title: {default: 'Заголовок'},
open: {default: false},
}
},
parseHTML() {
parseHTML () {
return [{
tag: 'div.toggle-block',
getAttrs: (element) => {
......@@ -83,12 +157,12 @@ const ToggleBlock = Node.create({
titleEl.parentNode.removeChild(titleEl)
}
return { title }
return {title}
}
}]
},
renderHTML({ HTMLAttributes }) {
renderHTML ({HTMLAttributes}) {
return [
'div',
{
......@@ -96,15 +170,15 @@ const ToggleBlock = Node.create({
},
[
'div',
{ class: 'toggle-block-inner' },
['span', { class: 'toggle-button ' }],
['span', { class: 'toggle-header' }, HTMLAttributes.title || 'Заголовок'],
{class: 'toggle-block-inner'},
['span', {class: 'toggle-button '}],
['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)
},
})
......
......@@ -49,8 +49,12 @@ const Video = Node.create({
closeBtn.textContent = 'X'
closeBtn.classList.add('closeBtn')
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)
return {
......
......@@ -1102,14 +1102,14 @@ body{
display: flex;
justify-content: end;
border-radius: 50%;
border: none;
background-color: #2677e3;
color: #fff;
border: 1px solid rgb(217, 217, 217);
background-color: white;
color: #ff4d4f;
font-size: 0.5rem;
padding: 4px 6px;
top: 10px;
top: 4px;
cursor: pointer;
right: 8px;
right: 4px;
z-index: 9;
}
......@@ -1149,20 +1149,18 @@ body{
}
.toggle-body {
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 {
max-height: 0;
padding: 0;
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 {
margin-bottom: 12px;
}
......@@ -1205,12 +1203,21 @@ body{
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTQiIGhlaWdodD0iMTQiIHZpZXdCb3g9IjAgMCAxNCAxNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzYxMDlfNDAzMjYpIj4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNi43NDc1NiA0LjIxMzMzQzYuODc3NDUgNC4wNjQzMSA3LjEyNDkxIDQuMDY0MzEgNy4yNTM0MiA0LjIxMzMzTDExLjc0MjcgOS40MTkzOEMxMS45MDk0IDkuNjEzNTIgMTEuNzU5MSA5Ljg5NzkgMTEuNDg5OCA5Ljg5NzlIMi41MTAyNkMyLjI0MDk5IDkuODk3OSAyLjA5MDcyIDkuNjEzNTIgMi4yNTczMyA5LjQxOTM4TDYuNzQ3NTYgNC4yMTMzM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF82MTA5XzQwMzI2Ij4KPHJlY3Qgd2lkdGg9IjE0IiBoZWlnaHQ9IjE0IiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=);
background-repeat: no-repeat;
background-position: 4px 3px;
transform: rotate(0deg);
transform: rotate(180deg);
background-color: white;
cursor: pointer;
&.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