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

update

parent a24ab44a
{
"name": "react-ag-qeditor",
"version": "1.1.82",
"version": "1.1.83",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......
......@@ -22,7 +22,7 @@ import TextStyle from '@tiptap/extension-text-style'
import Superscript from '@tiptap/extension-superscript'
import Subscript from '@tiptap/extension-subscript'
import ToolBar from './components/ToolBar'
import ToolBar, { Tip, toolInfo } from './components/ToolBar'
import EditorModal from './components/EditorModal'
import Uploader from './components/Uploader'
import EmojiPicker from './components/EmojiPicker'
......@@ -2120,10 +2120,14 @@ const QEditor = ({
} else if (type === 'glossary') {
if (window.location.pathname.includes('admin/maps')) {
return (
<Tip key={'bubbleItems-glossary'}
placement={'top'}
info={toolInfo('glossary', null, labels)}>
<div
key={'bubbleItems-glossary'}
className={'qicon'}
title={'Добавить в глоссарий'}
role={'button'}
tabIndex={0}
aria-label={toolInfo('glossary', null, labels).title}
onClick={()=>{
if (!editor.state.selection.empty) {
const selectedText = editor.state.doc.textBetween(
......@@ -2142,18 +2146,27 @@ const QEditor = ({
>
<Icon type='glossary' />
</div>
</Tip>
)
}
} else {
// Подсказка та же, что на панели: одна таблица
// подписей и одна обёртка. Раньше здесь стоял
// нативный `title` браузера — серый и с задержкой.
const info = toolInfo(type, toolsLib[type], labels);
return (
<div
key={'bubbleItems' + i}
className={'qicon' + (editor.isActive(type) ? ' active' : '')}
title={toolsLib[type] ? toolsLib[type].title : ''}
onClick={toolsLib[type].onClick}
>
<Icon type={type} />
</div>
<Tip key={'bubbleItems' + i} placement={'top'} info={info}>
<div
className={'qicon' + (editor.isActive(type) ? ' active' : '')}
role={'button'}
tabIndex={0}
aria-label={info.title}
onClick={toolsLib[type].onClick}
>
<Icon type={type} />
</div>
</Tip>
)
}
})}
......
......@@ -240,20 +240,31 @@ const EmojiPicker = ({ editor }) => {
ref={rootRef}
className={'atma-editor-toolbar-emoji' + (isOpen ? ' open' : '')}
>
<div
className={'qicon' + (isOpen ? ' active' : '')}
title='Эмодзи'
onMouseDown={(event) => {
event.preventDefault()
}}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
setIsOpen(!isOpen)
}}
>
<Icon type='emoji' />
</div>
{/*
Подсказка та же, что у остальных кнопок панели. Здесь дольше
всего оставался нативный `title` браузера: кнопка приходит
из своего компонента и обёртку панели минует — из-за этого
эмодзи была единственной кнопкой без общей подсказки.
*/}
<Tip info={{ title: 'Эмодзи', hint: 'Вставить символ из набора' }}>
<div
className={'qicon' + (isOpen ? ' active' : '')}
role='button'
tabIndex={0}
aria-label='Эмодзи'
aria-expanded={isOpen}
onMouseDown={(event) => {
event.preventDefault()
}}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
setIsOpen(!isOpen)
}}
>
<Icon type='emoji' />
</div>
</Tip>
{isOpen && (
<div
className='atma-editor-emoji-popover'
......
......@@ -26,6 +26,7 @@ const HEADINGS = { type: 'g', items: ['paragraph', 'h2', 'h3', 'h4'] }
const TABLE_GROUP = {
type: 's',
toggle: false,
tip: 'table',
items: [
'insertTable', '|',
'addRowBefore', 'addRowAfter', 'deleteRow', '|',
......@@ -81,10 +82,10 @@ const LESSON = [
{ type: 'g', items: ['|'] },
{ type: 'g', items: ['bulletList', 'orderedList'] },
{ type: 's', toggle: true, items: ['alignLeft', 'alignCenter', 'alignRight'] },
{ type: 's', toggle: true, tip: 'align', items: ['alignLeft', 'alignCenter', 'alignRight'] },
// Особые блоки одним списком: у всех пяти общая роль — выделить кусок
// материала из общего потока.
{ type: 's', toggle: false, items: ['blockquote', 'callout', 'showAnswer', 'hr', 'codeBlock'] },
{ type: 's', toggle: false, tip: 'blocks', items: ['blockquote', 'callout', 'showAnswer', 'hr', 'codeBlock'] },
TABLE_GROUP,
{ type: 'g', items: ['emoji'] },
......@@ -102,7 +103,7 @@ const LESSON = [
//
// Иконки у записей близкие и в ряд читались как одно и то же; в списке
// у каждой подпись словами, и различить их можно с первого взгляда.
{ type: 's', toggle: false, title: 'video',
{ type: 's', toggle: false, title: 'video', tip: 'media',
items: ['video', 'iframe', 'iframe_custom', 'voicemessage', 'webcamera', 'screencust'] },
// ── «Оживить модуль» ─────────────────────────────────────────────────────
......@@ -122,7 +123,7 @@ const LESSON = [
// четыре разные вещи со словом «pdf» в подписи. Рядом, с человеческими
// названиями, они наконец различимы. «Файл на скачивание» сюда же:
// вложение — такой же документ, а не сосед ссылки.
{ type: 's', toggle: false, items: ['iframe_pptx', 'iframe_pdf', 'pdf', 'export_pdf', 'file'] },
{ type: 's', toggle: false, tip: 'docs', items: ['iframe_pptx', 'iframe_pdf', 'pdf', 'export_pdf', 'file'] },
{ type: 'g', items: ['link'] },
]
......@@ -139,7 +140,7 @@ const LESSON = [
*/
const LESSON_NARROW = [
{ type: 'g', items: ['undo', 'redo'] },
{ type: 's', toggle: true, items: ['paragraph', 'h2', 'h3', 'h4'] },
{ type: 's', toggle: true, tip: 'headings', items: ['paragraph', 'h2', 'h3', 'h4'] },
{ type: 'g', items: ['bulletList', 'orderedList'] },
{ type: 's', toggle: true, items: ['alignLeft', 'alignCenter', 'alignRight'] },
{ type: 'g', items: ['link'] },
......@@ -317,9 +318,85 @@ const TOOL_INFO = {
},
insertMore: { title: 'Вставить', hint: 'Всё, что кладёт в текст новый блок: картинка, видео, задание, файл' },
more: { title: 'Ещё', hint: 'Редкое и служебное: цитата, код, записи, документы' },
// ── Подписи самих выпадающих списков ────────────────────────────────────
//
// У кнопки, открывающей список, подсказки не было вовсе: наводишь —
// и ничего, хотя у каждого пункта внутри она есть. Кнопок таких шесть,
// и это ровно те места, где автор не понимает, что за ними прячется.
align: { title: 'Выравнивание', hint: 'По левому краю, по центру, по правому' },
headings: { title: 'Уровень текста', hint: 'Обычный текст или заголовок: H2, H3, H4' },
blocks: { title: 'Особые блоки', hint: 'Врезка, скрытый разбор, цитата, разделитель, блок кода' },
media: { title: 'Видео и встраивание', hint: 'Видеофайл, ролик по ссылке, вставка по коду, запись голоса, камеры и экрана' },
docs: { title: 'Документы', hint: 'Презентация, PDF, вложение на скачивание, выгрузка модуля в PDF' },
table: { title: 'Таблица', hint: 'Вставить таблицу и настроить строки, столбцы и границы' },
undo: { title: 'Отменить' },
redo: { title: 'Вернуть' },
emoji: { title: 'Эмодзи', hint: 'Вставить символ из набора' },
help: { title: 'Справка по редактору', hint: 'Что умеет редактор и как этим пользоваться' },
// ── Всплывающее меню над выделением ─────────────────────────────────────
//
// Там подсказок не было вовсе: работал нативный `title` браузера — серый,
// с секундной задержкой и одной строкой. Именно он и создавал ощущение
// «панель из 2003 года», из-за которого всю эту работу начали.
//
// Жирному и курсиву пояснение не нужно, их узнают по иконке, — но название
// всё равно должно всплывать в общем виде, а не системным всплывающим
// окном браузера.
bold: { title: 'Жирный' },
italic: { title: 'Курсив' },
underline: { title: 'Подчёркнутый' },
strike: { title: 'Зачёркнутый' },
superscript: { title: 'Надстрочный символ', hint: 'Степень или сноска: м², 1-й' },
subscript: { title: 'Подстрочный символ', hint: 'Индекс в формуле: H₂O' },
colorText: { title: 'Цвет текста', hint: 'Шесть смысловых цветов вместо произвольной палитры' },
highlight: { title: 'Выделение цветом', hint: 'Подсветка фоном, как маркером' },
fontSize: { title: 'Размер шрифта' },
glossary: { title: 'Добавить в глоссарий', hint: 'Занести выделенное слово в словарь компании' },
}
/**
* Название и пояснение кнопки — одна таблица на панель и на всплывающее меню.
*
* Порядок: словарь от хоста → встроенная таблица → title из toolsLib.
*/
export const toolInfo = (type, item, labels = {}) => {
const custom = labels[type] || {}
const built = TOOL_INFO[type] || {}
return {
title: custom.title || built.title || (item && item.title) || '',
hint: custom.hint !== undefined ? custom.hint : built.hint,
}
}
/**
* Обёртка-подсказка. Вынесена, чтобы панель и всплывающее меню показывали
* подсказку ОДИНАКОВО: разъедься они, получилось бы две разные подсказки
* в одном редакторе.
*/
export const Tip = ({ info, placement = 'bottom', children }) => {
if (!info.title) {
return children
}
return (
<Tooltip
placement={placement}
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>
}
>
{children}
</Tooltip>
)
}
/** Ширина панели, ниже которой берётся узкий набор */
......@@ -376,15 +453,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
* Хост (у него есть formatjs и словари) передаёт переведённые строки пропом.
* Нет пропа — работают встроенные русские, как раньше.
*/
const infoFor = (type, item) => {
const custom = labels[type] || {}
const built = TOOL_INFO[type] || {}
return {
title: custom.title || built.title || (item && item.title) || '',
hint: custom.hint !== undefined ? custom.hint : built.hint,
}
}
const infoFor = (type, item) => toolInfo(type, item, labels)
const presetName = (toolsOptions && toolsOptions.type) ? toolsOptions.type : 'all'
......@@ -648,13 +717,40 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
}}>
<Fragment>
{
<div className={'qicon' + (activeType !== false && section.toggle !== false ? ' active' : '')}>
<Icon type={
section.title
? section.title
: (activeType === false || section.toggle === false ? section.items[0] : activeType)
} />
</div>
// Подсказка у кнопки, открывающей список.
// Ключ берётся из `tip` секции; нет его —
// из первого пункта, чтобы кнопка не осталась
// совсем без подписи.
(() => {
const tipKey = section.tip || section.title || section.items[0];
const tip = infoFor(tipKey, toolsLib[tipKey]);
const iconType = section.title
? section.title
: (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>
}
>
<div className={'qicon' + (activeType !== false && section.toggle !== false ? ' active' : '')}
role="button"
tabIndex={0}
aria-label={tip.title}
aria-haspopup="true">
<Icon type={iconType} />
</div>
</Tooltip>
);
})()
}
<div
className={'atma-editor-toolbar-s-opts'}
......@@ -684,10 +780,11 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
<div className="atma-editor-toolbar" ref={rootRef}>
{ getItems() }
{onInfo && (
<Tip info={infoFor('help', { title: 'Справка по редактору' })}>
<button
type="button"
className="atma-editor-toolbar-info"
title="Справка по редактору"
aria-label="Справка по редактору"
onClick={onInfo}
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
......@@ -696,6 +793,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
<circle cx="9" cy="5.5" r="0.75" fill="currentColor"/>
</svg>
</button>
</Tip>
)}
</div>
)
......
......@@ -113,6 +113,34 @@ const ICONS = {
export default ICONS
/**
* Уровни текста подписываются буквами, а не рисуются значком.
*
* ── Почему ───────────────────────────────────────────────────────────────────
*
* Рисунком «обычный текст» и три заголовка были стопкой горизонтальных полос
* разной толщины. Ровно так же выглядят и три кнопки выравнивания — тоже
* стопки полос. В ряду семь кнопок, и различить, где заголовок, а где
* выравнивание, невозможно: полосы и там, и там.
*
* «H2» читается без обучения и без наведения. Это не отступление от языка
* набора: буквы в нём уже есть — B, I, U, S нарисованы так же.
*
* ── Почему уровни настоящие ──────────────────────────────────────────────────
*
* Подписи H2/H3/H4 — это те уровни, которые действительно попадут в разметку.
* H1 в контенте нет намеренно: единственный H1 на странице — название модуля,
* и второй сломал бы структуру для поиска и озвучивания с экрана. Написать
* «H1» на кнопке, которая делает H2, значило бы соврать в интерфейсе.
*/
const TEXT_ICONS = {
paragraph: 'T',
h1: 'H1',
h2: 'H2',
h3: 'H3',
h4: 'H4',
}
/**
* Иконка панели.
*
......@@ -123,6 +151,15 @@ export default ICONS
* Рисунок 20×20 внутри кнопки 32×32 — полезная площадь из макета.
*/
export const Icon = ({ type, size = 20 }) => {
// Буквенная подпись — обычным текстом, а не текстом внутри svg: так она
// наследует цвет кнопки во всех шести состояниях тем же правилом
// на `color`, что и остальные иконки, и не зависит от шрифтов в svg.
if (TEXT_ICONS[type]) {
return (
<span className="qicon-text" aria-hidden="true">{TEXT_ICONS[type]}</span>
)
}
const body = ICONS[type]
if (!body) {
return null
......
......@@ -376,10 +376,23 @@ body{
height: 8px;
}
// ── Список открывается ПОД кнопкой ───────────────────────────
//
// Здесь не было `top`, и абсолютно позиционированный список
// вставал в своё «статическое» место — то есть по вертикальному
// центру кнопки, потому что группа выровнена `align-items: center`.
// У короткого списка сдвиг незаметен, а список таблицы из
// одиннадцати пунктов уезжал НАД панелью и накрывал текст модуля.
//
// `max-height` со прокруткой — на случай длинного списка внизу
// экрана: лучше прокрутить, чем показать половину за краем окна.
&-opts{
position: absolute;
clip: rect(0,0,0,0);
top: calc(100% + 6px);
display: none;
width: 260px;
max-height: min(60vh, 420px);
overflow-y: auto;
border: 1px solid #E3E7EC;
border-radius: 12px;
box-shadow: 0 12px 32px 0 rgba(15, 20, 25, 0.12);
......@@ -452,18 +465,24 @@ body{
}
}
// В списке уровень показан САМИМ размером подписи — это
// нагляднее любой пометки. Поэтому и значок, и буквенная
// подпись здесь лишние: «H2» рядом с крупным «Заголовок
// раздела» повторяет то, что уже видно.
&[data-type="h2"], &[data-type="h3"], &[data-type="h4"]{
font-weight: 500;
padding: 8px;
.qicon{
.qicon,
.qicon-text{
display: none;
}
}
&[data-type="paragraph"]{
padding: 8px;
.qicon{
.qicon,
.qicon-text{
display: none;
}
}
......@@ -498,8 +517,11 @@ body{
}
&.show{
// Было `clip: rect(0,0,0,0)` / `clip: auto`. Свойство устарело
// и, главное, не уживается с прокруткой внутри списка:
// обрезка считается от исходной коробки, а не от прокрученной.
#{$this}-opts{
clip: auto;
display: block;
}
&:after{
......@@ -539,6 +561,19 @@ body{
pointer-events: none;
}
// Буквенная подпись уровня текста: T, H2, H3, H4.
// Цвет НЕ задаётся — наследуется от кнопки, как и у svg-иконок,
// поэтому все шесть состояний работают тем же правилом.
.qicon-text{
display: block;
pointer-events: none;
font-weight: 700;
font-size: 13px;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
&:hover{
cursor: pointer;
background-color: #EDF1F6;
......@@ -970,13 +1005,21 @@ body{
margin-bottom: 10px;
}
// Выделение файла рисуется рамкой СНАРУЖИ плитки (`box-shadow` без
// inset), а список прокручиваемый. `overflow-y: auto` по стандарту
// делает `auto` и по второй оси — значит рамка у крайних плиток
// обрезалась: сверху, снизу и по бокам. На снимке автора это видно
// как «выделение подрезано».
//
// Отступ в 3 px даёт рамке место со всех сторон. Справа больше:
// там ещё полоса прокрутки.
&-list{
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 10px;
max-height: min(280px, calc(90dvh - 440px));
overflow-y: auto;
padding-right: 4px;
padding: 3px 7px 3px 3px;
&.is-video{
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
......
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