Эта статья является китайским переводом, английская оригинальная ссылкаmedium.com/Бизнес-пилот/Re imp...
илиDev.to/David сыграл довольно много…(к этому можно получить доступ)
переводчик сказал
яwangEditorАвтор , в настоящее время занимается рефакторингом версии 4.0 с командой разработчиков. Код перед выпуском V4.0 находится вwe-next, доступно после публикацииОфициальный сайт.
Студенты, знакомые с редакторами форматированного текста, знают, чтоdocument.execCommandЭто основной API для реализации форматированного текста на веб-страницах. но, к сожалениюdocument.execCommandОн устарел MDN. Более того, уже долгое время различные браузеры не полностью унифицируют детали его реализации, а совместимость браузеров всегда была головной болью.
Чтобы решить вышеуказанные проблемы, различные текстовые редакторы уже давно проводят собственные исследования. Сильный Google Doc был первым, кто реализовал собственный API для редактирования форматированного текста. Позже, с продвижением и использованием онлайн-офисов, редакторы с форматированным текстом в стране и за рубежом также быстро развивались, а некоторые были разработаны сами по себе.document.execCommand.wangEditorВ будущем (после выпуска версии 4.0 и стабильной работы) такие самостоятельные исследования и обновления неизбежны.
Итак, как вы реализуете это самостоятельноdocument.execCommandШерстяная ткань? Китайских материалов пока мало, поэтому нашел английскую статью для перевода. Если перевод неверен, вы можете ответить на комментарии, чтобы исправить его.
(Исходный адресНаггетс.Талант/пост/686491…, воспроизведено с согласия автора! )
Теперь официально начинаем перевод! ! !
Теперь официально начинаем перевод! ! !
Теперь официально начинаем перевод! ! !
Эта функция (т.
document.execCommand) устарела и может быть удалена в любое время, хотя она по-прежнему доступна во многих браузерах. Итак, старайтесь избегать этого - веб-документы MDN
Не знаю с тех пор, когда по какой-то причине,document.execCommand()был отмечен в MDN какзаброшенный. Интересно, что он помечен как устаревший не на всех языках (например, на китайском, английском, но не на языках программирования, примечание переводчика), таких как французский и испанский, об этом нет упоминания 😜.
дляDeckDeckGo, редактор слайд-шоу с открытым исходным кодом, мы разработали и выпустили пользовательскийWYSIWYG-редактор, оно используетdocument.execCommand()эта функция.
Думаю, через кастомную реализациюdocument.execCommand()Возможно, это способ развития в будущем, поэтому мне потребовалось довольно много времени, чтобы переделать его 😄.
Несмотря на то, что моя реализация выглядит не так уж плохо (надеюсь), я все еще чувствую, что мне придется заново изобретать велосипед. Это также причина, по которой я написал эту статью, я надеюсь, что вы можете дать несколько предложений по улучшению, чтобы сделать ее более зрелой и совершенной🙏.
представлять
Редактор WYSIWYG, его совместимость на разных устройствах лучше. Он работает как в браузерах ПК, так и на мобильных устройствах. Его можно прикрепить к верхней (iOS) или нижней (Android) части попытки, в зависимости от поведения клавиатуры.
Он может изменять стиль текста (жирный, курсив, подчеркивание и зачеркивание), цвет шрифта и цвет фона, выравнивание, список. Он даже поддерживает некоторые пользовательские операцииslot.
ограниченное
я сам это сделалdocument.execCommand()Работает нормально, но на данный момент не поддерживает отмену, а жаль 😕.
Каждый может внести свои хорошие предложения по этому поводу.
Цель
Следующее, что будет реализовано в этой статье, — это следующий API, на который вы можете ссылаться.Документация из MDN.
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
Функции, которые должны быть реализованы,
-
boldЖирный/Нежирный -
italicКурсив/не курсив -
underlineустановить/снять подчеркивание -
strikeThroughУстановить/снять удар -
foreColorИзменить цвет шрифта -
backColorизменить цвет фона
выполнить
Мне нравится разрабатывать с помощью ts, поэтому следующий код будет строго типизированным и также будет использоватьinterface.
export interface ExecCommandStyle {
style: 'color' |
'background-color' |
'font-size' |
'font-weight' |
'font-style' |
'text-decoration';
value: string;
initial: (element: HTMLElement | null) => Promise<boolean>;
}
По умолчанию выполнитьdocument.execCommandсоздаст новый элемент, когда<b>element, Примечание переводчика), и я решил изменить CSS для достижения эффекта. Напримерvalue: 'bold'это может быть выраженоstyle: 'font-weight',еслиstyle: "color"Времяvalueвозможно#ccc,так. Этот интерфейс также содержитinitialфункцию, которую я использую для подтверждения: установить стиль или удалить стиль.
Интерфейс определен, а затем запустите конкретную реализацию, чтобы увидеть, как применить стиль. сначала черезselectionПолучить текст, выбранный пользователем, затем распознать егоcontainer.注意,这个containerЭто может быть текстовый узел или родительский элемент выделения.
Кроме того, второй параметр в функцииcontainers, который определяет список элементов, к которым может быть применена функция, по умолчаниюh1,h2,h3,h4,h5,h6,div. Это ограничение введено для того, чтобы при поиске не обходилось все дерево DOM.
export async function execCommandStyle(action: ExecCommandStyle, containers: string) {
const selection: Selection | null = await getSelection();
if (!selection) {
return;
}
const anchorNode: Node = selection.anchorNode;
if (!anchorNode) {
return;
}
const container: HTMLElement =
anchorNode.nodeType !== Node.TEXT_NODE
&& anchorNode.nodeType !== Node.COMMENT_NODE ?
(anchorNode as HTMLElement) : anchorNode.parentElement;
// TODO: next chapter
}
async function getSelection(): Promise<Selection | null> {
if (window && window.getSelection) {
return window.getSelection();
} else if (document && document.getSelection) {
return document.getSelection();
} else if (document && (document as any).selection) {
return (document as any).selection.createRange().text;
}
return null;
}
Чтобы установить стили CSS, преобразуйте текст, выбранный пользователем, вspanЭтикетка.
Кроме того, я думаю, что установка стилей CSS лучше, чем постоянное добавление новых тегов. Например, если пользователь выбирает один и тот же текст, сначала устанавливает цвет фона на красный, а затем устанавливает цвет фона на зеленый, определенно лучше изменить стиль CSS напрямую, а не создавать новые элементы для тех же настроек (все цвета фона). Итак, когда я это реализую, я буду судить, чтоupdateSelectionещеreplaceSelection.
const sameSelection: boolean = container && container.innerText === selection.toString();
if (sameSelection &&
!isContainer(containers, container)
&& container.style[action.style] !== undefined) {
await updateSelection(container, action, containers);
return;
}
await replaceSelection(container, action, selection, containers);
Обновить выбор
Проще говоря, это установка новых стилей CSS для существующих элементов. Например, когда пользователь изменяет цвет фона, поместите<span style="background-color: red;"/>Установить как<span style="background-color: green;"/>.
Кроме того, когда задан стиль, его дочерние элементы должны наследовать новый стиль, например, в Microsoft Word. Итак, после установки стиля я добавил еще одну функцию для очистки стиля дочернего элемента.
async function updateSelection(container: HTMLElement, action: ExecCommandStyle, containers: string) {
container.style[action.style] = await getStyleValue(container, action, containers);
await cleanChildren(action, container);
}
修改样式比新增样式要更加麻烦一些。 Например,boldилиitalic, пользователь может захотеть установить, затем отменить, затем установить, затем отменить...
async function getStyleValue(container: HTMLElement, action: ExecCommandStyle, containers: string): Promise<string> {
if (!container) {
return action.value;
}
if (await action.initial(container)) {
return 'initial';
}
const style: Node | null =
await findStyleNode(container, action.style, containers);
if (await action.initial(style as HTMLElement)) {
return 'initial';
}
return action.value;
}
дляbold,initialФункция — это простая проверка свойства.
{
style: 'font-weight',
value: 'bold',
initial: (element: HTMLElement | null) =>
Promise.resolve(element &&
element.style['font-weight'] === 'bold')
}
С цветами немного сложнее, потому что значение цвета может бытьhexа такжеrbgФормат, оба должны поддерживаться.
{
style: this.action,
value: $event.detail.hex, // 用户选中的颜色
initial: (element: HTMLElement | null) => {
return new Promise<boolean>(async (resolve) => {
const rgb: string = await hexToRgb($event.detail.hex);
resolve(element && (element.style[this.action] ===
$event.detail.hex ||
element.style[this.action] === `rgb(${rgb})`));
});
}
с помощью вышеперечисленногоinitial, я могу определить, добавлять или удалять стили.
Но этого недостаточно. Поскольку стиль выделения может наследоваться от его родительского элемента, например<div style="font-weight: bold"><span/></div>. Поэтому я создал новую функциюfindStyleNode, он ищет рекурсивно, пока не найдет элемент или контейнер с тем же стилем.
async function findStyleNode(
node: Node,
style: string,
containers: string
): Promise<Node | null> {
// Just in case
if (node.nodeName.toUpperCase() === 'HTML' || node.nodeName.toUpperCase() === 'BODY') {
return null;
}
if (!node.parentNode) {
return null;
}
if (DeckdeckgoInlineEditorUtils.isContainer(containers, node)) {
return null;
}
const hasStyle: boolean =
(node as HTMLElement).style[style] !== null &&
(node as HTMLElement).style[style] !== undefined &&
(node as HTMLElement).style[style] !== '';
if (hasStyle) {
return node;
}
return await findStyleNode(node.parentNode, style, containers);
}
Наконец, стиль будет установлен,cleanChildrenтакже будет выполнено.cleanChildrenЭто также рекурсивный метод, но вместо рекурсии на верхний уровень дерева DOM он будет рекурсивно выполнять все дочерние элементы контейнера до тех пор, пока он не будет обработан.
async function cleanChildren(action: ExecCommandStyle, span: HTMLSpanElement) {
if (!span.hasChildNodes()) {
return;
}
// Clean direct (> *) children with same style
const children: HTMLElement[] =
Array.from(span.children)
.filter((element: HTMLElement) => {
return element.style[action.style] !== undefined &&
element.style[action.style] !== '';
}) as HTMLElement[];
if (children && children.length > 0) {
children.forEach((element: HTMLElement) => {
element.style[action.style] = '';
if (element.getAttribute('style') === '' || element.style === null) {
element.removeAttribute('style');
}
});
}
// Direct children (> *) may have children (*) to be clean too
const cleanChildrenChildren: Promise<void>[] =
Array.from(span.children).map((element: HTMLElement) => {
return cleanChildren(action, element);
});
if (!cleanChildrenChildren || cleanChildrenChildren.length <= 0) {
return;
}
await Promise.all(cleanChildrenChildren);
}
Заменить выделение Заменить выделение
Стилизация выделения для замены немного проще (по сравнению с Обновить выделение), используйтеrange.extractContentsМожет получить фрагмент, который можно добавить в качестве контента в новыйspanсередина.
async function replaceSelection(container: HTMLElement,
action: ExecCommandStyle,
selection: Selection,
containers: string) {
const range: Range = selection.getRangeAt(0);
const fragment: DocumentFragment = range.extractContents();
const span: HTMLSpanElement = await createSpan(container, action, containers);
span.appendChild(fragment);
await cleanChildren(action, span);
await flattenChildren(action, span);
range.insertNode(span);
selection.selectAllChildren(span);
}
к новомуspanУстановите стиль, я могу напрямую использовать вышеприведенноеgetStyleValueфункция.
async function createSpan(
container: HTMLElement,
action: ExecCommandStyle,
containers: string
): Promise<HTMLSpanElement> {
const span: HTMLSpanElement = document.createElement('span');
span.style[action.style] = await getStyleValue(container, action, containers);
return span;
}
создал новыйspanИ после добавления фрагмента нужно идтиcleanChildrenчтобы стиль действовал на все дочерние элементы.cleanChildrenТакже как описано выше.
В конце концов, я бы постарался вообще не использовать стили.spanлейбл, так что новыйflattenChildrenфункция. он проверяет дочерние элементы наspan, если стилей нет, ставимspanстановится обычным текстовым узлом. т. е. очистить бесполезные пустыеspanэлемент.
async function flattenChildren(
action: ExecCommandStyle,
span: HTMLSpanElement
) {
if (!span.hasChildNodes()) {
return;
}
// Flatten direct (> *) children with no style
const children: HTMLElement[] =
Array.from(span.children).filter((element: HTMLElement) => {
const style: string | null = element.getAttribute('style');
return !style || style === '';
}) as HTMLElement[];
if (children && children.length > 0) {
children.forEach((element: HTMLElement) => {
const styledChildren: NodeListOf<HTMLElement> = element.querySelectorAll('[style]');
if (!styledChildren || styledChildren.length === 0) {
const text: Text = document.createTextNode(element.textContent);
element.parentElement.replaceChild(text, element);
}
});
return;
}
// Direct children (> *) may have children (*) to flatten too
const flattenChildrenChildren: Promise<void>[] =
Array.from(span.children).map((element: HTMLElement) => {
return flattenChildren(action, element);
});
if (!flattenChildrenChildren || flattenChildrenChildren.length <= 0) {
return;
}
await Promise.all(flattenChildrenChildren);
}
в общем
Подробный код, относящийся к этой статье, можно получить здесь.
- the WYSIWYG Web Component
ExecCommandStyleинтерфейс- Реализация функции
Если вы хотите загрузить локально, вам нужно клонироватьmono-repo.
В заключение
Статья почти закончена, я сам ее просмотрел, и, честно говоря, не уверен, что кто-то сможет понять то, что я написал 😅. Я надеюсь, что эта статья хотя бы пробудит ваше любопытство к проекту WYSIWYG.
Добро пожаловать, чтобы попробовать это в следующий раз, когда вы делаете слайд-шоуDeckDeckGoВы можете дать нам несколько советов и хороших идей.
Лети в бесконечность!
David(Автор этой статьи, прим. переводчика)