Commit c4ae2bf2 authored by Яков's avatar Яков
Browse files

update

parent 843a1b15
Pipeline #9938 canceled with stages
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ClaudeCodeTabState">
<option name="tabSessions">
<map>
<entry key="0">
<value>
<TabSessionState>
<option name="provider" value="claude" />
<option name="sessionId" value="bca3c9d2-5b93-479f-bd56-e8e8b8ddc1a3" />
<option name="cwd" value="$PROJECT_DIR$" />
<option name="model" value="claude-sonnet-4-6" />
<option name="permissionMode" value="bypassPermissions" />
<option name="reasoningEffort" value="medium" />
</TabSessionState>
</value>
</entry>
</map>
</option>
</component>
</project>
\ No newline at end of file
...@@ -4,7 +4,7 @@ import QEditor from 'react-ag-qeditor' ...@@ -4,7 +4,7 @@ import QEditor from 'react-ag-qeditor'
import 'react-ag-qeditor/dist/index.css' import 'react-ag-qeditor/dist/index.css'
const App = () => { const App = () => {
return <div style={{padding:40}}> return <div>
<QEditor <QEditor
// value={`<iframe src="https://cdn.atmaguru.online/2/atmacompany/8/b/8bTfGoWtAuv5waVabQRTtWaNOrve5uv8UBXFbGOH9cowQ1K56dYi7TFz6h5jUfzr.pdf" width="100%" /><iframe src="https://www.youtube.com/embed/YmZGP7YP8c4" frameborder="0" allowfullscreen="true"></iframe><video src="https://cdn.atmaguru.online/2/demo/V/k/VkrEXjkxnutXLgcJPt5CLXNgEj4RaL9Zk4SQhIMUjOeIRpu0dSKtQCIMl49pJM6N.webm" controls="true"></video><p>Так исторически сложилось, что взрослым людям стараются дать максимум материалов: часовые лекции, объемные массивы текста и должностных инструкций. Сотрудник изучает огромный объем информации. Пытается его запомнить, а потом в конце курса сдать большой аттестационный экзамен. Вы не учитывете при этом, что мозг взрослого человека перегружен, ему нужно выполнять обязанности по работе, думать о домашних делах, его постоянно отвлекают менеджеры и коллеги по работе… Единственный правильный способ — это давать информацию небольшими кусочками и после каждой порции проверять усвоена она или нет.</p><p></p><p>что-то новое о компании<br><a href="https://cdn.atmaguru.online/1/demo/T/G/TGvSAoLawONkteJ47yyNfmsC8zNe3ZRG4iO0ZfAjmvOIZkm20BWp8KdWCH5p1Rrx.gif" target="_blank" download="Редактор.gif" data-size="37 Мб">РСкачать книгу</a> <br></p>`} // value={`<iframe src="https://cdn.atmaguru.online/2/atmacompany/8/b/8bTfGoWtAuv5waVabQRTtWaNOrve5uv8UBXFbGOH9cowQ1K56dYi7TFz6h5jUfzr.pdf" width="100%" /><iframe src="https://www.youtube.com/embed/YmZGP7YP8c4" frameborder="0" allowfullscreen="true"></iframe><video src="https://cdn.atmaguru.online/2/demo/V/k/VkrEXjkxnutXLgcJPt5CLXNgEj4RaL9Zk4SQhIMUjOeIRpu0dSKtQCIMl49pJM6N.webm" controls="true"></video><p>Так исторически сложилось, что взрослым людям стараются дать максимум материалов: часовые лекции, объемные массивы текста и должностных инструкций. Сотрудник изучает огромный объем информации. Пытается его запомнить, а потом в конце курса сдать большой аттестационный экзамен. Вы не учитывете при этом, что мозг взрослого человека перегружен, ему нужно выполнять обязанности по работе, думать о домашних делах, его постоянно отвлекают менеджеры и коллеги по работе… Единственный правильный способ — это давать информацию небольшими кусочками и после каждой порции проверять усвоена она или нет.</p><p></p><p>что-то новое о компании<br><a href="https://cdn.atmaguru.online/1/demo/T/G/TGvSAoLawONkteJ47yyNfmsC8zNe3ZRG4iO0ZfAjmvOIZkm20BWp8KdWCH5p1Rrx.gif" target="_blank" download="Редактор.gif" data-size="37 Мб">РСкачать книгу</a> <br></p>`}
// value={"<p style=\"text-align: left\"><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://telemost.yandex.ru/j/5911922929\">дшдлодлод</a></p><table data-bordered=\"true\" class=\"\" style=\"min-width: 75px\"><colgroup><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"></colgroup><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></th><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\"></p></th><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></td><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\"></p></td><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></td></tr></tbody></table><p style=\"text-align: left\"></p>"} // value={"<p style=\"text-align: left\"><a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://telemost.yandex.ru/j/5911922929\">дшдлодлод</a></p><table data-bordered=\"true\" class=\"\" style=\"min-width: 75px\"><colgroup><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"><col style=\"min-width: 25px\"></colgroup><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></th><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\"></p></th><th colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></td><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\"></p></td><td colspan=\"1\" rowspan=\"1\"><p style=\"text-align: left\">sdfsdf</p></td></tr></tbody></table><p style=\"text-align: left\"></p>"}
...@@ -18,8 +18,7 @@ const App = () => { ...@@ -18,8 +18,7 @@ const App = () => {
errorMessage: 'Загрузка временно невозможна' errorMessage: 'Загрузка временно невозможна'
}} }}
style={{ style={{
maxWidth: '830px', maxWidth: '1000px',
margin: '20px 20px 20px 100px',
}} }}
toolsOptions={{ toolsOptions={{
type: 'all' type: 'all'
......
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.1.32", "version": "1.1.33",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -68,7 +68,51 @@ export const DragAndDrop = Extension.create({ ...@@ -68,7 +68,51 @@ export const DragAndDrop = Extension.create({
}; };
// <<< NEW // <<< NEW
const handleFileUpload = async (file, view, position) => { // Парсим атрибуты изображения из Word HTML (размеры и выравнивание)
const parseWordImageAttrs = (html) => {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const img = doc.querySelector('img');
if (!img) return {};
const style = img.getAttribute('style') || '';
const result = {};
// Конвертация единиц в пиксели (96 dpi)
const toPixels = (value, unit) => {
const n = parseFloat(value);
if (unit === 'in') return Math.round(n * 96);
if (unit === 'cm') return Math.round(n * 37.795);
if (unit === 'pt') return Math.round(n * 1.333);
if (unit === 'px') return Math.round(n);
return null;
};
const parseStyleDim = (prop) => {
const m = style.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([\\d.]+)(in|cm|pt|px)`, 'i'));
return m ? toPixels(m[1], m[2]) : null;
};
result.width = parseStyleDim('width') || parseInt(img.getAttribute('width'), 10) || null;
result.height = parseStyleDim('height') || parseInt(img.getAttribute('height'), 10) || null;
// Выравнивание по margin/float
const ml = (style.match(/margin-left\s*:\s*([^;]+)/i) || [])[1]?.trim();
const mr = (style.match(/margin-right\s*:\s*([^;]+)/i) || [])[1]?.trim();
const fl = (style.match(/float\s*:\s*([^;]+)/i) || [])[1]?.trim();
if (ml === 'auto' && mr === 'auto') result.align = 'center';
else if (fl === 'right' || ml === 'auto') result.align = 'right';
else result.align = 'left';
return result;
} catch {
return {};
}
};
const handleFileUpload = async (file, view, position, extraAttrs = {}) => {
if (!extension.options.uploadUrl && !extension.options.uploadHandler) { if (!extension.options.uploadUrl && !extension.options.uploadHandler) {
console.error('No upload URL or handler provided'); console.error('No upload URL or handler provided');
return; return;
...@@ -95,50 +139,37 @@ export const DragAndDrop = Extension.create({ ...@@ -95,50 +139,37 @@ export const DragAndDrop = Extension.create({
const id = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const id = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
if (nodeType === 'image') { if (nodeType === 'image') {
const img = new Image(); // Если есть готовые атрибуты (напр. из Word) — используем их,
img.src = result.file_path; // ограничивая ширину размером редактора.
img.onload = () => { // Иначе вставляем без size — Image.jsx сам инициализирует.
// === твой расчёт размеров — без изменений === let nodeAttrs = {
const viewDom = view.dom; src: result.file_path,
const editorContent = viewDom.closest('.atma-editor-content'); alt: '',
const fullEditorWidth = editorContent?.clientWidth || 1000; title: file.name,
const editorStyles = editorContent ? window.getComputedStyle(editorContent) : {}; align: extraAttrs.align || 'left',
const paddingLeft = parseFloat(editorStyles.paddingLeft) || 0; 'data-node-id': id
const paddingRight = parseFloat(editorStyles.paddingRight) || 0; };
const availableEditorWidth = fullEditorWidth - paddingLeft - paddingRight;
if (extraAttrs.width && extraAttrs.height) {
const container = editorContent; const editorContent = view.dom.closest('.atma-editor-content');
const containerStyles = container ? window.getComputedStyle(container) : {}; const editorWidth = editorContent
const containerPaddingLeft = parseFloat(containerStyles.paddingLeft) || 0; ? editorContent.clientWidth - (parseFloat(getComputedStyle(editorContent).paddingLeft) || 0) - (parseFloat(getComputedStyle(editorContent).paddingRight) || 0)
const containerPaddingRight = parseFloat(containerStyles.paddingRight) || 0; : Infinity;
const containerWidth = container ? (container.clientWidth - containerPaddingLeft - containerPaddingRight) : availableEditorWidth;
let { width, height } = extraAttrs;
const maxWidth = Math.min(containerWidth, availableEditorWidth); if (width > editorWidth && editorWidth !== Infinity) {
// === конец блока расчёта === const ratio = editorWidth / width;
width = Math.round(editorWidth);
let width = img.naturalWidth;
let height = img.naturalHeight;
if (width > maxWidth) {
const ratio = maxWidth / width;
width = maxWidth;
height = Math.round(height * ratio); height = Math.round(height * ratio);
} }
nodeAttrs.width = width;
nodeAttrs.height = height;
}
const node = view.state.schema.nodes[nodeType].create({ const node = view.state.schema.nodes[nodeType].create(nodeAttrs);
src: result.file_path, const tr = view.state.tr.insert(position || view.state.selection.from, node);
alt: '', view.dispatch(tr);
title: file.name, extension.options.onUploadSuccess(result);
width,
height,
align: 'left',
'data-node-id': id
});
const tr = view.state.tr.insert(position || view.state.selection.from, node);
view.dispatch(tr);
extension.options.onUploadSuccess(result);
};
} else { } else {
const node = view.state.schema.nodes[nodeType].create({ const node = view.state.schema.nodes[nodeType].create({
src: result.file_path, src: result.file_path,
...@@ -160,17 +191,99 @@ export const DragAndDrop = Extension.create({ ...@@ -160,17 +191,99 @@ export const DragAndDrop = Extension.create({
const handlePaste = (view, event) => { const handlePaste = (view, event) => {
const items = Array.from(event.clipboardData?.items || []); const items = Array.from(event.clipboardData?.items || []);
if (event.clipboardData.getData('text/html').includes('urn:schemas-microsoft-com')) { const html = event.clipboardData.getData('text/html');
return false;
// 1. Word: берём изображение из clipboard items + атрибуты из HTML
if (html.includes('urn:schemas-microsoft-com')) {
const imageItem = items.find(
item => item.kind === 'file' && item.type.startsWith('image/')
);
if (!imageItem) return false;
const file = imageItem.getAsFile();
if (!file) return false;
event.preventDefault();
handleFileUpload(file, view, null, parseWordImageAttrs(html));
return true;
} }
// 2. Бинарный файл в буфере (обычный drag&paste)
const maybeFile = items.find(item => isRealFile(item.getAsFile())); const maybeFile = items.find(item => isRealFile(item.getAsFile()));
const file = maybeFile?.getAsFile(); const binaryFile = maybeFile?.getAsFile();
if (!file) return false; if (binaryFile) {
event.preventDefault();
handleFileUpload(binaryFile, view);
return true;
}
event.preventDefault(); // 3. HTML с внешними картинками (Google Docs, веб-страницы и т.д.)
handleFileUpload(file, view); if (html) {
return true; console.log('[DragAndDrop] paste html types:', items.map(i => `${i.kind}:${i.type}`));
const tmpDoc = new DOMParser().parseFromString(html, 'text/html');
const externalImgs = Array.from(tmpDoc.querySelectorAll('img[src]')).filter(img => {
const src = img.getAttribute('src') || '';
return src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:');
});
console.log('[DragAndDrop] external imgs found:', externalImgs.length, externalImgs.map(i => i.getAttribute('src')?.slice(0, 80)));
if (externalImgs.length > 0) {
const imgData = externalImgs.map(img => ({
src: img.getAttribute('src'),
width: parseInt(img.getAttribute('width')) || null,
height: parseInt(img.getAttribute('height')) || null,
}));
// Убираем <img> из HTML — TipTap вставит только текст
externalImgs.forEach(img => img.remove());
event.preventDefault();
const cleanHtml = tmpDoc.body.innerHTML;
if (cleanHtml.trim()) {
extension.editor.commands.insertContent(cleanHtml);
}
// Асинхронно загружаем каждую картинку на сервер и вставляем
;(async () => {
for (const { src, width, height } of imgData) {
try {
let blob;
if (src.startsWith('data:')) {
// data URI — конвертируем сразу в blob
const [header, b64] = src.split(',');
const mime = (header.match(/:(.*?);/) || [])[1] || 'image/png';
const binary = atob(b64);
const arr = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
blob = new Blob([arr], { type: mime });
} else {
// Внешний URL — без credentials (иначе CORS с Access-Control-Allow-Origin: * падает)
const resp = await fetch(src);
console.log('[DragAndDrop] fetch', src.slice(0, 80), resp.status, resp.headers.get('content-type'));
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
blob = await resp.blob();
}
if (!blob.type.startsWith('image/')) {
console.warn('[DragAndDrop] unexpected blob type:', blob.type);
continue;
}
const ext = blob.type.split('/')[1]?.split('+')[0] || 'png';
const file = new File([blob], `pasted-image.${ext}`, { type: blob.type });
await handleFileUpload(file, view, null, { width, height, align: 'left' });
} catch (e) {
console.warn('[DragAndDrop] не удалось загрузить картинку:', src?.slice(0, 80), e);
}
}
})();
return true;
}
}
return false;
}; };
const handleDragStart = (event) => { const handleDragStart = (event) => {
......
...@@ -212,10 +212,11 @@ const ResizableIframeView = ({ editor, node, updateAttributes, getPos, selected ...@@ -212,10 +212,11 @@ const ResizableIframeView = ({ editor, node, updateAttributes, getPos, selected
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
pointerEvents: 'auto', pointerEvents: 'none',
}} }}
/> />
{(selected || isResizing) && ( {(selected || isResizing) && (
<Fragment> <Fragment>
<button <button
......
...@@ -278,16 +278,21 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -278,16 +278,21 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
const startWidth = node.attrs.width || imgRef.current.naturalWidth; const startWidth = node.attrs.width || imgRef.current.naturalWidth;
const startHeight = node.attrs.height || imgRef.current.naturalHeight; const startHeight = node.attrs.height || imgRef.current.naturalHeight;
const aspectRatio = startWidth / startHeight; const aspectRatio = startWidth / startHeight;
const startX = e.clientX;
const startY = e.clientY; const getClientX = (e) => e.touches ? e.touches[0].clientX : e.clientX;
const getClientY = (e) => e.touches ? e.touches[0].clientY : e.clientY;
const startX = getClientX(e);
const startY = getClientY(e);
const { width: initialEditorWidth, availableSpace: initialAvailableSpace } = getEditorDimensions(); const { width: initialEditorWidth, availableSpace: initialAvailableSpace } = getEditorDimensions();
const onMouseMove = (e) => { const onMove = (e) => {
if (e.cancelable) e.preventDefault();
requestAnimationFrame(() => { requestAnimationFrame(() => {
const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace; const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace;
const deltaX = e.clientX - startX; const deltaX = getClientX(e) - startX;
const deltaY = e.clientY - startY; const deltaY = getClientY(e) - startY;
let newWidth, newHeight; let newWidth, newHeight;
...@@ -328,10 +333,11 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -328,10 +333,11 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
}); });
}; };
const onEnd = () => {
const onMouseUp = () => { window.removeEventListener('mousemove', onMove);
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onEnd);
window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('touchmove', onMove);
window.removeEventListener('touchend', onEnd);
setIsResizing(false); setIsResizing(false);
try { try {
const pos = getPos?.() const pos = getPos?.()
...@@ -341,22 +347,30 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -341,22 +347,30 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
} catch (e) { } catch (e) {
console.warn('getPos() failed:', e) console.warn('getPos() failed:', e)
} }
// editor.commands.setNodeSelection(getPos());
editor.commands.focus(); editor.commands.focus();
}; };
window.addEventListener('mousemove', onMouseMove); window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onMouseUp); window.addEventListener('mouseup', onEnd);
window.addEventListener('touchmove', onMove, { passive: false });
window.addEventListener('touchend', onEnd);
}; };
// Изменение выравнивания с автоматическим масштабированием // Изменение выравнивания с автоматическим масштабированием
const handleAlign = (align) => { const handleAlign = (align) => {
safeUpdateAttributes({ align }); // первый вызов safeUpdateAttributes({ align });
setTimeout(() => { setTimeout(() => {
safeUpdateAttributes({ align }); // повторный вызов с обновлёнными размерами safeUpdateAttributes({ align });
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos)
}
} catch (e) {
console.warn('getPos() failed:', e)
}
}, 50); }, 50);
setShowAlignMenu(false); setShowAlignMenu(false);
editor.commands.focus();
}; };
// Стили для обертки изображения // Стили для обертки изображения
...@@ -388,11 +402,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -388,11 +402,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
float: 'left', float: 'left',
marginRight: '1rem', marginRight: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto', width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}), }),
...(node.attrs.align === 'right' && { ...(node.attrs.align === 'right' && {
float: 'right', float: 'right',
marginLeft: '1rem', marginLeft: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto', width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}), }),
...(node.attrs.align === 'text' && { ...(node.attrs.align === 'text' && {
display: 'inline-block', display: 'inline-block',
...@@ -400,6 +416,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -400,6 +416,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
margin: '0 0.2rem', margin: '0 0.2rem',
verticalAlign: 'middle', verticalAlign: 'middle',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto', width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}), }),
}; };
}; };
...@@ -538,6 +555,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select ...@@ -538,6 +555,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
<div <div
key={dir} key={dir}
onMouseDown={handleResizeStart(dir)} onMouseDown={handleResizeStart(dir)}
onTouchStart={handleResizeStart(dir)}
style={{ style={{
position: 'absolute', position: 'absolute',
width: 12, width: 12,
......
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' import { TextSelection, Plugin, PluginKey } from 'prosemirror-state'
// React компонент NodeView // React компонент NodeView
export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => { export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => {
...@@ -90,7 +90,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) = ...@@ -90,7 +90,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
className="toggle-body" className="toggle-body"
data-collapsed={!open} data-collapsed={!open}
style={{ style={{
maxHeight: open ? '1000px' : '0', maxHeight: open ? 'none' : '0',
}} }}
> >
<div <div
...@@ -182,6 +182,57 @@ const ToggleBlock = Node.create({ ...@@ -182,6 +182,57 @@ const ToggleBlock = Node.create({
addNodeView () { addNodeView () {
return ReactNodeViewRenderer(ToggleBlockComponent) return ReactNodeViewRenderer(ToggleBlockComponent)
}, },
addProseMirrorPlugins () {
return [
new Plugin({
key: new PluginKey('toggleBlockPaste'),
props: {
handlePaste (view, _event, slice) {
const { selection } = view.state
const $from = selection.$from
// Ищем toggleBlock в стеке предков
let toggleDepth = -1
for (let d = $from.depth; d > 0; d--) {
if ($from.node(d).type.name === 'toggleBlock') {
toggleDepth = d
break
}
}
if (toggleDepth === -1) return false
// Только если есть заголовки — иначе стандартная обработка
let hasHeadings = false
slice.content.forEach(node => {
if (node.type.name === 'heading') hasHeadings = true
})
if (!hasHeadings) return false
// replaceSelection с openStart > 0 уходит выше toggleBlock и заменяет его.
// Вставляем узлы через tr.insert напрямую — позиция строго внутри toggleBlock.
let insertPos = $from.after(toggleDepth + 1)
const tr = view.state.tr
if (!selection.empty) {
tr.deleteSelection()
insertPos = tr.mapping.map(insertPos)
}
const nodes = []
slice.content.forEach(n => nodes.push(n))
// Вставляем в обратном порядке в одну позицию — итоговый порядок правильный
for (let i = nodes.length - 1; i >= 0; i--) {
tr.insert(insertPos, nodes[i])
}
view.dispatch(tr.scrollIntoView())
return true
}
}
})
]
},
}) })
export default ToggleBlock export default ToggleBlock
...@@ -288,6 +288,7 @@ body{ ...@@ -288,6 +288,7 @@ body{
padding: 8px; padding: 8px;
right: 42%; right: 42%;
transform: translateX(50%); transform: translateX(50%);
z-index: 10;
&-item{ &-item{
display: flex; display: flex;
...@@ -589,6 +590,17 @@ body{ ...@@ -589,6 +590,17 @@ body{
max-height: 900px; max-height: 900px;
overflow-y: scroll; overflow-y: scroll;
@media (max-width: 600px) {
min-width: 0;
width: calc(100vw - 32px);
max-width: calc(100vw - 32px);
left: 50%;
transform: translate(-50%, -50%);
padding: 24px 16px;
max-height: calc(100dvh - 40px);
border-radius: 12px;
}
&-header{ &-header{
display: block; display: block;
text-align: left; text-align: left;
...@@ -637,6 +649,11 @@ body{ ...@@ -637,6 +649,11 @@ body{
&:hover{ &:hover{
border-color: #adb0b6; border-color: #adb0b6;
} }
@media (max-width: 600px) {
width: 100%;
height: 120px;
}
} }
&-placeholder{ &-placeholder{
display: inline-block; display: inline-block;
...@@ -1230,3 +1247,19 @@ body{ ...@@ -1230,3 +1247,19 @@ body{
border-radius: 4px; border-radius: 4px;
transition: background 0.3s ease, border 0.3s ease; transition: background 0.3s ease, border 0.3s ease;
} }
@media (max-width: 768px) {
.ProseMirror,
.atma-editor-content {
// Ограничиваем враппер шириной контейнера, не трогая float и width
div[data-image-wrapper] {
max-width: 100% !important;
}
// Картинка масштабируется пропорционально внутри враппера
div[data-image-wrapper] img {
max-width: 100% !important;
height: auto !important;
}
}
}
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