Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
lib
react-ag-qeditor
Commits
b1578e7d
Commit
b1578e7d
authored
Aug 29, 2026
by
Яков
Browse files
update
parent
a24ab44a
Changes
6
Show whitespace changes
Inline
Side-by-side
package.json
View file @
b1578e7d
{
{
"name"
:
"react-ag-qeditor"
,
"name"
:
"react-ag-qeditor"
,
"version"
:
"1.1.8
2
"
,
"version"
:
"1.1.8
3
"
,
"description"
:
"WYSIWYG html editor"
,
"description"
:
"WYSIWYG html editor"
,
"author"
:
"atma"
,
"author"
:
"atma"
,
"license"
:
"
MIT
"
,
"license"
:
"
MIT
"
,
...
...
src/QEditor.jsx
View file @
b1578e7d
...
@@ -22,7 +22,7 @@ import TextStyle from '@tiptap/extension-text-style'
...
@@ -22,7 +22,7 @@ import TextStyle from '@tiptap/extension-text-style'
import
Superscript
from
'
@tiptap/extension-superscript
'
import
Superscript
from
'
@tiptap/extension-superscript
'
import
Subscript
from
'
@tiptap/extension-subscript
'
import
Subscript
from
'
@tiptap/extension-subscript
'
import
ToolBar
from
'
./components/ToolBar
'
import
ToolBar
,
{
Tip
,
toolInfo
}
from
'
./components/ToolBar
'
import
EditorModal
from
'
./components/EditorModal
'
import
EditorModal
from
'
./components/EditorModal
'
import
Uploader
from
'
./components/Uploader
'
import
Uploader
from
'
./components/Uploader
'
import
EmojiPicker
from
'
./components/EmojiPicker
'
import
EmojiPicker
from
'
./components/EmojiPicker
'
...
@@ -2120,10 +2120,14 @@ const QEditor = ({
...
@@ -2120,10 +2120,14 @@ const QEditor = ({
}
else
if
(
type
===
'
glossary
'
)
{
}
else
if
(
type
===
'
glossary
'
)
{
if
(
window
.
location
.
pathname
.
includes
(
'
admin/maps
'
))
{
if
(
window
.
location
.
pathname
.
includes
(
'
admin/maps
'
))
{
return
(
return
(
<
Tip
key
=
{
'
bubbleItems-glossary
'
}
placement
=
{
'
top
'
}
info
=
{
toolInfo
(
'
glossary
'
,
null
,
labels
)
}
>
<
div
<
div
key
=
{
'
bubbleItems-glossary
'
}
className
=
{
'
qicon
'
}
className
=
{
'
qicon
'
}
title
=
{
'
Добавить в глоссарий
'
}
role
=
{
'
button
'
}
tabIndex
=
{
0
}
aria
-
label
=
{
toolInfo
(
'
glossary
'
,
null
,
labels
).
title
}
onClick
=
{
()
=>
{
onClick
=
{
()
=>
{
if
(
!
editor
.
state
.
selection
.
empty
)
{
if
(
!
editor
.
state
.
selection
.
empty
)
{
const
selectedText
=
editor
.
state
.
doc
.
textBetween
(
const
selectedText
=
editor
.
state
.
doc
.
textBetween
(
...
@@ -2142,18 +2146,27 @@ const QEditor = ({
...
@@ -2142,18 +2146,27 @@ const QEditor = ({
>
>
<
Icon
type
=
'glossary'
/>
<
Icon
type
=
'glossary'
/>
</
div
>
</
div
>
</
Tip
>
)
)
}
}
}
else
{
}
else
{
// Подсказка та же, что на панели: одна таблица
// подписей и одна обёртка. Раньше здесь стоял
// нативный `title` браузера — серый и с задержкой.
const
info
=
toolInfo
(
type
,
toolsLib
[
type
],
labels
);
return
(
return
(
<
Tip
key
=
{
'
bubbleItems
'
+
i
}
placement
=
{
'
top
'
}
info
=
{
info
}
>
<
div
<
div
key
=
{
'
bubbleItems
'
+
i
}
className
=
{
'
qicon
'
+
(
editor
.
isActive
(
type
)
?
'
active
'
:
''
)
}
className
=
{
'
qicon
'
+
(
editor
.
isActive
(
type
)
?
'
active
'
:
''
)
}
title
=
{
toolsLib
[
type
]
?
toolsLib
[
type
].
title
:
''
}
role
=
{
'
button
'
}
tabIndex
=
{
0
}
aria
-
label
=
{
info
.
title
}
onClick
=
{
toolsLib
[
type
].
onClick
}
onClick
=
{
toolsLib
[
type
].
onClick
}
>
>
<
Icon
type
=
{
type
}
/>
<
Icon
type
=
{
type
}
/>
</
div
>
</
div
>
</
Tip
>
)
)
}
}
})
}
})
}
...
...
src/components/EmojiPicker.js
View file @
b1578e7d
...
@@ -240,9 +240,19 @@ const EmojiPicker = ({ editor }) => {
...
@@ -240,9 +240,19 @@ const EmojiPicker = ({ editor }) => {
ref
=
{
rootRef
}
ref
=
{
rootRef
}
className
=
{
'
atma-editor-toolbar-emoji
'
+
(
isOpen
?
'
open
'
:
''
)}
className
=
{
'
atma-editor-toolbar-emoji
'
+
(
isOpen
?
'
open
'
:
''
)}
>
>
{
/*
Подсказка та же, что у остальных кнопок панели. Здесь дольше
всего оставался нативный `title` браузера: кнопка приходит
из своего компонента и обёртку панели минует — из-за этого
эмодзи была единственной кнопкой без общей подсказки.
*/
}
<
Tip
info
=
{{
title
:
'
Эмодзи
'
,
hint
:
'
Вставить символ из набора
'
}}
>
<
div
<
div
className
=
{
'
qicon
'
+
(
isOpen
?
'
active
'
:
''
)}
className
=
{
'
qicon
'
+
(
isOpen
?
'
active
'
:
''
)}
title
=
'
Эмодзи
'
role
=
'
button
'
tabIndex
=
{
0
}
aria
-
label
=
'
Эмодзи
'
aria
-
expanded
=
{
isOpen
}
onMouseDown
=
{(
event
)
=>
{
onMouseDown
=
{(
event
)
=>
{
event
.
preventDefault
()
event
.
preventDefault
()
}}
}}
...
@@ -254,6 +264,7 @@ const EmojiPicker = ({ editor }) => {
...
@@ -254,6 +264,7 @@ const EmojiPicker = ({ editor }) => {
>
>
<
Icon
type
=
'
emoji
'
/>
<
Icon
type
=
'
emoji
'
/>
<
/div
>
<
/div
>
<
/Tip
>
{
isOpen
&&
(
{
isOpen
&&
(
<
div
<
div
className
=
'
atma-editor-emoji-popover
'
className
=
'
atma-editor-emoji-popover
'
...
...
src/components/ToolBar.js
View file @
b1578e7d
...
@@ -26,6 +26,7 @@ const HEADINGS = { type: 'g', items: ['paragraph', 'h2', 'h3', 'h4'] }
...
@@ -26,6 +26,7 @@ const HEADINGS = { type: 'g', items: ['paragraph', 'h2', 'h3', 'h4'] }
const
TABLE_GROUP
=
{
const
TABLE_GROUP
=
{
type
:
'
s
'
,
type
:
'
s
'
,
toggle
:
false
,
toggle
:
false
,
tip
:
'
table
'
,
items
:
[
items
:
[
'
insertTable
'
,
'
|
'
,
'
insertTable
'
,
'
|
'
,
'
addRowBefore
'
,
'
addRowAfter
'
,
'
deleteRow
'
,
'
|
'
,
'
addRowBefore
'
,
'
addRowAfter
'
,
'
deleteRow
'
,
'
|
'
,
...
@@ -81,10 +82,10 @@ const LESSON = [
...
@@ -81,10 +82,10 @@ const LESSON = [
{
type
:
'
g
'
,
items
:
[
'
|
'
]
},
{
type
:
'
g
'
,
items
:
[
'
|
'
]
},
{
type
:
'
g
'
,
items
:
[
'
bulletList
'
,
'
orderedList
'
]
},
{
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
,
TABLE_GROUP
,
{
type
:
'
g
'
,
items
:
[
'
emoji
'
]
},
{
type
:
'
g
'
,
items
:
[
'
emoji
'
]
},
...
@@ -102,7 +103,7 @@ const LESSON = [
...
@@ -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
'
]
},
items
:
[
'
video
'
,
'
iframe
'
,
'
iframe_custom
'
,
'
voicemessage
'
,
'
webcamera
'
,
'
screencust
'
]
},
// ── «Оживить модуль» ─────────────────────────────────────────────────────
// ── «Оживить модуль» ─────────────────────────────────────────────────────
...
@@ -122,7 +123,7 @@ const LESSON = [
...
@@ -122,7 +123,7 @@ const LESSON = [
// четыре разные вещи со словом «pdf» в подписи. Рядом, с человеческими
// четыре разные вещи со словом «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
'
]
},
{
type
:
'
g
'
,
items
:
[
'
link
'
]
},
]
]
...
@@ -139,7 +140,7 @@ const LESSON = [
...
@@ -139,7 +140,7 @@ const LESSON = [
*/
*/
const
LESSON_NARROW
=
[
const
LESSON_NARROW
=
[
{
type
:
'
g
'
,
items
:
[
'
undo
'
,
'
redo
'
]
},
{
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
:
'
g
'
,
items
:
[
'
bulletList
'
,
'
orderedList
'
]
},
{
type
:
'
s
'
,
toggle
:
true
,
items
:
[
'
alignLeft
'
,
'
alignCenter
'
,
'
alignRight
'
]
},
{
type
:
'
s
'
,
toggle
:
true
,
items
:
[
'
alignLeft
'
,
'
alignCenter
'
,
'
alignRight
'
]
},
{
type
:
'
g
'
,
items
:
[
'
link
'
]
},
{
type
:
'
g
'
,
items
:
[
'
link
'
]
},
...
@@ -317,9 +318,85 @@ const TOOL_INFO = {
...
@@ -317,9 +318,85 @@ const TOOL_INFO = {
},
},
insertMore
:
{
title
:
'
Вставить
'
,
hint
:
'
Всё, что кладёт в текст новый блок: картинка, видео, задание, файл
'
},
insertMore
:
{
title
:
'
Вставить
'
,
hint
:
'
Всё, что кладёт в текст новый блок: картинка, видео, задание, файл
'
},
more
:
{
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
:
'
Отменить
'
},
undo
:
{
title
:
'
Отменить
'
},
redo
:
{
title
:
'
Вернуть
'
},
redo
:
{
title
:
'
Вернуть
'
},
emoji
:
{
title
:
'
Эмодзи
'
,
hint
:
'
Вставить символ из набора
'
},
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 = {} }) =
...
@@ -376,15 +453,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
* Хост (у него есть formatjs и словари) передаёт переведённые строки пропом.
* Хост (у него есть formatjs и словари) передаёт переведённые строки пропом.
* Нет пропа — работают встроенные русские, как раньше.
* Нет пропа — работают встроенные русские, как раньше.
*/
*/
const
infoFor
=
(
type
,
item
)
=>
{
const
infoFor
=
(
type
,
item
)
=>
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
,
}
}
const
presetName
=
(
toolsOptions
&&
toolsOptions
.
type
)
?
toolsOptions
.
type
:
'
all
'
const
presetName
=
(
toolsOptions
&&
toolsOptions
.
type
)
?
toolsOptions
.
type
:
'
all
'
...
@@ -648,13 +717,40 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
...
@@ -648,13 +717,40 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
}}
>
}}
>
<
Fragment
>
<
Fragment
>
{
{
<
div
className
=
{
'
qicon
'
+
(
activeType
!==
false
&&
section
.
toggle
!==
false
?
'
active
'
:
''
)}
>
// Подсказка у кнопки, открывающей список.
<
Icon
type
=
{
// Ключ берётся из `tip` секции; нет его —
section
.
title
// из первого пункта, чтобы кнопка не осталась
// совсем без подписи.
(()
=>
{
const
tipKey
=
section
.
tip
||
section
.
title
||
section
.
items
[
0
];
const
tip
=
infoFor
(
tipKey
,
toolsLib
[
tipKey
]);
const
iconType
=
section
.
title
?
section
.
title
?
section
.
title
:
(
activeType
===
false
||
section
.
toggle
===
false
?
section
.
items
[
0
]
:
activeType
)
:
(
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
>
<
/div
>
<
/Tooltip
>
);
})()
}
}
<
div
<
div
className
=
{
'
atma-editor-toolbar-s-opts
'
}
className
=
{
'
atma-editor-toolbar-s-opts
'
}
...
@@ -684,10 +780,11 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
...
@@ -684,10 +780,11 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
<
div
className
=
"
atma-editor-toolbar
"
ref
=
{
rootRef
}
>
<
div
className
=
"
atma-editor-toolbar
"
ref
=
{
rootRef
}
>
{
getItems
()
}
{
getItems
()
}
{
onInfo
&&
(
{
onInfo
&&
(
<
Tip
info
=
{
infoFor
(
'
help
'
,
{
title
:
'
Справка по редактору
'
})}
>
<
button
<
button
type
=
"
button
"
type
=
"
button
"
className
=
"
atma-editor-toolbar-info
"
className
=
"
atma-editor-toolbar-info
"
title
=
"
Справка по редактору
"
aria
-
label
=
"
Справка по редактору
"
onClick
=
{
onInfo
}
onClick
=
{
onInfo
}
>
>
<
svg
width
=
"
18
"
height
=
"
18
"
viewBox
=
"
0 0 18 18
"
fill
=
"
none
"
xmlns
=
"
http://www.w3.org/2000/svg
"
>
<
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 = {} }) =
...
@@ -696,6 +793,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
<
circle
cx
=
"
9
"
cy
=
"
5.5
"
r
=
"
0.75
"
fill
=
"
currentColor
"
/>
<
circle
cx
=
"
9
"
cy
=
"
5.5
"
r
=
"
0.75
"
fill
=
"
currentColor
"
/>
<
/svg
>
<
/svg
>
<
/button
>
<
/button
>
<
/Tip
>
)}
)}
<
/div
>
<
/div
>
)
)
...
...
src/icons.js
View file @
b1578e7d
...
@@ -113,6 +113,34 @@ const ICONS = {
...
@@ -113,6 +113,34 @@ const ICONS = {
export
default
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
...
@@ -123,6 +151,15 @@ export default ICONS
* Рисунок 20×20 внутри кнопки 32×32 — полезная площадь из макета.
* Рисунок 20×20 внутри кнопки 32×32 — полезная площадь из макета.
*/
*/
export
const
Icon
=
({
type
,
size
=
20
})
=>
{
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
]
const
body
=
ICONS
[
type
]
if
(
!
body
)
{
if
(
!
body
)
{
return
null
return
null
...
...
src/index.scss
View file @
b1578e7d
...
@@ -376,10 +376,23 @@ body{
...
@@ -376,10 +376,23 @@ body{
height: 8px;
height: 8px;
}
}
// ── Список открывается ПОД кнопкой ───────────────────────────
//
// Здесь не было `top`, и абсолютно позиционированный список
// вставал в своё «статическое» место — то есть по вертикальному
// центру кнопки, потому что группа выровнена `align-items: center`.
// У короткого списка сдвиг незаметен, а список таблицы из
// одиннадцати пунктов уезжал НАД панелью и накрывал текст модуля.
//
// `max-height` со прокруткой — на случай длинного списка внизу
// экрана: лучше прокрутить, чем показать половину за краем окна.
&-opts{
&-opts{
position: absolute;
position: absolute;
clip: rect(0,0,0,0);
top: calc(100% + 6px);
display: none;
width: 260px;
width: 260px;
max-height: min(60vh, 420px);
overflow-y: auto;
border: 1px solid #E3E7EC;
border: 1px solid #E3E7EC;
border-radius: 12px;
border-radius: 12px;
box-shadow: 0 12px 32px 0 rgba(15, 20, 25, 0.12);
box-shadow: 0 12px 32px 0 rgba(15, 20, 25, 0.12);
...
@@ -452,18 +465,24 @@ body{
...
@@ -452,18 +465,24 @@ body{
}
}
}
}
// В списке уровень показан САМИМ размером подписи — это
// нагляднее любой пометки. Поэтому и значок, и буквенная
// подпись здесь лишние: «H2» рядом с крупным «Заголовок
// раздела» повторяет то, что уже видно.
&[data-type="h2"], &[data-type="h3"], &[data-type="h4"]{
&[data-type="h2"], &[data-type="h3"], &[data-type="h4"]{
font-weight: 500;
font-weight: 500;
padding: 8px;
padding: 8px;
.qicon{
.qicon,
.qicon-text{
display: none;
display: none;
}
}
}
}
&[data-type="paragraph"]{
&[data-type="paragraph"]{
padding: 8px;
padding: 8px;
.qicon{
.qicon,
.qicon-text{
display: none;
display: none;
}
}
}
}
...
@@ -498,8 +517,11 @@ body{
...
@@ -498,8 +517,11 @@ body{
}
}
&.show{
&.show{
// Было `clip: rect(0,0,0,0)` / `clip: auto`. Свойство устарело
// и, главное, не уживается с прокруткой внутри списка:
// обрезка считается от исходной коробки, а не от прокрученной.
#{
$this
}
-opts{
#{
$this
}
-opts{
clip: auto
;
display: block
;
}
}
&:after{
&:after{
...
@@ -539,6 +561,19 @@ body{
...
@@ -539,6 +561,19 @@ body{
pointer-events: none;
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{
&:hover{
cursor: pointer;
cursor: pointer;
background-color: #EDF1F6;
background-color: #EDF1F6;
...
@@ -970,13 +1005,21 @@ body{
...
@@ -970,13 +1005,21 @@ body{
margin-bottom: 10px;
margin-bottom: 10px;
}
}
// Выделение файла рисуется рамкой СНАРУЖИ плитки (`box-shadow` без
// inset), а список прокручиваемый. `overflow-y: auto` по стандарту
// делает `auto` и по второй оси — значит рамка у крайних плиток
// обрезалась: сверху, снизу и по бокам. На снимке автора это видно
// как «выделение подрезано».
//
// Отступ в 3 px даёт рамке место со всех сторон. Справа больше:
// там ещё полоса прокрутки.
&-list{
&-list{
display: grid;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 10px;
gap: 10px;
max-height: min(280px, calc(90dvh - 440px));
max-height: min(280px, calc(90dvh - 440px));
overflow-y: auto;
overflow-y: auto;
padding
-right: 4
px;
padding
: 3px 7px 3px 3
px;
&.is-video{
&.is-video{
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment