Предварительное исследование Rich Text Editor

внешний интерфейс

В течение долгого времени, как пользователь, я был пользователем форматированных текстовых редакторов.Как интерфейсный разработчик, я являюсь пользователем только расширенных текстовых плагинов.Я мало знаю о деталях внутренней реализации.некоторые дела логика. Недавний проект, который требовал разработки простого редактора форматированного текста, дал мне возможность взглянуть на него.

Редактируемый форматированный текст

Мы знаем, что такие теги, как input и textarea в форме формы, поддерживают редактируемое содержимое, но не поддерживают форматированный текст. Если вы вставите отформатированное содержимое в эти теги, оно будет деформатировано, и останется только текстовое содержимое. Если вы хотите установить редактируемый форматированный текст, есть два способа:

  • Вставьте iframe пустой страницы и установитеdesignModeЗначение атрибута «включено», поэтому весь документ становится редактируемым.
<iframe
 name="richtext" src="blank.html"></iframe>

window.addEventListener("load",function (){
 frames("richtext").document.designMode = "on"
});

Необходимо динамически устанавливать документ iframe после загрузки встроенной страницы.designModeАтрибуты.

  • использоватьcontenteditableАтрибуты

Этот атрибут был впервые реализован в IE и может воздействовать на любой тег на странице. Вам нужно только установить вышеуказанные атрибуты для тега в документе, без встраивания iframe и установки атрибутов js, поэтому этот метод также является текущим форматированным текстом. плагин редактора в большем количестве способов;

 <div class="editbox" id="richtext" contenteditable>
    <p></p>
    <p contenteditable="false"></p>   
 </>

Таким образом, содержимое, содержащееся в этом элементе div, можно редактировать, и, конечно же, дочерние элементы (например, второй элемент P) можно сделать недоступными для редактирования. Вы также можете изменить режим редактирования, установив этот атрибут элемента через js:

var elm = document.getElementById('richtext');
elm.contentEditable = 'true';

contenteditableАтрибут имеет три возможных значения: «true», чтобы включить режим редактирования, «false», чтобы выключить, и «inherit», чтобы наследовать значение этого атрибута от родительского элемента.contenteditableСовместимость атрибутов хорошая, и они поддерживаются основными браузерами, включая IE и большинство современных мобильных браузеров.

Управление форматированным текстом

image

Распространенные плагины для текстового редактора, такие какwangEditor, БайдуUEditor, существуют различные богатые области меню для установки содержимого и формата редактирования, такие как обычная настройка заголовка, полужирный текст, гиперссылки и т. д., и победителем является вставка изображений, видео и пользовательских структур содержимого и т. д., и реализовать эти функции.document.execCommand(), этот метод является основным способом взаимодействия с редактором форматированного текста.

грамматика:

bool = document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
  • Возвращаемое значение: логическое значение, false означает, что операция не поддерживается или не включена.
  • aCommandName, имя команды, например «жирный»
  • aShowDefaultUI, следует ли предоставлять пользовательский интерфейс для этой команды, как правило, установлено значение false, основные браузеры не реализуют эту функцию.
  • aValueArgument, дополнительный параметр для некоторых команд (команда insertImage должна предоставить URL-адрес вставленного изображения)

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

Примеры часто используемых команд:

// 加粗
document.execCommand('bold', false, null);
// 超链接
document.execCommand('createlink', false, 'https://www.kaola.com');
// 格式化为h1标题
document.execCommand('formatblock', false, '<h1>');

[Примечание] Хотя все браузеры поддерживают приведенные выше команды, HTML-структуры, созданные этими командами, все же различаются. Подобно жирной команде, IE и Opera будут использовать<strong>Вкладки переносят текст, в то время как Safari и Chrome используют<b>теги, использует firefox<span>.

Методы, связанные с командами:
  • запроскомманденаблед Возвращает логическое значение, которое определяет, может ли команда быть выполнена для текущего выделенного текста или текущей позиции курсора;
var canBold = document.queryCommandEnabled("bold");
  • запросCommandState Возвращает логическое значение, используемое для определения того, применена ли указанная команда к выделенному в данный момент тексту;
var isBold = document.queryCommandState("bold");

Вы можете использовать этот метод, чтобы установить в редакторе состояние таких кнопок, как выделение полужирным шрифтом, курсивом и т. д.

  • QueryCommandValue При использовании для получения выполнения команды сторонний параметр метода execcommand())

Выбор расширенного текста

Объект Selection относится к текстовому диапазону, выбранному пользователем, или к текущей позиции мыши.window.getSelection()чтобы получить объект.

image

Свойства объекта Seletion следующие:

  • anchorNode: узел, в котором находится начальная точка выделения;
  • anchorOffset: количество символов, содержащихся в области выбора в anchorNode;
  • focusNode: узел, в котором находится конечная точка выделения;
  • focusOffset: количество символов, включенных в область выбора в focusNode;
  • isCollapsed: boolean, совпадают ли начальная и конечная точки области выделения, если да, то можно считать, что в данный момент не выбрано содержимое;
  • rangeCount: количество диапазонов DOM, содержащихся в выборке;
  • type: описывает тип текущего выделения

О методах объекта Selection см.MDN. Эти методы очень полезны в плагине редактора форматированного текста, например, метод управления курсором.collapse()、collapseToEnd()、collapseToStart(), вы можете установить положение курсора после вставки содержимого; метод получения текста, содержащегося в выделенииtoString();getRangeAt(index)Метод возвращает диапазон DOM в выборке, соответствующей индексу, т.е.объект диапазона

Давайте посмотрим пример:

// 获取选区内容的位置
function  getSelPos () {
    let sel = window.getSelection()
    let rg = sel.getRangeAt(0)
    let elmRect = rg.getClientRects()[0]
    let editorRect = $('.j-editor')[0].getBoundingClientRect() // 编辑器容器
    let pos = {}
    if (elmRect) {
      // 选区内容居中位置距容器的左距离
      pos.x = elmRect.left - editorRect.left + elmRect.width / 2 
      pos.y = elmRect.top - editorRect.top
    }
    return pos
}

Вышеупомянутый метод может получить положение текущего выделения относительно контейнера редактора и может использоваться для установки панели инструментов, которая появляется рядом с выделением. Если вы хотите отслеживать изменения в избирательном округе в режиме реального времени, вы можете отслеживатьonselectionchangeмероприятие

// 高频事件,做好节流
document.onselectionchange = _.debounce(this.onSelect, 100)

Обработать содержимое вставки

Если вы вставляете содержимое в редактор форматированного текста, стиль содержимого также будет вставлен, и браузер автоматически встроит стиль, примененный к тегу, в атрибут стиля этого тега. Но чаще нам просто нужно сохранить некоторые форматы внутри, и нам нужно фильтровать, форматировать и сохранять определенный контент для контента в буфере обмена.


    editorElem.on('paste', event => {
        event.preventDefault();
        let clipboardData = event.clipboardData || event.originalEvent && event.originalEvent.clipboardData || {};
        let text = clipboardData.getData('text/plain');
        let html = clipboardData.getData('text/html');
    })

Прослушивая событие вставки, можно получить объект clipboardData объекта события, можно получить вставленное содержимое, а с помощью метода getData можно получить обычный текст или структуру html в буфере обмена. С помощью структуры html его можно преобразовать в объект dom для обработки. По умолчанию буфер обмена

Далее основное внимание уделяется обработке изображений в буфере обмена. Если элемент веб-страницы в буфере обмена содержит картинку, т.е. тег img, то при прямой вставке в редактор адрес ссылки на картинку является адресом картинки исходной веб-страницы, здесь это нужно учитывать. Если вы свяжете изображения с других веб-сайтов, может быть, однажды это изображение будет недоступно, поэтому вы можете безопасно разместить его на своем собственном сервере. Это включает в себя, зная доступный адрес ссылки изображения, как загрузить изображение на свой собственный сервер?

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

  1. Создайте новый объект Image и задайте для атрибута src URL-адрес известного изображения;
  2. В событии загрузки объекта изображения создайте холст холста и получите данные изображения с помощью его метода toDataURL;
  3. Преобразуйте данные изображения в объект большого двоичного объекта и вызовите интерфейс загрузки изображения, чтобы загрузить объект большого двоичного объекта;
// 根据图片的url,上传图片
export function uploadImgWithUrl(imgUrl,  editor) {
    /**
     * 数据转blob对象
     */
    function dataToBlob(data) {
        var bytes = void 0;
        bytes = data.split(",")[0].indexOf("base64") >= 0 ? window.atob(data.split(",")[1]) : unescape(data.split(",")[1]);
        var paramType = data.split(",")[0].split(":")[1].split(";")[0];
        var uArr = new Uint8Array(bytes.length);
        for (let i=0; i < bytes.length; i++) {
            uArr[i] = bytes.charCodeAt(i);
        }
        return new Blob([uArr], {
            type: paramType,
            name: 'blob.png'
        });
    }

    var options = this;
    return new Promise(function(resolve, reject) {
        var img = new Image;
        img.setAttribute("crossOrigin", "anonymous");

        img.onload = function() {
            var canvas = document.createElement("canvas");
            canvas.width = img.width;
            canvas.height = img.height;
            canvas.getContext("2d").drawImage(img, 0, 0);
            var data = canvas.toDataURL("image/png");
            var blob = dataToBlob(data);
            
            blob.name = 'blob.jpg'
            // 调用编辑器的上传图片接口 or 也可自行实现一个图片上传方法
            editor.uploadImg.uploadImg([blob], function(result){
                if (result && result.body) {
                    var link = result.body.imageUrlList || [];
                    var item = {
                        resourceType: 'image',
                        imageUrl: link[0]
                    }
                    resolve(item);
                }
            })
        };
        img.onerror = function() {
            reject();
            Message.error('图片不允许跨域访问,请手动下载后添加')
        };
        imgUrl = -1 !== imgUrl.indexOf("?") ? imgUrl + "&time=" + (new Date).getTime() : imgUrl + "?time=" + (new Date).getTime();
        img.src = imgUrl;
    });
}

Выше приведено решение для загрузки онлайн картинок на сервер, которое также практикуется в проекте. Для содержимого изображения в буфере обмена вы можете использоватьgetAsFile()способ получения и загрузки:

// 处理内存中的图片
if (clipboardData.items[0]) {
    let item = clipboardData.items[0]
    let type = item.type;
    let regResult = type.match(/image\/(.+)/)
    if (regResult) {
        let blob = item.getAsFile();
        // 调用编辑器的通用上传接口
        editor.uploadImg.uploadImg([blob])
    }
}

наконец

Вышеупомянутое является лишь предварительным обзором основных пунктов знаний редактора форматированного текста.Если вы хотите построить свои собственные колеса и создать редактор, есть еще много проблем, которые необходимо решить.Вы можете прочитать обсуждение на Zhihu.Почему вы говорите, что редактор форматированного текста — это провал?, в котором упоминалось, что реализация удовлетворительного редактора требует всевозможных подводных камней, а также хороших шаблонов проектирования, а путь предстоит пройти долгий путь...


использованная литература

by lzf

Попробуйте обратить внимание на паблик WeChat фронтенд-команды NetEase Koala.

image