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

update

parent cf69071b
{
"name": "react-ag-qeditor",
"version": "1.1.87",
"version": "1.1.89",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......
......@@ -1797,21 +1797,29 @@ const QEditor = ({
})
.command(({ tr, state }) => {
// После setImage курсор — NodeSelection на картинке.
// На десктопе: TextSelection сразу после картинки (inline).
// На мобильном: курсор в следующий параграф — иначе он
// растягивается на всю высоту картинки.
// Уводим его в пустой абзац ПОД картинкой, при
// необходимости создавая его.
//
// Раньше на десктопе курсор ставился «сразу после
// картинки» — а она занимает всю ширину строки, и это
// верхний ПРАВЫЙ угол: тонкая каретка сбоку от картинки
// выглядит как сбой, печатать «под картинкой» нельзя,
// текст прыгает на следующую строку сам (замечание
// владельца 29.08.2026). На мобильном курсор туда же
// уводили и раньше — теперь поведение одно на всех.
const { $to } = state.selection
if (isMobile) {
const afterPara = $to.after(Math.min($to.depth, 1))
const target = afterPara < tr.doc.nodeSize - 1
? afterPara + 1
: afterPara
try {
tr.setSelection(TextSelection.create(tr.doc, target))
} catch {
tr.setSelection(TextSelection.create(tr.doc, $to.pos))
const paragraph = state.schema.nodes.paragraph
try {
const after = $to.after(Math.min($to.depth, 1))
const следом = tr.doc.nodeAt(after)
const пустойАбзац = следом
&& следом.type === paragraph
&& следом.content.size === 0
if (!пустойАбзац) {
tr.insert(after, paragraph.create())
}
} else {
tr.setSelection(TextSelection.create(tr.doc, after + 1))
} catch {
tr.setSelection(TextSelection.create(tr.doc, $to.pos))
}
return true
......
import React from 'react'
import React, { useState } from 'react'
import { Tooltip } from 'antd'
/**
......@@ -21,18 +21,66 @@ import { Tooltip } from 'antd'
* Именно она давала ощущение «панель из 2003 года», с которого началась вся
* работа над редактором. Здесь — название и строка «что я получу».
*/
/**
* Насколько опустить подсказку, чтобы она не легла на саму панель.
*
* Когда кнопки не помещаются в один ряд (узкое окно, панель 70–80 px вместо 46),
* подсказка кнопки ВЕРХНЕГО ряда всплывала под этой кнопкой — то есть поверх
* нижнего ряда: иконки закрыты, а сама подсказка читается вперемешку с ними.
* Поэтому опускаем её до нижнего края панели: расстояние считаем от кнопки,
* над которой курсор, до низа панели.
*
* Кнопку ищем через :hover, а не по ref: подсказка оборачивает и обычные
* кнопки, и чужие компоненты (выпадающие списки, эмодзи), и не каждый из них
* пробрасывает ref наружу. Вне панели (меню над выделением) поиск ничего
* не находит, и поведение остаётся прежним.
*/
const смещениеПодПанель = () => {
if (typeof document === 'undefined') {
return null
}
const панель = document.querySelector('.atma-editor-toolbar')
if (!панель) {
return null
}
// Без spread по NodeList: сборка пакета превращает [...nodeList] в
// [].concat(nodeList), а это кладёт в массив сам список, а не его элементы —
// обработчик падал с TypeError, и подсказка оставалась на месте.
const подКурсором = панель.querySelectorAll(':hover')
const кнопка = подКурсором.length ? подКурсором[подКурсором.length - 1] : null
if (!кнопка) {
return null
}
const сдвиг = Math.round(панель.getBoundingClientRect().bottom - кнопка.getBoundingClientRect().bottom)
return сдвиг > 1 ? [0, сдвиг] : null
}
const Tip = ({ info, placement = 'bottom', children }) => {
const [offset, setOffset] = useState(null)
// Нет названия — нет и подсказки: пустое чёрное окошко хуже, чем ничего
if (!info || !info.title) {
return children
}
const onOpenChange = (open) => {
if (!open) {
setOffset(null)
return
}
setOffset(placement === 'bottom' ? смещениеПодПанель() : null)
}
return (
<Tooltip
placement={placement}
mouseEnterDelay={0.3}
mouseLeaveDelay={0}
overlayClassName="atma-editor-tip"
onOpenChange={onOpenChange}
align={offset ? { offset } : undefined}
title={
<span className="atma-editor-tip-body">
<b className="atma-editor-tip-title">{info.title}</b>
......
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { Tooltip } from 'antd';
import { Icon } from '../icons';
import Tip from './Tip';
......@@ -559,14 +558,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
const undoInfo = infoFor(type, item);
return (
<Tooltip
key={idx}
placement="bottom"
mouseEnterDelay={0.3}
mouseLeaveDelay={0}
overlayClassName="atma-editor-tip"
title={<span className="atma-editor-tip-title">{undoInfo.title}</span>}
>
<Tip key={idx} info={undoInfo}>
<div
onClick={(e) => {
item.onClick();
......@@ -582,27 +574,13 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
>
<Icon type={type} />
</div>
</Tooltip>
</Tip>
)
}
const info = infoFor(type, item);
return (
<Tooltip
key={idx}
placement="bottom"
// 300 мс: не мешает быстрому клику, но не заставляет ждать,
// как нативный title (там задержка около секунды и её не настроить)
mouseEnterDelay={0.3}
mouseLeaveDelay={0}
overlayClassName="atma-editor-tip"
title={
<span className="atma-editor-tip-body">
<b className="atma-editor-tip-title">{info.title}</b>
{info.hint && <span className="atma-editor-tip-hint">{info.hint}</span>}
</span>
}
>
<Tip key={idx} info={info}>
<div
onClick={(e) => {
item.onClick();
......@@ -618,7 +596,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
>
<Icon type={type} />
</div>
</Tooltip>
</Tip>
)
}
......@@ -703,18 +681,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
: (activeType === false || section.toggle === false ? section.items[0] : activeType);
return (
<Tooltip
placement="bottom"
mouseEnterDelay={0.3}
mouseLeaveDelay={0}
overlayClassName="atma-editor-tip"
title={
<span className="atma-editor-tip-body">
<b className="atma-editor-tip-title">{tip.title}</b>
{tip.hint && <span className="atma-editor-tip-hint">{tip.hint}</span>}
</span>
}
>
<Tip info={tip}>
<div className={'qicon' + (activeType !== false && section.toggle !== false ? ' active' : '')}
role="button"
tabIndex={0}
......@@ -722,7 +689,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
aria-haspopup="true">
<Icon type={iconType} />
</div>
</Tooltip>
</Tip>
);
})()
}
......
......@@ -29,41 +29,12 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
const isNoWrap = !node.attrs.wrap && (node.attrs.align === 'left' || node.attrs.align === 'right');
// На десктопе вставляем \u200B после картинки, чтобы курсор можно было
// поставить inline сразу после неё. На мобильном это не нужно — там курсор
// всё равно встаёт на всю высоту картинки, выглядит некорректно.
useEffect(() => {
if (isMobile) return
if (!editor || !getPos || editor.isDestroyed) return
let pos
try {
pos = getPos()
} catch {
return
}
if (typeof pos !== 'number') return
const { doc } = editor.state
const node = doc.nodeAt(pos)
if (!node || node.type.name !== 'image') return
const next = doc.nodeAt(pos + node.nodeSize)
if (next?.isText && next.text === '\u200B') return
requestAnimationFrame(() => {
if (editor.isDestroyed) return
try {
const p = getPos()
const n = editor.state.doc.nodeAt(p)
if (!n || n.type.name !== 'image') return
editor.commands.insertContentAt(p + n.nodeSize, '\u200B')
} catch {}
})
}, [])
// Раньше сразу после картинки вставлялся \u200B — «чтобы курсор можно было
// поставить inline сразу после неё». Убрано 29.08.2026: картинка занимает
// всю ширину строки, и позиция «сразу после» — это верхний ПРАВЫЙ угол,
// где тонкая каретка выглядит как сбой («курсор почему-то справа», замечание
// владельца). Курсор после вставки теперь уводится в абзац под картинкой
// (QEditor), а невидимый символ только мусорил в сохранённом HTML.
// Получаем текущую ширину редактора и доступное пространство
const getEditorDimensions = () => {
......
......@@ -26,6 +26,28 @@ import { Plugin, PluginKey, TextSelection } from 'prosemirror-state'
* Абзацы после заголовков и текста НЕ добавляем: там курсор ставится и так,
* а лишняя пустая строка уезжала бы в сохранённый HTML на каждом сохранении.
*/
/**
* Есть ли в узле место, куда встанет курсор.
*
* Абзац — текстовый узел, но абзац, в котором лежит ОДНА картинка и больше
* ничего, для автора такой же «блок», как видео или таблица: картинка занимает
* всю ширину строки, и печатать в этом абзаце можно только сбоку от неё.
* Поэтому документ, заканчивающийся таким абзацем, тоже требует пустой строки
* следом. (Обычная картинка — инлайновый узел внутри <p>, в отличие
* от интерактивной, которая сама по себе блок.)
*/
const местоДляКурсора = (узел) => {
if (!узел.isTextblock) {
return false
}
if (узел.childCount === 0) {
return true
}
// \u200B — невидимый символ, который прежние версии редактора ставили
// после картинки; текстом он не является
return узел.textContent.replace(/\u200B/g, '').trim() !== ''
}
export const TrailingNode = Extension.create({
name: 'trailingNode',
......@@ -41,7 +63,7 @@ export const TrailingNode = Extension.create({
const { doc, schema, tr } = newState
const last = doc.lastChild
if (!last || last.isTextblock || !schema.nodes.paragraph) {
if (!last || местоДляКурсора(last) || !schema.nodes.paragraph) {
return null
}
......@@ -51,7 +73,7 @@ export const TrailingNode = Extension.create({
props: {
handleClick: (view, pos, event) => {
const last = view.state.doc.lastChild
if (!last || last.isTextblock) {
if (!last || местоДляКурсора(last)) {
return false
}
......
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