Использование API EditContext
API EditContext API можно использовать для создания веб-редакторов форматированного текста, поддерживающих расширенные возможности ввода текста, такие как Input Method Editor (IME) составление, выбор эмодзи или другие платформенные элементы пользовательского интерфейса, связанные с редактированием.
В этой статье описаны необходимые шаги для создания редактора текста с использованием API EditContext. В этом руководстве вы ознакомитесь с основными этапами создания простого редактора HTML-кода, который выделяет синтаксис кода по мере набора текста и поддерживает составление IME.
Конечный код и демонстрация
Чтобы увидеть конечный код, ознакомьтесь с исходным кодом на GitHub. Рекомендуется держать исходный код открытым во время чтения, так как в этом руководстве показаны только самые важные части кода.
Исходный код организован в следующие файлы:
- index.html содержит элемент пользовательского интерфейса редактора и загружает необходимый CSS и JavaScript-код для демонстрации.
- styles.css содержит стили для пользовательского интерфейса редактора.
- editor.js содержит JavaScript-код, который настраивает пользовательский интерфейс редактора, отображает HTML-код и обрабатывает пользовательский ввод.
- tokenizer.js содержит JavaScript-код, который разделяет HTML-код на отдельные токены, такие как открывающие теги, закрывающие теги и текстовые узлы.
- converter.js содержит JavaScript-код, преобразующий смещения символов, используемые API EditContext, в узлы DOM, используемые браузером для выбора текста.
Чтобы использовать демонстрацию, откройте Демонстрацию API Edit Context: редактор HTML в браузере, поддерживающем API EditContext.
Создание пользовательского интерфейса редактора
Первый шаг — создание пользовательского интерфейса редактора. Редактор представляет собой элемент <div> с атрибутом spellcheck, установленным на false для отключения проверки орфографии:
<div id="html-editor" spellcheck="false"></div>
Для стилизации элемента редактора используется следующий CSS-код. Код делает редактор занимающим весь viewport и прокручиваемым, когда содержимого слишком много, чтобы оно уместилось. Также используется свойство white-space для сохранения пробельных символов, найденных в тексте HTML-ввода, и свойство tab-size для отображения символов табуляции как двух пробелов. Наконец, заданы некоторые значения по умолчанию для фона, текста и цвета курсора:
#html-editor {
box-sizing: border-box;
width: 100%;
height: 100%;
border-radius: 0.5rem;
padding: 1rem;
overflow: auto;
white-space: pre;
tab-size: 2;
caret-color: red;
background: #000;
line-height: 1.6;
color: red;
}
Делаем редактор редактируемым
Чтобы сделать элемент редактируемым в веб-приложении, чаще всего используется элемент <input>, элемент <textarea> или атрибут contenteditable.
Однако с помощью API EditContext вы можете сделать редактируемыми другие типы элементов без использования атрибутов. Чтобы увидеть список элементов, которые можно использовать с API EditContext, см. Возможные элементы на странице свойства HTMLElement editContext.
Чтобы сделать редактор редактируемым, приложение-демонстрация создаёт экземпляр EditContext, передавая некоторый начальный текст HTML в конструктор, а затем устанавливает свойство editContext элемента редактора на экземпляр EditContext.
// Retrieve the editor element from the DOM.
const editorEl = document.getElementById("html-editor");
// Create the EditContext instance.
const editContext = new EditContext({
text: "<html>\n <body id=foo>\n <h1 id='header'>Cool Title</h1>\n <p class=\"wow\">hello<br/>How are you? test</p>\n </body>\n</html>",
});
// Set the editor's editContext property value.
editorEl.editContext = editContext;
Эти строки кода делают элемент редактора фокусируемым. Ввод текста в элемент вызывает событие textupdate в экземпляре EditContext.
Отображение текста и выделения пользователя
Для отображения HTML-кода с выделением синтаксиса в редакторе при вводе текста приложение-демонстрация использует функцию с именем render(), которая вызывается при вводе нового текста, при удалении символов или при изменении выделения.
Токенизация HTML-кода
Одним из первых действий функции render() является токенизация содержимого HTML-текста. Токенизация HTML-текста необходима для выделения синтаксиса HTML и включает в себя чтение строки HTML-кода и определение того, где начинаются и заканчиваются каждый открывающий тег, закрывающий тег, атрибут, узел комментария и текстовый узел.
Для этого приложение-демонстрация использует функцию tokenizeHTML(), которая итерирует строку символ за символом, поддерживая состояние автомата. Исходный код функции tokenizeHTML() вы можете посмотреть в файле tokenizer.js на GitHub.
Функция импортируется в файл HTML приложения-демонстрации следующим образом:
import { tokenizeHTML } from "./tokenizer.js";
Отображение текста
Всякий раз, когда вызывается функция render(), что происходит при вводе текста пользователем или изменении выделения, функция удаляет содержимое в элементе редактора и затем отображает каждый токен как отдельный HTML-элемент:
// Stores the list of HTML tokens.
let currentTokens = [];
function render(text, selectionStart, selectionEnd) {
// Empty the editor. We're re-rendering everything.
editorEl.textContent = "";
// Tokenize the text.
currentTokens = tokenizeHTML(text);
for (const token of currentTokens) {
// Render each token as a span element.
const span = document.createElement("span");
span.classList.add(`token-${token.type}`);
span.textContent = token.value;
// Attach the span to the editor element.
editorEl.appendChild(span);
// Store the new DOM node as a property of the token
// in the currentTokens array. We will need it again
// later in fromOffsetsToRenderedTokenNodes.
token.node = span;
}
// Code to render the text selection is omitted for brevity.
// See "Rendering the selection", below.
// ...
}
API EditContext предоставляет возможность управлять способом отображения отредактированного текста. В приведенной выше функции отображение осуществляется с помощью HTML-элементов, но оно может быть реализовано любым другим способом, включая отображение в элементе <canvas>.
Приложение-демонстрация выполняет функцию render() при необходимости. Это включает в себя один раз при запуске приложения, а затем снова при вводе текста пользователем, прослушивая событие textupdate:
// Listen to the EditContext's textupdate event.
// This tells us when text input happens. We use it to re-render the view.
editContext.addEventListener("textupdate", (e) => {
render(editContext.text, e.selectionStart, e.selectionEnd);
});
// Do the initial render.
render(editContext.text, editContext.selectionStart, editContext.selectionEnd);
Стиль токенов
Как показано в предыдущем примере кода функции render(), каждому токену присваивается имя класса, соответствующее типу токена. Приложение-демонстрация использует это имя класса для стилизации токенов с помощью CSS, как показано ниже:
.token-openTagStart,
.token-openTagEnd,
.token-closeTagStart,
.token-closeTagEnd,
.token-selfClose {
background: rgb(7 53 92);
margin: 0 2px;
color: white;
border-radius: 0.25rem;
}
.token-equal {
color: white;
}
.token-tagName {
font-weight: bold;
color: rgb(117 186 242);
}
.token-attributeName {
color: rgb(207 81 198);
}
.token-attributeValue {
font-style: italic;
color: rgb(127 230 127);
border: 1px dashed #8c8c8c;
border-width: 1px 0 1px 0;
}
.token-quoteStart,
.token-quoteEnd {
font-weight: bold;
color: rgb(127 230 127);
border: 1px solid #8c8c8c;
border-width: 1px 0 1px 1px;
border-radius: 0.25rem 0 0 0.25rem;
}
.token-quoteEnd {
border-width: 1px 1px 1px 0;
border-radius: 0 0.25rem 0.25rem 0;
}
.token-text {
color: #6a6a6a;
padding: 0 0.25rem;
}
Отображение выделения
Несмотря на то, что приложение-демонстрация использует элемент <div> для редактора, который уже поддерживает отображение мигающего курсора текста и выделения пользовательского выбора, API EditContext все равно требует отображения выделения. Это связано с тем, что API EditContext можно использовать с другими типами элементов, которые не поддерживают эти возможности. Само отображение выделения также предоставляет нам больший контроль над отображением выделения. Наконец, так как функция render() очищает содержимое HTML элемента редактора каждый раз при выполнении, любое выделение, которое мог сделать пользователь, теряется в следующий раз при выполнении функции render().
Для отображения выделения приложение-демонстрация использует метод Selection.setBaseAndExtent() в конце функции render(). Для использования метода setBaseAndExtent(), нам нужен набор узлов DOM и смещений символов, представляющих начало и конец выделения. Однако API EditContext сохраняет состояние текущего выделения только как пару начального и конечного смещений символов в весь буфер редактирования. В приложении-демонстрации используется другая функция с именем fromOffsetsToSelection(), которая используется для преобразования этих смещений символов в четыре значения:
- Узел DOM, содержащий начало выделения.
- Число, представляющее позицию выделения начала внутри начального узла.
- Узел DOM, содержащий конец выделения.
- Число, представляющее позицию выделения конца внутри конечного узла.
function render(text, selectionStart, selectionEnd) {
// ...
// The beginning of the render function is omitted for brevity.
// Convert the start/end offsets to a DOM selection.
const { anchorNode, anchorOffset, extentNode, extentOffset } =
fromOffsetsToSelection(selectionStart, selectionEnd);
// Render the selection in the editor element.
document
.getSelection()
.setBaseAndExtent(anchorNode, anchorOffset, extentNode, extentOffset);
}
Код функции fromOffsetsToSelection() можно найти в файле converter.js.
Обновление границ управления
API EditContext предоставляет нам большую гибкость для определения собственного пользовательского интерфейса редактора текста. Однако это также означает, что нам необходимо обрабатывать некоторые вещи, которые обычно обрабатываются браузером или операционной системой (ОС).
Например, мы должны указать ОС, где находится область редактирования текста на странице. Таким образом, ОС может правильно расположить любой пользовательский интерфейс редактирования текста, с которым пользователь может вводить текст, например, окно составления IME.
Приложение-демонстрация использует метод EditContext.updateControlBounds(), передавая ему объект DOMRect, представляющий границы области редактирования текста. Приложение-демонстрация вызывает этот метод при инициализации редактора и при изменении размера окна:
function updateControlBounds() {
// Get the DOMRect object for the editor element.
const editorBounds = editorEl.getBoundingClientRect();
// Update the control bounds of the EditContext instance.
editContext.updateControlBounds(editorBounds);
}
// Call the updateControlBounds function when the editor is initialized,
updateControlBounds();
// And call it again when the window is resized.
window.addEventListener("resize", updateControlBounds);
Обработка клавиш Tab, Enter и других клавиш редактирования текста
Событие textupdate , использованное в предыдущем разделе, не срабатывает при нажатии пользователем клавиш Tab или Enter, поэтому нам нужно обработать эти клавиши отдельно.
Для их обработки демонстрационное приложение использует обработчик событий для события keydown на элементе редактора и использует этот обработчик для обновления содержимого текста и выделения экземпляра EditContext, как показано ниже:
// Handle key presses that are not already handled by the EditContext.
editorEl.addEventListener("keydown", (e) => {
// EditContext.updateText() expects the start and end offsets
// to be in the correct order, but the current selection state
// might be backwards.
const start = Math.min(editContext.selectionStart, editContext.selectionEnd);
const end = Math.max(editContext.selectionStart, editContext.selectionEnd);
// Handling the Tab key.
if (e.key === "Tab") {
// Prevent the default behavior of the Tab key.
e.preventDefault();
// Use the EditContext.updateText method to insert a tab character
// at the current selection position.
editContext.updateText(start, end, "\t");
// Update the selection to be after the inserted tab character.
updateSelection(start + 1, start + 1);
// Re-render the editor.
render(
editContext.text,
editContext.selectionStart,
editContext.selectionEnd,
);
}
// Handling the Enter key.
if (e.key === "Enter") {
// Use the EditContext.updateText method to insert a newline character
// at the current selection position.
editContext.updateText(start, end, "\n");
// Update the selection to be after the inserted newline character.
updateSelection(start + 1, start + 1);
// Re-render the editor.
render(
editContext.text,
editContext.selectionStart,
editContext.selectionEnd,
);
}
});
В приведенном выше коде также вызывается функция updateSelection() для обновления выделения после обновления содержимого текста. Дополнительную информацию см. ниже в разделе Обновление состояния выделения и границ выделения.
Мы могли бы улучшить код, обработав другие сочетания клавиш, такие как Ctrl+C и Ctrl+V для копирования и вставки текста или Ctrl+Z и Ctrl+Y для отмены и повтора изменений текста.
Обновление состояния выделения и границ выделения
Как мы видели ранее, функция render() обрабатывает отображение текущего выделения пользователя в элементе редактора. Но демонстрационное приложение также должно обновлять состояние выделения и границы, когда пользователь меняет выделение. API EditContext не делает этого автоматически, опять же, потому что пользовательский интерфейс редактора может быть реализован по-другому, например, с помощью элемента <canvas>.
Чтобы узнать, когда пользователь меняет выделение, демонстрационное приложение использует событие selectionchange и метод Document.getSelection(), которые предоставляют объект Selection, показывая, где находится выделение пользователя. Используя эту информацию, демонстрационное приложение обновляет состояние выделения и границы выделения EditContext, используя методы EditContext.updateSelection() и EditContext.updateSelectionBounds(). Это используется ОС для правильного позиционирования окна составления IME.
Однако, поскольку API EditContext использует смещения символов для представления выделения, демонстрационное приложение также использует функцию fromSelectionToOffsets(), которая преобразует объекты выделения DOM в смещения символов.
// Listen to selectionchange events to let the
// EditContext know where it is.
document.addEventListener("selectionchange", () => {
const selection = document.getSelection();
// Convert the DOM selection into character offsets.
const offsets = fromSelectionToOffsets(selection, editorEl);
if (offsets) {
updateSelection(offsets.start, offsets.end);
}
});
// Update the selection and selection bounds in the EditContext object.
// This helps the OS position the IME composition window correctly.
function updateSelection(start, end) {
editContext.updateSelection(start, end);
// Get the bounds of the selection.
editContext.updateSelectionBounds(
document.getSelection().getRangeAt(0).getBoundingClientRect(),
);
}
Код функции fromSelectionToOffsets() вы можете посмотреть в файле converter.js.
Вычисление границ символов
Помимо использования методов EditContext.updateControlBounds() и EditContext.updateSelectionBounds() для помощи ОС в позиционировании пользовательского интерфейса редактирования текста, ОС требуется еще одна информация: положение и размер определенных символов в элементе редактора.
Для этого демонстрационное приложение прослушивает событие characterboundsupdate, использует его для вычисления границ некоторых символов в элементе редактора и затем использует метод EditContext.updateCharacterBounds() для обновления границ символов.
Как и прежде, API EditContext знает только смещения символов, что означает, что событие characterboundsupdate предоставляет начальное и конечное смещения для символов, для которых требуются границы. Демонстрационное приложение использует другую функцию fromOffsetsToRenderedTokenNodes(), чтобы найти элементы DOM, в которых эти символы были отображены, и использует эту информацию для вычисления необходимых границ.
// Listen to the characterboundsupdate event to know when character bounds
// information is needed, and which characters need bounds.
editContext.addEventListener("characterboundsupdate", (e) => {
// Retrieve information about the token nodes in the range.
const tokenNodes = fromOffsetsToRenderedTokenNodes(
currentTokens,
e.rangeStart,
e.rangeEnd,
);
// Convert this information into a list of DOMRect objects.
const charBounds = tokenNodes.map(({ node, nodeOffset, charOffset }) => {
const range = document.createRange();
range.setStart(node.firstChild, charOffset - nodeOffset);
range.setEnd(node.firstChild, charOffset - nodeOffset + 1);
return range.getBoundingClientRect();
});
// Let the EditContext instance know about the character bounds.
editContext.updateCharacterBounds(e.rangeStart, charBounds);
});
Код функции fromOffsetsToRenderedTokenNodes() вы можете посмотреть в файле converter.js.
Применение форматирования текста составления IME
Демонстрационное приложение выполняет последний шаг для полной поддержки составления IME. Когда пользователь создает текст с помощью IME, IME может решить, что определенные части создаваемого текста должны быть отформатированы по-разному для обозначения состояния составления. Например, IME может решить подчеркнуть текст.
Поскольку демонстрационное приложение отвечает за отображение содержимого в области редактирования текста, оно также отвечает за применение необходимого форматирования IME. Демонстрационное приложение достигает этого, прослушивая событие textformatupdate для того, чтобы узнать, когда IME хочет применить форматирование текста, где и какое форматирование применить.
Как показано в следующем фрагменте кода, демонстрационное приложение использует событие textformatupdate и функцию fromOffsetsToSelection() снова, чтобы найти диапазон текста, который IME составления хочет отформатировать:
editContext.addEventListener("textformatupdate", (e) => {
// Get the list of formats that the IME wants to apply.
const formats = e.getTextFormats();
for (const format of formats) {
// Find the DOM selection that corresponds to the format's range.
const selection = fromOffsetsToSelection(
format.rangeStart,
format.rangeEnd,
editorEl,
);
// Highlight the selection with the right style and thickness.
addHighlight(selection, format.underlineStyle, format.underlineThickness);
}
});
Обработчик события выше вызывает функцию с именем addHighlight() для форматирования текста. Эта функция использует API пользовательского выделения CSS для отображения форматирования текста. API пользовательского выделения CSS предоставляет механизм стилизации произвольных диапазонов текста с помощью JavaScript для создания диапазонов и CSS для их стилизации. Для использования этого API используется псевдоэлемент ::highlight() для определения стилей выделения:
::highlight(ime-solid-thin) {
text-decoration: underline 1px;
}
::highlight(ime-solid-thick) {
text-decoration: underline 2px;
}
::highlight(ime-dotted-thin) {
text-decoration: underline dotted 1px;
}
::highlight(ime-dotted-thick) {
text-decoration: underline dotted 2px;
}
/* Other highlights are omitted for brevity. */
Highlight экземпляры также создаются, хранятся в объекте и регистрируются в HighlightRegistry с использованием свойства CSS.highlights:
// Instances of CSS custom Highlight objects, used to render
// the IME composition text formats.
const imeHighlights = {
"solid-thin": null,
"solid-thick": null,
"dotted-thin": null,
"dotted-thick": null,
"dashed-thin": null,
"dashed-thick": null,
"wavy-thin": null,
"wavy-thick": null,
"squiggle-thin": null,
"squiggle-thick": null,
};
for (const [key, value] of Object.entries(imeHighlights)) {
imeHighlights[key] = new Highlight();
CSS.highlights.set(`ime-${key}`, imeHighlights[key]);
}
С этим демонстрационное приложение использует функцию addHighlight() для объектов диапазонов Range, которые необходимо стилизовать, и добавляет их в объект Highlight:
function addHighlight(selection, underlineStyle, underlineThickness) {
// Get the right CSS custom Highlight object depending on the
// underline style and thickness.
const highlight =
imeHighlights[
`${underlineStyle.toLowerCase()}-${underlineThickness.toLowerCase()}`
];
if (highlight) {
// Add a range to the Highlight object.
const range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.extentNode, selection.extentOffset);
highlight.add(range);
}
}
Резюме
В этой статье вы узнали, как использовать API EditContext для создания простого HTML-редактора кода, поддерживающего составление IME и подсветку синтаксиса.
Конечный код и демонстрационное приложение можно найти на GitHub: демонстрация и исходный код.
Более важно, что эта статья показала вам, что API EditContext предоставляет большую гибкость, когда дело доходит до пользовательского интерфейса вашего редактора. Основываясь на этой демонстрации, вы могли бы создать похожий текстовый редактор, использующий элемент <canvas> для отображения HTML-кода с подсветкой синтаксиса вместо элемента <div>, который используется в демонстрации. Вы также могли бы изменить способ отображения каждого маркера или отображения выделения.
См. также
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/EditContext_API/Guide