WYSIWYG HTML редактор в браузере. Часть 2

http://dev.opera.com/articles/view/rich-html-editing-in-the-browser-part-1/
  • Перевод
Это вторая часть перевода статьи о свойствах designMode и contentEditable, их поведении и особенностях.

В статье рассматриваются базовые принципы и проблемы унификации особенностей редактирования в современных браузерах. Темы рассматриваемые в статье:
  • Различные методы включения режима редактирования
  • Команды редактирования
  • HTML генерируемый в процессе редактирования
  • Взаимодействие с DOM

WYSIWYG HTML редактор в браузере. Часть 1 находится тут.

Команды редактирования


В браузерах поддерживается множество команд редактирования. HTML генерируемый этими командами не стандартизирован и отличается от браузера к браузеру. Например, в IE «Bold» сгенерирует код:
  1. <strong>Hello!</strong>
Safari генерирует:
  1. <span class="Apple-style-span" style="font-weight: bold;">hello!</span>


Код, как минимум в IE, несколько старомоден. Во множестве команд используется ужасный тег font:
  1. <FONT color=#ff0000>23</FONT>

Генерируемая разметка не является валидным XHTML и как правило даже валидным HTML кодом.

Opera генерирует код сходный с IE (нет, не полностью), используя тег font и так далее.

Safari генерирует форматирование используя span'ы и инлайн CSS. Достоинство подхода Safari в том, что он по крайней мере является валидным HTML 4.01 Strict.

Mozilla поддерживает 2 режима работы — она либо генерирует презентационные теги как IE/Opera или же стилизирует контейнеры как Safari.

Если вы уверены, что вам нужен валидный HTML код, то вам стоит чистить генерированный редактором код на стороне сервера, что бы из этого безобразия получить валидный (X)HTML. (Ну, вам это в любом случае стоит делать, что бы избежать XSS-атак).

Горячие сочетания клавиш


Множество команд доступно через горячие сочетания клавиш, например Ctrl/Cmd + B для полужирного начертания, Ctrl/Cmd + Z для отмены последнего действия и т.д. Но эти сочетания могут различаться в зависимости от локализации браузера.

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

API редактора


Возможно, вы захотите сделать панель управления, что бы позволить пользователю использовать команды редактирования. Это можно сделать с использованием API редактора. Это API не похоже на обычное API DOM, на самом деле это скриптовая адаптация интерфейса IOleCommandTarget, который является COM интерфейсом используемым Microsoft для синхронизации панели управления и редактирования документа.

API находится в объекте document и состоит из метода execCommand и нескольких методов с префиксом «query» которые возвращает информацию о команде.

Все методы принимают ID команды в качестве первого аргумента, это строка, содержащая имя команды. Собственно, методы:

ExecCommand


Выполняет команду применительно к текущему выделению. Некоторые команды переключаются между состояниями в зависимости от контекста. Например, если вы применяете команду «Bold» к выделению, которое уже имеет полужирное начертание, то выделение будет отображено в нормальном начертании. Некоторые команды тредуют аргументов, например forecolor требует в качестве аргумента код цвета.

Некоторые команды вызывают модальные диалоги — например, команда link вызовет диалог, который предложит ввести URL. Диалоги нельзя изменить, но возможно заменить их. Например:
  1. result = document.execCommand(command, useDialog, value)
Что есть что:
command: Строка; имя команды.
useDialog: Булев тип; Показывать ли встроенный диалог (Не все команды могут вызывать диалоги).
value: Значение, принимаемое командой. Не все команды принимают значения; Если вызывает диалог, то значение береться из него.
result: true если команда выполнена, false если отменена пользователем (закрытием диалога) или команда не может быть выполнена.

Если выделения нет (только курсор), команды форматирования текста применяются по разному, в зависимости от браузера. Если курсор внутри слова, IE применит форматирование к слову; остальные браузеры применят его к следующему символу, который будет набран, если курсор не будет перемещен назад.

QueryCommands

Query-команды имеет смысл использовать для определения состояния кнопок на панели управления в зависимости от текущего выделения и положения курсора.

QueryCommandEnabled

Определяет возможно ли выполнение команды применительно к текущему выделению. Например «unlink» можно применить только в том случае, если курсор находится внутри ссылки. Если выделение содержит область недоступную для редактирования, то никакие команды выполнятся не будут.

QueryCommandState

Показывает была ли команда применена к выделению, тоесть если выделение имеет полужирное начертание, то QueryCommandState вернет true для команды bold.

QueryCommandValue

Возвращает значение некой команды для выделения. Значение соответствует тому, которое было задано при использовании execCommand, тоесть, например для ForeColor это будет код цвета (в виде строки) для текущего выделения.

Формат будет отличаться в разных браузерах. Например, ForeColor возвращает шестнадцатеричное значение в IE (такое как #ff0000), а другие вернут RGB выражение, такое как Rgb(255,0,0).

Некоторые значения вариируются в зависимости от локализации браузера, например значение для FormatBlock в IE вернет имя для параграфа на языке локализации интерфейса баузера.

Команды, для которых значений нет, например bold, просто вернут false. (API содержит два дополнительных метода, queryCommandSupported и queryCommandIndeterminate, но они слишком глючные, что бы хоть как то их использовать.)

Range и Selection API


Встроенные команды полезны, но невозможно изменить их поведение или добавить нестандартную реализацию. Используя Range и Selection API вы можете использовать произвольные HTML трансформации, которые можно использовать для реализации нестандартных команд.

Проблема в том, что любые трансформации затрагивающие DOM разрушают стек undo, который используется для реализации команд UnDo/ReDo. Это не слишком хорошо, но может быть приемлемой ценой за нестандартные команды.

Range/selection API имеют два основных класса:

Range — непрерывный диапазон символов документа. Диапазон может перекрывать границы элементов. У диапазона есть стартовая и конечная точки. Если стартовая точка совпадает с конечной, то диапазон называется вырожденным.

Selection — представляет текущее пользовательское выделение в документе. Выделение содержит один подсвеченный диапазон. Если диапазон выделения вырожден, то он отображается как курсор. (Диапазоны и выделения могут быть использованы вне элементов в режиме редактирования. Вы можете создать выделение в документе, доступном только для чтения. Такое выделение, впрочем, не может быть вырожденным, так как курсор показывается только когда элемент в режиме редактирования.)

Эти принципы идентичны во всех браузерах, но сами API отличаются по реализации в IE и всех остальных браузерах. IE использует собственное проприетарное решение для range м selection API, остальные браузеры используют W3C DOM Range API в комбинации с нестанжартизированным selection API.

Основное отличие в том, что в IE содержимое диапазона доступно как строка с HTML разметкой. В W3C DOM Range API, содержимое диапазона доступно как дерево узлов DOM.

Пример диапазона

Что бы показать различные подходы, ниже приведена команда, которая применяет тег «code» к текущему выделению.

В IE (editWindow это ссылка на frame в designMode):
  1. var rng = editWindow.document.selection.createRange();
  2. rng.pasteHTML("" + rng.htmlText + "");
В Mozilla:
  1. var rng = editWindow.getSelection().getRangeAt(0);
  2. rng.surroundContents(document.createElement("code"));

Выделение элементов управления


IE поддерживает выделение элементов управления, которое отличает от обычного выделения диапазона. Выделение элемента управления происходит, когда вы кликаете на объект, такой, как изображение, элемент управления формы или рамку таблицы.

В IE возможно выделение более, чем одного элемента одновременно с использование комбинации ctrl+клик. Остальные браузеры не поддерживают концепцию выделения элементов управления; в остальных браузерах выделение это всегда текстовый диапазон.

Выводы


Статья рассматривает основные принципы редактирования данных в браузере. Часть вторая статьи показывает множество примеров применения вышеописанных API.

От переводчика: если это интересно не только мне, то я переведу и вторую статью.

И пара ссылок на дополнительные материалы:
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 46
    +3
    Эх, помню, как-то принялся я свой визивиг писать…
      +4
      Написали бы об этом опыте. Это так занимательно, насколько я понимаю, что просто слов нет. Только маты.
        +1
        Да, до сих пор снится мне в страшных снах. Ну ооочень занимательно.
          0
          Кстати, мне вот на полном серьёзе сниться =(
      –2
      > Если ты уверены
        0
        Спасибо. Поправил.
        +7
        В статье описан один вариант реализации WYSIWYG HTML редактора.

        Есть еще второй вариант, так сказать Google Docs way — когда designMode не используется. Вместо него присутствует JS эмуляция, вплоть до того, что курсор рисуется обычным html элементом.
          +2
          В частности, это позволяет использовать rich text editing на устройствах вроде iPad, где designMode увы не поддерживается.
            +2
            Это очень сурово.
            Очень круто. И очень-очень сурово.
              +2
              Или как в Bespin — используется canvas
                +6
                по-моему, это единственный правильный подход, если хотите сделать нормальный результат на выходе, которым легко управлять.
                  0
                  Затраты на разработку редактора основанного на designMode и contentEditable, думаю, будут на несколько порядков меньше, чем если полностью написать его на js.
                  А если нужен мини-редактор с 2-я 3-я дополнительными функциями (изменение начертания на курсивное или полужирное и вставка пары тройки специальных символов. Например значков карт для социальной сети любителей покера) то делать решение на без designMode/contentEditable мягко говоря не выгодно.
                    +2
                    в свое время свой визивиг делал, да перестал поддерживать, т.к. времени нет на него. Так вот мое резюме — не факт, что вариант гугла будет дороже в конечном итоге. там очень много всего такого, что отличается не только в броузерах, но и в их версиях.

                    Мой вариант визивига здесь:
                    code.google.com/p/lwrte/

                    Когда делал его преследовал простую мысль — сделать ядро которое бы позволяло:
                    переключаться в режим и обратно, разные тулбары для каждого из режимов, что-то типа АПИ для каждого из режимов (получить выделенный текст, заменить его и т.п.). А весь функционал самого визивига уже перенести в сам тулбар и подключать отдельно.

                    В общем-то все получилось, но как писал выше — поддерживать нереально, когда это никак не участвует в основных проектах в данный момент.
                      0
                      Спасибо за ссылку. При случае покопаюсь в коде.
                      Но касательно того, что вариант гугла будет дешевле я очень сомневаюсь.
                      Попробуйте реализовать что то подобное и увидите, что там тоже масса подводных камней, даже не учитывая того, что вы в этом случае беретесь реализовать функционал, который, пусть худо-бедно, но уже реализован браузерами.
                        +3
                        смысл-то не в том, что «более-менее» реализован броузерами, а в том что нет одинакового результата на всех броузерах всех версий и под все ОС. в результате начнутся костыли. поэтому вариант гугла — не самый худший. Но опять же, чтобы любой вариант выбрать, визивиг должен быть не последним элементом в системе. и не на пару-тройку месяцев.
                          +1
                          Зависит от требований к визивигу.

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

                          Костыли они вездесущи.
                    0
                    Да, но это практически полностью лишает пользователя возможности работать с DOM редактируемого документа. Т.е. в standalone-решениях типа google docs и ms docs это и не нужно. Для редакторов, которые создаются для встраивания (для разработчиков), типа ckEditor (ex fckEditor), ASPxHtmlEditor — такой функционал полезен. Кроме того, генерация чистого HTML документа уже становится специфической задачей.

                    В общем, как уже сказал Beresta, здесь полностью все, что делает в contentEditable моде сам браузер, переносится «на плечи» скрипта (js/html5) редактора.
                      +3
                      А что за Beresta? Не могу нагуглить.
                        +2
                        Это он меня имел ввиду :)
                          +2
                          >_< Ой. Простите.
                    0
                    Вот сейчас смотрю через firebug открытый на редактирование документ в google docs и вижу там iframe c designMode=«on». Что я делаю не так?

                    На самом деле вариант с заменой iframe на canvas или манипулирование содержимым самостоятельно неработоспособен по одной простой причине — буфер обмена
                      0
                      Моя ошибка. Оказывается старые документы открываются так как они были созданы. В новых уже div. Интересный способ, надо б поизучать
                        0
                        нормально всё с буфером обмена. можете попробовать: dev.freecr.ru
                          0
                          пока только один баг нашёл — нельзя выделять текст мышкой. хотя, вполне возможно, и это можно как-то обойти.
                            0
                              0
                              Способ установки фокуса на элемент с возможностью ввода текста уже у гугла подсмотрел. Только у них вместо texarea используется все тот же iframe с designMode=«on» со всеми вытекающими.
                            0
                            Вместо него присутствует JS эмуляция, вплоть до того, что курсор рисуется обычным html элементом.

                            кстати, а есть такие редакторы уже?
                              0
                              Встраиваемых не встречал пока, только приложения — Google Docs и docs.com от MS.

                              Дело скорее всего в том, что порог вхождения в случае полной эмуляции существенно выше — нужна серьезная команда и большое количество времени что бы функциональный редактор сделать.
                            0
                            С разработкой wysiwyg редактора знаком совсем не понаслышке :)
                            Форматирование с помощью execCommand, в большинстве случаев — просто кошмар! Нормально (те более-менее одинаково во всех браузерах) работают только манипуляции со списками и блочное форматирование. Все остальное лучше реализовать самостоятельно — хотя это очень непросто.
                            В первой версии мы во многом положились именно на execCommand и очень быстро поняли, что неправы :(
                            В делоперской версии это уже исправлено, но до ее выхода еще не близко
                            А разница в реализации textRange с легкостью вынесет мозг начинающему разработчику
                              0
                              А как вы решаете проблему с UnDo/ReDo? Я так понимаю, что после изменения dom без использования execCommand они уже не работают и их надо реализовать руками, да?

                              Я пробовал использовать команду insertHTML, но результат меня печалит.
                              Похоже придется в своей тестовой поделке использовать таки диапазоны.

                              document.execCommand(«insertHTML»,true,"
                              "); — например в хроме не работает если курсор в самой правой части текста внутри параграфа.
                              Про поведение оперы я вежливо промолчу. Она под win норовит забыть отрисовать изменения в блоке с contentEditable. Ох.
                                0
                                С UnDo/ReDo в текущей версии полагались почти полностью на встроенную реализацию — результат не очень. В новой версии отслеживаем события редактора и при наличии изменений добавляем новый уровень к истории. Шоткаты отката/повтора переопределили.
                                insertHTML тоже не используем — собственная реализация работы с textRange и DOM. Если интересно — можете посмотреть в нашем гите ветку master
                                +1
                                О! А еще гениальная придумка MS с позиционированием выбранной области по символам, и где она окажется в dom-дереве это как правило большой сюрприз.

                                А еще чего стоит баг отображения если в редактируемом тексте встречается <br> непосредственно перед закрывающим </p>

                                Тоже не по наслышки знаком. Фазу к черту execCommand уже прошел и в рабочей версии =)
                                0
                                Да, кстати, вторую статью цикла перевести?
                                  0
                                  конечно) очень интересно, думаю, будет
                                    0
                                    ок. тогда скорее всего завтра выложу.
                                  0
                                  В FireFox'е execCommand-команда «inserthtml» разрешает вставлять произвольный html-код. Это позволяет заменить другие execCommand-команды на свои варианты. Большая проблема только в том, что другие браузеры не работают с inserthtml. :(
                                    0
                                    Так то оно — так. Но без анализа вставляемого кода и того, куда он вставляется — вероятность получить совсем кривой html — почти 100%
                                      +1
                                      inserthtml к сожалению ведет себя вовсе не так здорово, как кажется на первый взгляд.
                                      Он бааажный.
                                      Кстати нету его только в IE, если не путаю.
                                        0
                                        В IE у textRange есть метод pasteHTML.
                                        Про inserthtml я это и говорил — слишком много надо делать, чтобы с его помощью получить валидный хтмл — имхо, оно того не стоит
                                      0
                                      Ну «куда вставляется» как раз понятно — либо в позицию курсора, либо обрамляем выделение.
                                        0
                                        по поводу генерации одинакового кода, а заодно и зачистки вордовского мусора делал так:

                                        сделана ф-я cleanup (выдранная из tiny 2.x на то время 3 еще не было) добавлены regex'ы для унификации html (тоесть киляем все что не нужно, все что нужно приводим к эдиному виду)

                                        вешаемся на onchange (и ему подобные события) и onblur и вызываем эту ф-ию

                                        чтобы ф-ия не положила браузер при каждом вызове onchange — сверяем количество символов до и после и если разница больше N тогда запускаем зачистку

                                        года два назад делал, за основу был взят пример из этой же статьи, суммарно вышло 500 с хвостиком строк js кода, что было в сотню раз меньше чем tiny (требовался минимальный функционал b,i,ul,ol и вставка из ворда)
                                          0
                                          Если команды редактора генерят корректный код, то так часто чистить не нужно. Только при вставке, переключении в исходник и обратно и перед сохранением
                                            0
                                            Команды редактора, при условии что мы юзаем execcommand, генерят не желательный код, те же span'ы с классами и стилями.

                                            Повеситься на вставку ну никак не получается, по этому пришлось извращаться с запоминанием количества символов.

                                            Переключалки в исходник нет как таковой — редактор для секретарш.

                                            И самое главное: зачистка на сохранении это конечно хорошо и обязательно, но тогда возможны ситуации когда, к примеру я скопипастил текст, дописал\подправил давлю на сохранить оно его зачищает и делает не красивым\не таким как задумано и сохраняет.
                                            Именно по этому зачистка должна происходить по чаще, чтобы пользователь максимально рано увидел результат.
                                              0
                                              Я имел ввиду «правильные» кооманды, которые не пользуют execCommand :)
                                              Почему нельзя подцепиться за вставку текста? Для нормальных браузеров есть событие paste (если не ошибаюсь даже ie его поддерживает) для оперы по keydown ловим ctrl+v и с небольшим таймаутом запускаем очистку. Единственное, что не ловится в таком варианте — вставка через контекстное меню в опере
                                                0
                                                Вставка, кстати, может производится т.о.:
                                                Выделяется кусок текста и перетаскивается в область редактирования — так же, приходится учитывать.
                                          0
                                          А если использовать какое-нибудь TeX-подобное промежуточное представление текста, которое будет рендериться в зависимости от пользовательского клиента? Правда, придется либо активно использовать AJAX для сохранения временных файлов на сервере, либо терпеть оверхэд по памяти…

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое