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

Панель на телефоне: два ряда вместо пяти



Полный набор на 390 px — это 25 кнопок по 44 px, пять рядов и 234 px:
больше половины экрана до первой строки текста. Замер, не оценка.

Частое остаётся на виду, редкое уходит в два списка с подписями словами —
«Вставить» и «Ещё». Стало 98 px. Горизонтальной прокрутки нет намеренно:
кнопка за правым краем на телефоне не находится вообще, методист не знает,
что там что-то есть.

Набор выбирается по ширине САМОЙ панели через ResizeObserver, а не по ширине
окна: в админке панель 1080 при окне 1600, и по окну решение было бы неверным.

Отдельно: умолчания больше не подсвечиваются активными. «Обычный текст»
и «по левому краю» редактор честно считает применёнными, но применил их
не автор. Пока подсветка была бледно-серой, это не мешало; теперь она ярко-синяя,
и пустой урок встречал автора двумя горящими кнопками, которых он не нажимал.

Два глифа («Вставить», «Ещё») дорисованы в языке набора: в поставке их нет,
макет описывает эти списки словами.
Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 985f276c
import React, { Fragment, useState } from 'react'; import React, { Fragment, useEffect, useRef, useState } from 'react';
import { Tooltip } from 'antd'; import { Tooltip } from 'antd';
import { Icon } from '../icons'; import { Icon } from '../icons';
...@@ -97,11 +97,48 @@ const LESSON = [ ...@@ -97,11 +97,48 @@ const LESSON = [
{ type: 'g', items: ['link', 'file', 'iframe_custom'] }, { type: 'g', items: ['link', 'file', 'iframe_custom'] },
] ]
/**
* Урок на узкой панели.
*
* На телефоне полный набор — это 25 кнопок по 44 px, то есть пять рядов
* и 234 px: больше половины экрана до первой строки текста. Замер, не оценка.
*
* Частое остаётся на виду, редкое уходит в два списка с подписями словами.
* Горизонтальной прокрутки нет намеренно: кнопка, скрытая справа за краем,
* на телефоне не находится вообще — методист не знает, что там что-то есть,
* и не тянет.
*/
const LESSON_NARROW = [
{ type: 'g', items: ['undo', 'redo'] },
{ type: 's', toggle: true, items: ['paragraph', 'h2', 'h3', 'h4'] },
{ type: 'g', items: ['bulletList', 'orderedList'] },
{ type: 's', toggle: true, items: ['alignLeft', 'alignCenter', 'alignRight'] },
{ type: 'g', items: ['link'] },
// Вставить: всё, что кладёт в урок новый блок
// title — иконка свёрнутой кнопки. Без него ею стала бы первая в списке
// («картинка»), и «Вставить» читалось бы как «вставить картинку».
{ type: 's', toggle: false, title: 'insertMore',
items: ['image', 'video', 'iframe', 'audio', 'interactiveImage',
'activityRef', 'insertToggleBlock', 'buttonLink', 'file', 'insertTable'] },
// Ещё: редкое и служебное
{ type: 's', toggle: false, title: 'more',
items: ['blockquote', 'hr', 'codeBlock', 'emoji', 'clearMarks',
'voicemessage', 'webcamera', 'screencust',
'iframe_pptx', 'iframe_pdf', 'pdf', 'export_pdf', 'iframe_custom'] },
]
const toolsInit = { const toolsInit = {
// Урок: полный набор. Размера шрифта здесь нет намеренно — см. toolsLib. // Урок: полный набор. Размера шрифта здесь нет намеренно — см. toolsLib.
'lesson': LESSON, 'lesson': LESSON,
'all': LESSON, 'all': LESSON,
// Узкие варианты. Выбираются по ширине САМОЙ панели, а не окна: панель
// в админке 1080 при окне 1600, и по окну решение было бы неверным.
'lesson-narrow': LESSON_NARROW,
'all-narrow': LESSON_NARROW,
// Описание модуля, инфо, профессии, маркет, база знаний в админке // Описание модуля, инфо, профессии, маркет, база знаний в админке
'text': [ 'text': [
{ type: 'g', items: ['undo', 'redo'] }, { type: 'g', items: ['undo', 'redo'] },
...@@ -237,12 +274,53 @@ const TOOL_INFO = { ...@@ -237,12 +274,53 @@ const TOOL_INFO = {
title: 'Задание в уроке', title: 'Задание в уроке',
hint: 'Поставить тест, эссе или видеоответ прямо в нужном месте урока, а не в конце', hint: 'Поставить тест, эссе или видеоответ прямо в нужном месте урока, а не в конце',
}, },
insertMore: { title: 'Вставить', hint: 'Всё, что кладёт в урок новый блок: картинка, видео, задание, файл' },
more: { title: 'Ещё', hint: 'Редкое и служебное: цитата, код, записи, документы' },
undo: { title: 'Отменить' }, undo: { title: 'Отменить' },
redo: { title: 'Вернуть' }, redo: { title: 'Вернуть' },
emoji: { title: 'Эмодзи', hint: 'Вставить символ из набора' }, emoji: { title: 'Эмодзи', hint: 'Вставить символ из набора' },
} }
/** Ширина панели, ниже которой берётся узкий набор */
const NARROW_AT = 640
/**
* Кнопки, которые НИКОГДА не подсвечиваются активными.
*
* Это состояния по умолчанию: абзац — обычный текст, выравнивание — по левому
* краю. Редактор честно сообщает, что они «применены», но применил их не автор.
*
* Пока активное состояние было бледно-серым, это никому не мешало. Теперь оно
* ярко-синее, и на пустом уроке панель встречала автора двумя горящими
* кнопками — при том что он ничего не нажимал. Подсветка должна означать
* «вы это выбрали», иначе она не значит ничего.
*/
const NEVER_ACTIVE = ['paragraph', 'alignLeft']
const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) => { const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) => {
const rootRef = useRef(null)
const [narrow, setNarrow] = useState(false)
// Решение по ширине САМОЙ панели, а не окна: панель в админке ограничена
// рамкой редактора и бывает уже окна вдвое. ResizeObserver, а не событие
// resize, — панель меняет ширину и без изменения размера окна: свернули
// боковое меню, открыли предпросмотр.
useEffect(() => {
const node = rootRef.current
if (!node || typeof ResizeObserver === 'undefined') {
return
}
const observer = new ResizeObserver((entries) => {
const width = entries[0] && entries[0].contentRect ? entries[0].contentRect.width : 0
if (width > 0) {
setNarrow(width < NARROW_AT)
}
})
observer.observe(node)
return () => observer.disconnect()
}, [])
if (!editor) { if (!editor) {
return null return null
} }
...@@ -267,10 +345,21 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) = ...@@ -267,10 +345,21 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
} }
} }
const [toolbarItems, setToolbarItems ] = useState((toolsOptions && toolsOptions.type ? toolsInit[toolsOptions.type] : toolsInit['all'])) const presetName = (toolsOptions && toolsOptions.type) ? toolsOptions.type : 'all'
// Узкий набор есть не у каждого контекста: у комментария наставника
// и ответа учащегося кнопок и так немного, полный набор там влезает.
const toolbarItems = (narrow && toolsInit[presetName + '-narrow'])
? toolsInit[presetName + '-narrow']
: (toolsInit[presetName] || toolsInit['all'])
const isActive = (type) => { const isActive = (type) => {
// См. NEVER_ACTIVE: умолчание — не выбор автора
if (NEVER_ACTIVE.indexOf(type) !== -1) {
return false;
}
if(type === 'h2' || type === 'h3' || type === 'h4'){ if(type === 'h2' || type === 'h3' || type === 'h4'){
return editor.isActive('heading', {level: parseInt(type.replace(/[a-z]/, ''))}); return editor.isActive('heading', {level: parseInt(type.replace(/[a-z]/, ''))});
} }
...@@ -519,7 +608,11 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) = ...@@ -519,7 +608,11 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
<Fragment> <Fragment>
{ {
<div className={'qicon' + (activeType !== false && section.toggle !== false ? ' active' : '')}> <div className={'qicon' + (activeType !== false && section.toggle !== false ? ' active' : '')}>
<Icon type={activeType === false || section.toggle === false ? section.items[0] : activeType} /> <Icon type={
section.title
? section.title
: (activeType === false || section.toggle === false ? section.items[0] : activeType)
} />
</div> </div>
} }
<div <div
...@@ -547,7 +640,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) = ...@@ -547,7 +640,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions, onInfo, labels = {} }) =
} }
return ( return (
<div className="atma-editor-toolbar"> <div className="atma-editor-toolbar" ref={rootRef}>
{ getItems() } { getItems() }
{onInfo && ( {onInfo && (
<button <button
......
...@@ -82,6 +82,14 @@ const ICONS = { ...@@ -82,6 +82,14 @@ const ICONS = {
video: '<path d="M5.2 5.5h13.6a2.2 2.2 0 0 1 2.2 2.2v8.6a2.2 2.2 0 0 1 -2.2 2.2h-13.6a2.2 2.2 0 0 1 -2.2 -2.2v-8.6a2.2 2.2 0 0 1 2.2 -2.2z"></path><path d="M7.2 5.5v13"></path><path d="M16.8 5.5v13"></path><path d="M3 12h4.2"></path><path d="M16.8 12h4.2"></path><path d="M10.6 9.5l3.6 2.5-3.6 2.5z"></path>', video: '<path d="M5.2 5.5h13.6a2.2 2.2 0 0 1 2.2 2.2v8.6a2.2 2.2 0 0 1 -2.2 2.2h-13.6a2.2 2.2 0 0 1 -2.2 -2.2v-8.6a2.2 2.2 0 0 1 2.2 -2.2z"></path><path d="M7.2 5.5v13"></path><path d="M16.8 5.5v13"></path><path d="M3 12h4.2"></path><path d="M16.8 12h4.2"></path><path d="M10.6 9.5l3.6 2.5-3.6 2.5z"></path>',
voicemessage: '<path d="M12 3.4h0a2.6 2.6 0 0 1 2.6 2.6v4.3999999999999995a2.6 2.6 0 0 1 -2.6 2.6h0a2.6 2.6 0 0 1 -2.6 -2.6v-4.3999999999999995a2.6 2.6 0 0 1 2.6 -2.6z"></path><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0"></path><path d="M12 17.1v3.3"></path><path d="M9 20.4h6"></path>', voicemessage: '<path d="M12 3.4h0a2.6 2.6 0 0 1 2.6 2.6v4.3999999999999995a2.6 2.6 0 0 1 -2.6 2.6h0a2.6 2.6 0 0 1 -2.6 -2.6v-4.3999999999999995a2.6 2.6 0 0 1 2.6 -2.6z"></path><path d="M6.5 11.6a5.5 5.5 0 0 0 11 0"></path><path d="M12 17.1v3.3"></path><path d="M9 20.4h6"></path>',
webcamera: '<path d="M6 6h12a2.5 2.5 0 0 1 2.5 2.5v7a2.5 2.5 0 0 1 -2.5 2.5h-12a2.5 2.5 0 0 1 -2.5 -2.5v-7a2.5 2.5 0 0 1 2.5 -2.5z"></path><path d="M8.7 12a3.3 3.3 0 1 0 6.6 0a3.3 3.3 0 1 0 -6.6 0z"></path><path d="M16.3 8.7a0.8 0.8 0 1 0 1.6 0a0.8 0.8 0 1 0 -1.6 0z" fill="currentColor" stroke="none"></path>', webcamera: '<path d="M6 6h12a2.5 2.5 0 0 1 2.5 2.5v7a2.5 2.5 0 0 1 -2.5 2.5h-12a2.5 2.5 0 0 1 -2.5 -2.5v-7a2.5 2.5 0 0 1 2.5 -2.5z"></path><path d="M8.7 12a3.3 3.3 0 1 0 6.6 0a3.3 3.3 0 1 0 -6.6 0z"></path><path d="M16.3 8.7a0.8 0.8 0 1 0 1.6 0a0.8 0.8 0 1 0 -1.6 0z" fill="currentColor" stroke="none"></path>',
// ── Дорисовано под мобильную панель ──────────────────────────────────────
//
// В поставке этих двух глифов нет: макет описывает списки «Вставить»
// и «Ещё» словами, но иконок для свёрнутых кнопок не даёт. Нарисованы
// в том же языке — сетка 24×24, штрих 1.5, скруглённые концы, — чтобы
// не выбиваться из набора.
insertMore: '<path d="M5 3.5h14a1.5 1.5 0 0 1 1.5 1.5v14a1.5 1.5 0 0 1 -1.5 1.5h-14a1.5 1.5 0 0 1 -1.5 -1.5v-14a1.5 1.5 0 0 1 1.5 -1.5z"></path><path d="M12 8.5v7"></path><path d="M8.5 12h7"></path>',
more: '<path d="M5.2 12a1.3 1.3 0 1 0 2.6 0a1.3 1.3 0 1 0 -2.6 0z"></path><path d="M10.7 12a1.3 1.3 0 1 0 2.6 0a1.3 1.3 0 1 0 -2.6 0z"></path><path d="M16.2 12a1.3 1.3 0 1 0 2.6 0a1.3 1.3 0 1 0 -2.6 0z"></path>',
} }
export default ICONS export default ICONS
......
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