Привет, Хабр... то есть, привет, Дзен! Сегодня мы займёмся удивительно интересной задачей — созданием собственного текстового редактора. Но не простого блокнота с парой кнопок, а полноценного инструмента, в котором можно печатать, форматировать текст и даже реализовать автсохранение.
Многие думают, что написать редактор — это сложно. Нужны фреймворки, тысячи строк кода и команда тестировщиков. На самом деле, базовый функционал можно реализовать на ванильном JavaScript буквально за 15 минут.
В этой статье мы пройдем путь от простейшего <textarea> до редактора с панелью инструментов и современными наворотами. Поехали!
Этап 1: Классика — textarea и localStorage
Самый простой способ создать редактор — использовать обычное текстовое поле <textarea> . Это нативный HTML-элемент, который умеет всё, что нужно для ввода текста.
Давайте добавим в него немного магии — сохранение черновика прямо в браузере пользователя с помощью localStorage .
html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Мой первый блокнот</title>
</head>
<body>
<h1>Мой супер-блокнот</h1>
<textarea id="editor" rows="20" cols="80" placeholder="Пишите здесь..."></textarea>
<br>
<button onclick="saveText()">Сохранить черновик</button>
<button onclick="loadText()">Загрузить черновик</button>
<script>
const editor = document.getElementById('editor');
function saveText() {
const text = editor.value;
localStorage.setItem('myDraft', text);
alert('Черновик сохранен!');
}
function loadText() {
const savedText = localStorage.getItem('myDraft');
if (savedText) {
editor.value = savedText;
} else {
alert('Нет сохраненного черновика.');
}
}
// Автозагрузка при открытии страницы
window.onload = function() {
const savedText = localStorage.getItem('myDraft');
if (savedText) {
editor.value = savedText;
}
}
</script>
</body>
</html>
Что мы здесь сделали?
- Создали поле textarea для ввода .
- Написали функцию saveText, которая берет текст из поля и сохраняет его в localStorage под ключом myDraft.
- Написали функцию loadText, которая достает текст из хранилища и вставляет обратно в поле.
Это полноценное простое приложение, которое можно использовать как ежедневник или блокнот для заметок. Минус только один — текст не форматировать: ни жирным не выделить, ни заголовком не сделать.
Этап 2: Вжух — и у нас WYSIWYG!
Хочется красоты? Тогда нам нужен WYSIWYG-редактор (What You See Is What You Get). В таких редакторах вы сразу видите, как будет выглядеть документ.
Секрет здесь кроется в магическом атрибуте contenteditable="true" и устаревшем, но всё ещё рабочем API document.execCommand() .
Добавим на страницу панель инструментов и сделаем область редактирования настоящей «верстальней».
html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Визуальный редактор</title>
<style>
#editor {
border: 1px solid #ccc;
min-height: 300px;
padding: 15px;
font-family: Arial, sans-serif;
margin-top: 10px;
overflow-y: auto;
}
.toolbar {
background: #f5f5f5;
padding: 10px;
border: 1px solid #ddd;
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.toolbar button {
padding: 5px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<h2>Визуальный редактор</h2>
<div class="toolbar">
<button data-command="bold"><b>B</b></button>
<button data-command="italic"><i>I</i></button>
<button data-command="underline"><u>U</u></button>
<button data-command="insertOrderedList">1. Список</button>
<button data-command="insertUnorderedList">• Список</button>
<button data-command="justifyLeft">⬅</button>
<button data-command="justifyCenter">⏺</button>
<button data-command="justifyRight">➡</button>
</div>
<!-- Вот он, секретный ингредиент! -->
<div id="editor" contenteditable="true">
<p>Начните печатать здесь...</p>
</div>
<script>
const editor = document.getElementById('editor');
// Навешиваем обработчики на кнопки
document.querySelectorAll('.toolbar button').forEach(button => {
button.addEventListener('click', function() {
const command = this.dataset.command;
// Волшебная функция, которая форматирует текст
document.execCommand(command, false, null);
// Возвращаем фокус в редактор, чтобы продолжить печатать
editor.focus();
});
});
</script>
</body>
</html>
Как это работает :
- Атрибут contenteditable="true" превращает обычный <div> в редактируемую область. Вы можете печатать, удалять, вставлять картинки.
- Функция document.execCommand() говорит браузеру: «Сделай выделенный текст жирным» или «Преврати это в список».
- Браузер сам генерирует HTML-код внутри редактора.
Это уже настоящий rich-text редактор. Можно экспериментировать и добавлять другие команды: createLink (создать ссылку), insertImage (вставить картинку) и так далее .
Этап 3: Только для хардкорщиков — режим plaintext-only
В 2025 году появился один очень элегантный трюк. Оказывается, атрибуту contenteditable можно дать значение plaintext-only .
Зачем это нужно? Представьте, что вы хотите, чтобы пользователь мог вводить и редактировать текст, но не мог случайно (или специально) вставить жирный кусок HTML-кода или сломать верстку. Режим plaintext-only включает редактирование, но запрещает форматирование. Пользователь может печатать, стирать, вставлять текст, но кнопки форматирования из прошлого примера работать не будут.
html
<div contenteditable="plaintext-only" style="border:1px solid #ccc; padding:10px;">
Здесь можно писать, но нельзя сделать жирным.
</div>
Этап 4: Профессиональный подход — автодополнение и debounce
Когда мы пишем большой текст, очень важно, чтобы он сохранялся автоматически, пока мы печатаем. Но представьте, что на каждое нажатие клавиши мы будем сохранять текст в localStorage или отправлять на сервер. Это слишком частая операция.
Для этого существует техника debounce (подавление). Мы будем сохранять текст только тогда, когда пользователь перестал печатать, например, на 1 секунду .
Посмотрите на элегантную реализацию:
javascript
function debounce(ms, fn) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
// Функция, которая будет сохранять текст
function saveToLocalStorage(text) {
localStorage.setItem('autoDraft', text);
console.log('Сохранено!');
}
// Оборачиваем нашу функцию сохранения в debounce
// Теперь она сработает только через 1 секунду после последнего ввода
const debouncedSave = debounce(1000, saveToLocalStorage);
// Следим за вводом в редакторе
editor.addEventListener('input', function(e) {
debouncedSave(editor.innerHTML); // или editor.innerText, или editor.textContent
});
Теперь редактор не будет дергать систему на каждую букву, а подождет, пока вы закончите печатать предложение, и только потом сохранит .
Стоит ли писать свой редактор с нуля?
Мы разобрали два пути: простой (contenteditable) и сложный (самописный парсинг токенов вроде редактора из MDN ). У каждого есть подводные камни.
Если вы пишете пет-проект или редактор для внутреннего использования — смело берите contenteditable. Это быстро и просто.
Но если вы создаете продукт для тысяч пользователей, как Google Docs или Notion, будьте готовы к тому, что contenteditable ведёт себя по-разному в разных браузерах . В этом случае разумнее взять готовую библиотеку.
Например, Tiptap. Это современный, красивый и удобный редактор, построенный на мощном фундаменте ProseMirror . Он сам управляет структурой документа, и вы точно не получите «мусорный» HTML на выходе.
Подключить его можно буквально в несколько строк :
bash
npm install @tiptap/core @tiptap/pm @tiptap/starter-kit
javascript
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
new Editor({
element: document.querySelector('.element'),
extensions: [StarterKit],
content: '<p>Hello World!</p>',
});
Итоги
Мы проделали большой путь:
- Научились хранить текст в браузере с помощью localStorage .
- Создали визуальный редактор через contenteditable и execCommand .
- Узнали про секретный режим plaintext-only .
- Оптимизировали сохранение с помощью debounce .
- Поняли, когда стоит писать самому, а когда брать готовую библиотеку .
Напишите в комментариях, какой редактор вы хотели бы собрать? Может быть, свой мини-Notion или просто красивый блокнот для заметок? Буду рад почитать и ответить на вопросы!
Подписывайтесь на канал, чтобы не пропустить новые выпуски о веб-разработке и неочевидных фишках HTML, CSS и JavaScript