Здравствуйте, меня зовут Дмитрий Карловский, и однажды я создал самый удобный редактор богатого текста. Попробовать его вы нигде не сможете, зато сможете узнать какого счастье вы лишились..

PiterUX🖐🏻
Это — текстовая расшифровка одноимённого выступления на PiterUX🖐🏻. Вы можете глянуть его в записи трансляции:
Либо читать далее как статью…
Дмитрий Карловский
Автор сих строк большую часть жизни работал фронтендером, и, как любой профессионал, осваивал смежные области знаний. В частности, это вылилось в концепцию математически выверенного дизайна и другие идеи, автоматизирующие лучшие дизайнерские практики.

|
Mathematics Driven Design: Размеры и отступы Проектирование гипер-адаптивной UI-библиотеки |
Плоский текст
Сегодня мы поговорим про редактирование текста. Казалось бы, тут уже всё давно устоялось, и нельзя сказать ничего нового. Это просто буковки в 1 или несколько строк.
Однострочный текст…
Много
строчный
текст
Легковесная разметка
Однако, разные буковки имеют разную семантику, которую надо как-то донести до читателя. Самый простой способ — использовать какую-либо разметку текста. Например, MarkedText:
= Плюсы + Точность намерений + //Простота// реализации -- = Минусы - ++Ограниченный++ визуал - --Визуальный-- шум - ;;Никакого;; медиа
Пока пользователь редактирует текст с этой разметкой, в дополнительной панели может отображаться результат её визуализации:
Плюсы
🟢 Точность намерений
🟢 Простота реализацииМинусы
🔴 Ограниченный визуал
🔴Визуальныйшум
🔴Никакогомедиа
И не смотря на точность выражения намерений, редактировать таким образом тексты не очень-то удобно из-за визуального шума от разметки и необходимости переключать внимание между панелями редактирования и просмотра.
Богатый текст
Тут нам приходит концепция WYSIWYG, где ты можешь редактировать то, что видишь, напрямую, а результат изменений, соответственно, видишь сразу в месте редактирования.

🟢 Неограниченный визуал
🟠 Трудность реализации
🔴 Неточность намерений
Реализовать такое существенно сложнее, зато для пользователя это существенно удобнее. Правда у современных таких редакторов есть существенные проблемы с пользовательским опытом, о чём мы и поговорим далее.
Проект мечты
Однажды ко мне пришёл 1C, и попросил сделать для их платформы редактор богатого текста. А я уже давно хотел что-то такое сделать, и тут звёзды сошлись. Для тех, кто не в курсе, 1C — это не просто софтина для бухгалтеров, а платформа, с кучей компонент, из которых можно собирать самые разнообразные бизнес-приложения.

Сценарии использования
Использовать редактор планировалось в самых разных контекстах: от ввода комментария к товару в интернет магазине, дло редактирования договоров на сотни страниц с pixel-perfect печатью.
Редактор комментария:
короткий текст, мало местаРедактор письма:
самодостаточный текстРедактор статьи:
текст с гиперсвязямиРедактор документа:
жёсткое оформлениеРедактор шаблона:
документ с плейсхолдерамиФорма ввода:
ограниченный текст в плейсхолдерахВключение шаблона:
вставка другого документа
Поэтому редактор должен был быть достаточно мощным и гибким, но в то же время легковесным и лаконичным. Задача выглядела неподъёмной, поэтому я, конечно же, за неё взялся.
Нельзя просто так взять и вывести текст
Просто вывести текст оказалось не так-то просто. Вот примерные шаги, необходимые для вывода простых буковок:
Разбить на токены
Вычислить их ширины
Разбить на строки
Вычислить их высоты
Посчитать положения
Отсеять невидимое
Отрендерить по координатам
Обычные браузерные механизмы нам не подходили из соображения перфоманса и точности печати. Это усложнило разработку, зато дало возможность полностью контролировать поведение и визуализацию, а значит и пробовать свежие идеи.
Длинные слова в узких джин… колонках
Проведя небольшое исследование, я сравнил самые длинные слова из словарей и самые маленькие экраны мобилок.
частнопредпринимательского — 26
antidisestablishmentarianism — 28
Donaudampfschiffahrtsgesellschaftskapitаn — 41
Влезает на самый маленький экран: 32+
Выяснилось, что просто переноса по словам почти достаточно, но в ряде случаев слово может не влезть целиком. Особенно, если это заголовок. И тут надо было принимать решение, как редактор должен вестис себя в этих краевых случаях.
Стратегии обрезания
У дизайнеров довольно популярен паттерн с обрезанием лишнего. Как пользователя, он меня вымораживает. Но в случае редактирования, это решение совсем не подходит, так как пользователь должен видеть что редактирует.
Обрезать

Скроллить

Затенять

Многоточить

И даже скроллинг не спасает. Он лишь меняет одну проблему на другую — необходимость мотать скроллбаром туда-сюда.
Стратегии переноса
Можно разрывать слова и переносить их на несколько строк, но тогда их сложнее читать: ты не воспринимаешь их как единый образ, а спотыкаешься на чтении отдельных частей и сборкой их в уме в единое слово.
Переносить где попало

Переносить по правилам

Стратегии скукоживания
Чтобы слово влезло целиком, можно уменьшить размер шрифта, но тогда появятся дырки между строк, а заголовок рискует стать меньше основного текста.
Уменьшать шрифт

В итоге мы остановились на весьма необычном подходе — сжимать буквы по ширине:
Уменьшать ширину

До двух крат буквы вполне можно сжимать без существенных проблем с читаемостью, а выглядит это куда опрятней всех остальных вариантов. Таким образом даже самое длинное слово у нас всегда помещается даже в самый маленький экран.
1C ненавидит пробелы
И тут я залезаю в 1C, нахожу там самый длинный идентификатор и понимаю, что он никуда не влезает:
ПоддерживатьСоответствиеОбъектамРасширяемойКонфигурацииПоВнутреннимИдентификаторам
Благо это PascalCase, а значит переносить можно по словам, просто разбивать на слова надо не только по пробелам:
ПоддерживатьСоответствиеОбъектамРасширяемойКонфигурацииПоВнутренним Идентификаторам
Каретка между символами
Ну ладно, текст мы вывели. Теперь пользователю нужно сказать, где он будет редактировать текст. Для этого ему нужна возможность поставить каретку в любое, нужное ему место:

Обратите внимание, что каретка ставится между букв, между слов и по краям параграфа, но не вначале и конце слова, чтобы не было трёх логических позиций в одном и том же месте, вместо одной.
Разный визуал для одной позиции каретки
Однако, если у нас случится перенос между словами, то у нас появится 2 разные визуальные позиции для одной логической. Поэтому у каретки должен быть так называемый спин — специальное состояние, которое указывает, где лучше рендерить каретку: прижимая к предыдущему слову или к следующему:

Каретка между блоками тоже
Если же мы нажмём Enter, это разорвёт параграф на два и у нас появится целых три позиции между словами: в конце одного параграфа, в начале другого, и… между параграфами.

Тут вы, возможно, возмутитесь: «это что ещё за дичь?». Однако, освоившись с такой кареткой, которую можно ставить не только между букв и слов, но и между блоками, вы уже будете задаваться вопросом «как я раньше-то жил без этого?», ибо это очень удобно благодаря тому, что пользователь может куда более чётко выражать свои намерения по редактированию текста.
Например, чтобы добавить пункт в начало списка, достаточно поставить туда каретку и начать вводить текст, вместо того, чтобы ставить каретку на начало первого пункта, жать Enter для добавления нового пункта перед первым, после чего Up для перехода в этот новый элемент.
Разные позиции каретки визуально в одной точке
Та же логика работает и с инлайн форматированием, но тут есть проблема: логически это могут быть 3 разные позиции, но визуально они все располагаются в одной точке:

Современные редакторы никак не решают это проблему, из-за чего пользователь никогда не понимает, что произойдёт, когда он поставит каретку между жирным и курсивом и начнёт вводить текст. А это фрустрирует, когда ожидание не сходится с реальностью.
Стилизация каретки под инлайн форматирование
Лучшее решение — дать пользователю обратную связь и стилизовать каретку в зависимости от того форматирования в котором она находится.

Тут по виду каретки пользователь всегда чётко понимает в какой именно логической позиции находится каретка, может между ними перемещаться и получать предсказуемый результат.
В начале/конце блока и между блоками
Соответственно, каждый тип форматирования должен влиять лишь на свой аспект визуализации каретки. Тогда их можно комбинировать.

Тут по виду каретки мы не только понимаем где она находится, но даже можем различать, например, когда акцент находится внутри ссылки, а когда наоборот.
Призрачная для курсора, не меняя курсора
При наведении на редактируемый блок обычно курсор меняется на символ каретки, показывая тем самым, что клик приведёт к установке каретки в… а вот куда не очень-то понятно. В этом примере есть целых три позиции куда может быть установлена каретка:
Традиционный курсор

Пользователь кликает, понимает, что попал не туда, начинает исправлять. Мы привыкли не замечать эти проблемы, но они есть, и они создают лёгкую фрустрацию каждый раз.
Лучшее решение тут не менять курсор, а рисовать призрачную каретку, которая показывает куда на самом деле будет установлена каретка при клике.
Призрачная каретка

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

К сожалению, в текстовых редакторах для обычных пользователей такое почти встречается.
Другой пример множественных кареток — каретки других пользователей при совместном редактировании, отображаемые разными цветами:
Каретки других пиров

Разные направления письма
Наконец, одна из выносящих мозг проблем современных редакторов — управление кареткой в тексте со смешенным направлением письма. Когда вы навигируетесь по тексту стрелочками, то перемещаетесь по логическим позициям, но визуально они могут располагаться в обратном порядке. Когда нажимаешь вправо, а каретка идёт то вправо, то влево — это просто вымораживает.

Лучшее решение тут перемещать каретку в соответствии с визуальными расположением. То есть выделяя, например, текст слева направо, в какой-то момент у нас должна выделиться вся арабская вязь, которая постепенно будет уменьшаться, чтобы потом снова целиком выделиться, когда мы перейдём в английским буквам. Это даёт более предсказуемое поведение.
На диаграмме хорошо видно, что логически цельный блок текста визуально состоит из двух частей, где конец визуально находится вообще где-то в середине.
Выделение
Ну и, кстати, о выделении — это обобщение над кареткой. Каретка — это просто выделение, где начало и конец совпадают. Так что всё, что мы говорили про позиции каретки справедливо и про границы выделения.
🔵 Начало и конец
🔵 Строчное и блочное
🔵 Множественное и чужое
Из интересной особенности именно выделения — оно бывает строчное (текстовое) и блочное, выделяющее группу блоков прямоугольником. Эта разница актуальна в случае таблиц.
Характеристики текста
Когда мы рендерим текст, у него есть множество разных свойств:
Буквы
Шрифт и размер
Цвет текста и фона
Толщина и наклонность
Межсимвольный интервал
Лигатурность
Декорации
Блоки
Выравнивание вертикальное
Выравнивание горизонтальное
Отступы внешние и внутренние
Межстрочный интервал
Стилизация фона
Стилизация рамочки
Типичный результат
Если дать пользователю возможность контролировать эти свойства рендеринга для каждой буквы, то пользователь обязательно сделает что-то такое:

Пример, конечно, утрированный, но он показывает, что пользователь — плохой дизайнер. А даже если и хороший, то при копипасте богатого текста из разных мест, получается лоскутное одеяло из разных стилей, исправлять которые в большом документе — настоящий ад.
Типичный текстовый процессор
Кроме того, интерфейс управления стилями текста превращается в пульт управления звездолётом, котором, не смотря на свою громоздкость, ещё и потребовался отдельный таббар.

Семантика против визуала
Лучшее решение — отделять семантику от оформления, чтобы при редактировании текста пользователь не думал о его оформлении, а оформление задавалось дизайнером под конкретный контекст вывода. Будь то светлая или тёмная тема, печатный лист или бил-борд, для дальтоников или астигматиков.
Смешение
🔴 Куча панелей с кучей кнопок
🔴 Лоскутное одеяло из разных стилей
🔴 Ад массового изменения стилей
🔴 Невозможно учесть все контексты вывода
🔴 Пользователь — плохой дизайнер
Разделение
🟢 Отдельный конфигуратор стилей
🟢 Всегда консистентный вид
🟢 Единые стандарты для многих текстов
🟢 Разный визуал под разные контексты
🟢 Выверенный дизайнером визуал
Минимальный набор инструментов
Для контроля семантики достаточно весьма скромного набора инструментов, которые легко уместить в одну строку кнопок. Например, базовый набор может выглядеть так:

🔨 Переключатели: акцент, иносказание, цитата, код.
🔨 Оборачиватели: секция, список, цитата, блок кода.
🔨 История: отмена, возврат.
🔨 Поиск: поле запроса, поле замены, следующее, предыдущее, всё.
🔨 Прочее: разворачивание, стирание форматирования.
Редактирование без инструментов
Для многих сценариев редактирования даже специальные кнопки не нужны — достаточно умной работы с выделением:
Вставка ссылки в выделение делает выделенное кликабельным.
Вставка медиа загружает файл и вставляет как медиа-блок.
Энтер разрывает текущий параграф или вставляет новый.
Удаление между одинаковыми блоками объединяет их.
Каретки между строк/ячеек и блочного выделения достаточно для таблиц.
💡 Вставка ссылки и медиа
💡 Enter разрывает блок
💡 Delete объединяет блоки
💡 Каретки даже в таблицах
Перемотка истории
Ну и под конец не могу не сказать про навигацию по истории редактирования. Частая проблема выглядит так: отменил пачку изменений, случайно нажал клавишу и редактор стёр всю историю, не давая её накатить обратно. Далется так обычно потому, что состояние документа изменилось, а редактор не может накатить историю на новое состояние. Тут важно хранить текст в правильных структурах данных, позволяющих перебазировать правки на новое состояние. Тогда историю можно не стирать.
📜 При откате — запоминаем правки
⏩ При накате — повторяем правки
📜 При вводе — не стираем будущее
⏩ Накат после ввода — перебазируем правки
При этом важно понимать, что хранить в истории нужно только локальные правки и позицию выделения текущего пользователя, чтобы случайно не стереть чужие правки при совместном редактировании или ещё чего лишнего.
👀 Следим за локальными правками
👀 Следим за выделением
😎 Не следим за чужими правками
😎 Не следим за иными состояниями
Кто-нибудь что-нибудь понял?
Я рассказал, конечно, далеко не всё, что касается редактирования текста, а только лишь то, что на мой взгляд современные редакторы делают совершенно не правильно. Как видите, даже в такой, казалось бы, простой и устоявшейся области, ещё очень далеко до совершенства.
Если вы, как и я, не боитесь бегать против шторма, то присоединяйтесь к техно-гильдии Giper.Dev, чтобы вместе привносить инновации для решения проблем в корне, а не как обычно.
nin_jin — личка
giper-dev — видосы
\https://giper.dev/ — о гильдии
Giper_Dev — про дизайн
ссылка на оригинал статьи https://habr.com/ru/articles/1095498/