Пишем правильный online WYSIWYG-редактор - Web - Shelek
стоимость арболитовых блоков Введение и понимание сути проблемы
Зачем это нужно

WYSIWYG (What You See Is What You Get) — это среда, в которой пользователь сразу видит результат своей работы. К примеру, редактор Frontpage — мы сразу видим документ в практически окончательном виде, в отличие от работы с исходным кодом страницы. Сделать редактор онлайновым позволяет поддержка браузерами Microsoft Internet Explorer (версии 5.5 и выше), Mozilla (1.3+)/ Firefox и Opera (9.0+) режима WYSIWYG-редактирования текста (designMode).

Примечание 1: Mozilla и Firefox далее будем объединять именем Gecko (это название их движка).

Примечание 2: Пока Opera 9 не является релизом, поэтому про нее потом напишем. Вообще, реализация designMode в Opera похожа на реализацию такового в Gecko.

WYSIWYG-редактор позволяет значительно облегчить рутинную работу публикации контента. Также он снижает требования к квалификации работающего с контентом персонала. Так что штука это полезная и нужная.

Но есть у него и серьезный недостаток — назовем его "синдромом Ворда" — преобладание визуальности разметки над логикой, когда, условно говоря, заголовок в документе делается путем выставления большого и жирного шрифта. В нашем проекте мы попытаемся избежать этого недостатка (или по крайней мере минимизировать его).

Стоит разграничить верстальщика, который делает шаблоны, сложные конструкции, и оператора, который работает с контентом. Будем считать, что онлайн-редактор стоит делать ориентируясь главным образом именно на оператора, верстальщик пускай верстает в блокноте или специализированном редакторе. Он профессионал, ему виднее. А вот для оператора дополнительные редакторы — лишнее. Поэтому пусть он работает в административном интерфейсе с нашим WYSIWYG-редактором, одной из задач которого будет не выпускать оператора за пределы заданной дизайнером таблицы стилей (и облегчать ему "возвращение на путь истинный"). Не должно быть возможности (или она должна быть затруднена) повесить на сайт "жирный красный комик +3" в обход дизайнера.

Каким это должно быть (Правильный WYSIWYG)

Очевидно, что подавляющее большинство предлагающихся online-редакторов построены по принципу "Выберите шрифт, размер, цвет", т. е. к правильным не относятся. Ибо разметку кода, полученного с их помощью, логической назвать никоим образом нельзя. Значит, мы будем писать редактор, где пользователь оперирует сущностями типа "Заголовок N-го уровня", "Абзац-примечание" и "Важный текст", а как они выглядят — решает дизайнер сайта, прописав свое решение в таблице стилей. Поэтому наш редактор должен будет оперировать разрешенными тегами и стилевыми классами.

Воплощение

Большой проблемой, стоящей перед нами, является то, что стандартные интерфейсы к встроенному редактору, реализованные в браузерах, как раз имеют большой уклон в сторону "шрифта-размера-цвета". Этим, кстати, и обусловлена "неправильность" вышеупомянутых WYSIWYG-редакторов. В интерфейсе есть команда "покрасить шрифт", есть команда "выставить размер шрифта", но нет команды "обрамить выделение нужным тегом с нужным классом". Подробнее об API (Application Program Interface — интерфейс к программированию приложения) встроенных редакторов см. информацию на сайтах Microsoft Developer Network — MSDN и Mozilla.

Так что наша работа будет заключаться в реализации этих дополнительных возможностей.

Примечание: Поскольку война браузеров пока не кончилась, то мы имеем необходимость для разных браузеров писать разный код. Различать браузеры будем путем проверки, какой код они поддерживают, а какой — нет. Это позволит нам не заморачиваться проверкой userAgent'ов и версий.

Итак, сформулируем, что нам нужно:

Оформление выделения нужным блочным тегом (к счастью, имеется команда formatBlock) с нужным атрибутом class (а тут уже ничего готового нет) или без оного.
Оформление выделения нужным строчным (inline) тегом с классом или без.
Присвоение атрибутов (в основном классов) нетекстовым объектам — картинкам (им еще полезно присваивать src и alt), таблицам, линиям <hr>.
Очистка форматирования, не подходящего под заданную таблицу стилей (полезно при копировании текста с документов Microsoft Office, других web-страниц и т. д.).
Ну и плюс ко всему редактор должен оправдывать звание "WYSIWYG", учитывая при отображении текста CSS-файлы с сайта.

Панель редактирования

Кроссбраузерная панель редактирования представляет собой document, которому свойство designMode установлено в "On". Поскольку обычно нам не нужно, чтобы редактированию подвергалось все содержимое окна браузера, удобно заключать этот document во фрейм (обычный — frame или плавающий — iframe).

Хорошей идеей будет добавить к этому фрейму еще и textarea для возможности переключать редактор из визуального режима в режим работы с HTML-кодом.

Браузеры автоматически заменяют адреса вставляемых в редактор относительных ссылок, преобразуя их в абсолютный вид. То есть, условно говоря, если наш редактор имеет адрес http://www.site.ru/admin/, мы в него вставляем картинку с адресом image.gif, то она автоматически преобразуется в http://www.site.ru/admin/image.gif и картинку мы, скорее всего, не увидим. Это является проблемой, так как для "правильного" редактора очень желательно иметь возможность вставлять относительные ссылки.

Решать эту проблему будем так:

Во-первых, нужно, чтобы у документа, служащего панелью редактирования, адресом был бы адрес той страницы на сайте, которую мы редактируем с точностью до location.search (части адреса после "?"). Тогда относительные ссылки с текста в редакторе и на сайте будут одинаковы.

Во-вторых, следует при переключении редактора в режим HTML-source и при сохранении преобразовывать адреса ссылок в относительный вид (как минимум, удалять из них часть адреса, общую для редактируемой страницы и ссылки).

В связи с этим, во фрейм будем подгружать отдельный документ с нужным адресом (к примеру, пусть движок сайта выдает пустой документ при запросе страницы с ключом ?wysiwyg=yes). Вернее, не совсем пустой, для MSIE нужно, чтобы в документе был тег <body>, иначе нечему будет присваивать innerHTML. Выдачу движком не пустого HTML, а редактируемого текста считаем нецелесообразным, т. к. это излишне усложняет движок без какой бы то ни было пользы. Текст мы будем получать из textarea, все равно необходимой для интерфейса редактора.

Заодно в этот подгружаемый документ можно вписать подгрузку стилей:

<link rel="stylesheet" href="css-файл" type="text/css">
<body></body>,

Также таблицу стилей можно привязать к документу, загруженному в iframe, путем создания методами DOM в его head'е элемента типа <link>, указывающего на файл с таблицей стилей.

var style = document.createElement('link')
style.rel = 'stylesheet'
style.type = 'text/css'
style.href = 'myStyleSheet.css'
document.getElementsByTagName('head')[0].appendChild(style)

Здесь document — это документ фрейма-редактора.

С присваиванием контента могут быть некоторые проблемы, связанные с тем, что присваивание надо делать после всевозможных onload'ов и через некоторый таймаут после установки designMode (в MSIE). Можно предложить такое решение:
Через try-catch() пытаемся присвоить innerHTML, если не получается, делаем небольшой setTimeout и пробуем снова. Практика показывает, что даже при таймауте в 0 миллисекунд зацикливания не происходит. Можно и изначально делать присваивание по таймауту.

Примечание 1: Сначала мы устанавливаем designMode, потом присваиваем контент.

Примечание 2: В Gecko нельзя устанавливать designMode у скрытого элемента (display:none). Это надо будет учесть, так как делается редактор с переключающимися панелями WYSIWYG / HTML-исходник.

Начнем писать код.

HTML
<textarea style="width:100%;height:350px" id="wysiwyg_textarea"></textarea>
<iframe id="wysiwyg_iframe" style="display:none;width:100%;height:350px" src="canvas.html"></iframe>

Здесь мы имеем textarea для работы с HTML-source и iframe для WYSIWYG. Редактор находится в режиме HTML-source (iframe спрятан). Чтобы изменить умолчание, достаточно перенести display:none; в стили textarea.

Можно добавить кнопку для переключения режимов:

<button onclick="wysiwyg_switch_mode('wysiwyg_textarea', 'wysiwyg_iframe')">Переключить режим отображения</button>

Сейчас мы не задумываемся над особой функциональностью. Можно сделать checkbox, можно сделать переключающиеся вкладки "Normal – HTML" и т. д.

Javascript
// Инициализация редактора
onload = function(){
wysiwyg_init('wysiwyg_textarea', 'wysiwyg_iframe')
}

// Функции инициализации на вход мы даем id составляющих редактор textarea и iframe
function wysiwyg_init(textarea_id, iframe_id){
var textarea = document.getElementById(textarea_id)
var iframe = document.getElementById(iframe_id)
// Проверим на существование iframe и textarea
// Через offsetWidth проверим видимость iframe – то есть редактор находится в визуальном режиме
if(iframe && textarea && iframe.offsetWidth){
iframe.contentWindow.document.designMode = 'On'
// Для Gecko устанавливаем такой режим, чтобы форматирование ставилось тегами, а не стилями
// Чтобы MSIE не выдавал ошибку, прячем это в конструкцию try-catch
try{
iframe.contentWindow.document.execCommand("useCSS", false, true)
}catch(e){}

// Копируем текст из textarea в iframe
wysiwyg_textarea2iframe(textarea_id, iframe_id)
}
}

// Копирование текста из textarea в iframe
function wysiwyg_textarea2iframe(textarea_id, iframe_id){
try{
document.getElementById(iframe_id).contentWindow.document.body.innerHTML = document.getElementById(textarea_id).value
}catch(e){
setTimeout("wysiwyg_textarea2iframe('" + textarea_id + "', '" + iframe_id + "')", 0)
}
}

// Переключение редактора из визуального режима в HTML-режим и обратно
function wysiwyg_switch_mode(textarea_id, iframe_id){
var textarea = document.getElementById(textarea_id)
var iframe = document.getElementById(iframe_id)
if(iframe && textarea){
// редактор в режиме редактирования HTML-source
if(textarea.offsetWidth){
// Сначала показываем iframe, потом прячем textarea.
// Такой порядок для того, чтобы прокрутка не перескакивала
// из-за укоротившейся на миг страницы.
iframe.style.display = ''
textarea.style.display = 'none'
wysiwyg_init(textarea_id, iframe_id)
iframe.focus()
}else{ // Редактор в визуальном режиме
textarea.style.display = ''
iframe.style.display = 'none'
textarea.value = iframe.contentWindow.document.body.innerHTML
textarea.focus()
}
}
}

Выделение / Selection
"Выделение" (selection) является ключевым понятием в работе редактора. Это область, на которую будет распространяться команда форматирования. Она может быть текстовой и "объектной". Попробуйте в каком-нибудь редакторе (например, Word) сделать документ с картинкой, потом ткнуть мышкой в картинку и нажать Ctrl+A (выделить все) — вы увидите, что выделение картинки будет выглядеть по-разному — в первом случае она выделена как картинка (объект), во втором — как часть текста.

Если мы захотим у изображения <img> указать класс, мы должны будем выделить его объектно. Если мы хотим поставить с него ссылку — текстово.

Наш редактор должен уметь получать список выделенных узлов документа, при необходимости создавая новые (если выделена часть узла, к которому надо применить inline-форматирование).

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

Получаем начальный и конечный узлы выделения (а так же их ближайшего общего родителя)
Примечание: Здесь есть нетривиальность, связанная со "странной" реализацией выделения в MSIE.

// Взятие крайних узлов выделения (корня — root и самых крайних "слева" и "справа" — start и end)
// на вход даем окно (т.е. iframe.contentWindow)
function get_selection_bounds(editor_window){
var range, root, start, end

if(editor_window.getSelection){ // Gecko, Opera
var selection = editor_window.getSelection()
// Выделение, вообще говоря, может состоять из нескольких областей.
// Но при написании редактора нас это не должно заботить, берем 0-ую:
range = selection.getRangeAt(0)

start = range.startContainer
end = range.endContainer
root = range.commonAncestorContainer
if(start == end) root = start

if(start.nodeName.toLowerCase() == "body") return null
// если узлы текстовые, берем их родителей
if(start.nodeName == "#text") start = start.parentNode
if(end.nodeName == "#text") end = end.parentNode

return {
root: root,
start: start,
end: end
}

}else if(editor_window.document.selection){ // MSIE
range = editor_window.document.selection.createRange()
if(!range.duplicate) return null

var r1 = range.duplicate()
var r2 = range.duplicate()
r1.collapse(true)
r2.moveToElementText(r1.parentElement())
r2.setEndPoint("EndToStart", r1)
start = r1.parentElement()

r1 = range.duplicate()
r2 = range.duplicate()
r2.collapse(false)
r1.moveToElementText(r2.parentElement())
r1.setEndPoint("StartToEnd", r2)
end = r2.parentElement()

root = range.parentElement()
if(start == end) root = start

return {
root: root,
start: start,
end: end
}
}
return null // браузер, не поддерживающий работу с выделением
}
Information
  • Posted on 27.04.2013 17:55
  • Просмотры: 1928