Атрибут contenteditable превращает любой элемент в поле для редактирования прямо в браузере: текст внутри него можно кликнуть и печатать, как в текстовом редакторе, без формы и без поля ввода. Это глобальный атрибут - его можно прописать ссылке, параграфу, ячейке таблицы или любому другому тегу.
<div contenteditable="true">Этот текст можно редактировать</div>
В этой статье
Атрибут contenteditable: что делает и какие у него значения
contenteditable - не булевый, а перечислимый атрибут: у него несколько допустимых значений, а не просто «есть» или «нет».
| Значение | Что происходит |
|---|---|
| true | Элемент редактируется, форматирование при вставке сохраняется |
| "" (пустое значение) | То же самое, что true - атрибут без значения и contenteditable="" равнозначны |
| false | Элемент нередактируемый - значение по умолчанию для большинства тегов |
| plaintext-only | Текст редактируется, но форматирование отключено: команды вроде «жирный» не действуют, а при вставке стили обрезаются |
Короткую запись contenteditable без значения браузеры понимают как contenteditable="true" - писать значение явно не обязательно.
Пример: true и plaintext-only рядом
В режиме true форматирование применяется, в plaintext-only кнопка «жирный» не действует вовсе.
Наследование: вложенный элемент берёт поведение родителя
Если у элемента нет своего contenteditable или в нём записано что-то некорректное, он берёт поведение ближайшего предка: редактируемый родитель делает редактируемым и всё содержимое внутри. Но это можно переопределить в обратную сторону - вложенный элемент с contenteditable="false" остаётся нередактируемым, даже если находится внутри редактируемого родителя. Этим пользуются, когда часть блока должна остаться неприкосновенной - например, бейдж статуса или кнопка внутри редактируемой карточки.
Пример: защищённый фрагмент внутри редактируемого блока
<div contenteditable="true">
Этот текст можно редактировать
<span contenteditable="false">статус: черновик</span>
</div>
Текст редактируется, а бейдж с contenteditable="false" печать игнорирует - он не наследует true от родителя.
Как узнать, что текст изменился - событие input
У редактируемых элементов нет своего значения и события change, как у поля формы - вместо этого на любое изменение текста (набор, вставку, удаление) срабатывает обычное событие input. На нём удобно вешать счётчик символов, индикатор несохранённых правок или автосохранение черновика.
Пример: счётчик символов на событии input
<div id="box" contenteditable="true">Правьте меня</div>
box.addEventListener("input", function () {
счётчик.textContent = box.textContent.length;
});
Каждое изменение текста - новое срабатывание input, счётчик обновляется сразу, без отдельной кнопки «Сохранить».
contenteditable и переход по Tab
Редактируемый элемент верхнего уровня сам попадает в очередь Tab без всякого tabindex - у него есть собственный фокус по умолчанию. А вот вложенный редактируемый элемент внутри другого редактируемого элемента в эту очередь не попадает: Tab останавливается только на внешнем блоке. Чтобы добраться клавиатурой и до вложенного, ему нужно явно прописать tabindex="0" - подробнее о том, как устроен порядок Tab, в статье про атрибут tabindex.
<div contenteditable="true">
Внешний блок - есть фокус по Tab
<div contenteditable="true">Вложенный блок - без tabindex="0" Tab его пропустит</div>
</div>
Частые ошибки с атрибутом contenteditable
С contenteditable чаще путают не сам атрибут, а то, что вокруг него приходится достраивать вручную.
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Считают contenteditable булевым атрибутом | У него четыре разных значения (true, "", false, plaintext-only), а не просто присутствие или отсутствие | Указывать нужное значение явно, когда это не очевидный true по умолчанию |
| Ждут событие change, как у поля формы | У contenteditable его нет - изменения текста ловит событие input, оно и срабатывает на каждый набор символа | Подписываться на input, а не на change |
| Думают, что текст из contenteditable сам отправится с формой | Текст внутри contenteditable не становится значением поля - форма его не увидит без отдельного кода | Копировать содержимое в скрытое поле формы перед отправкой через JavaScript |
| Прописывают contenteditable без plaintext-only там, где нужен только обычный текст | Пользователь может вставить форматированный текст из буфера, и в разметке появятся чужие теги | Использовать contenteditable="plaintext-only", если форматирование не нужно |
| Ожидают, что вложенный редактируемый блок попадёт в Tab-очередь сам | Вложенные contenteditable-элементы Tab пропускает по умолчанию | Добавлять им tabindex="0" вручную |
Шпаргалка по атрибуту contenteditable
| Правило | Что запомнить |
|---|---|
| Где работает | На любом теге (глобальный атрибут) |
| true / "" (без значения) | Элемент редактируется, форматирование сохраняется |
| false | Элемент нередактируемый (значение по умолчанию) |
| plaintext-only | Редактируется только обычный текст, форматирование отключено |
| Наследование | Элемент без своего значения берёт поведение ближайшего предка |
| Защита фрагмента | Вложенный contenteditable="false" не наследует true от родителя |
| Событие при изменении | input - срабатывает на каждое изменение текста |
| Данные формы | В форму не попадают сами по себе, нужен отдельный код |
| Tab-очередь | Верхний редактируемый элемент - в очереди по умолчанию, вложенный - только с tabindex="0" |