Пишем правильный online WYSIWYG-редактор. Подробнее в картинках ниже

При очередном написании кода, а точнее простенького движка, решил реализовать редактор самих страниц сайта и в том числе админки. Многие на форумах пишут — «Зачем это надо, редактируй в блокноте». А я вот хочу как в вордпрессе, но чтобы для редактирования была доступна даже сама страница редактирования.

СТАТЬЯ МОЖЕТ НЕ ВСЕМ БУДЕТ ПОНЯТНА. СНАЧАЛА ВЫЛОЖУ ВЕСЬ ИСХОДНИК ТОГО, ЧТО УСПЕЛ СДЕЛАТЬ. Яндекс диск ссылка В ПАПКЕ WWW ВЕСЬ ИСХОДНИК С TPL ФАЙЛАМИ

Сам редактор находится в админке, в файле admin.php . Установите Denwer запустите админку для наглядности.

Написать сам редактор было не так сложно. Написал функцию вывода файлов из корня сайта и всех папок, рекурсивным методом. И добавил функцию вывода содержимого файла в textarea , с помощью file_get_contents() и вроде все работало. Сохранение содержимого через file_put_contents() с последующим обноврением страницы редактора, НО.

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

Подробнее в картинках ниже:

1. То как реализован редактор. Выводим в textarea содержимое файла с помощью функции.

2. То что содержит функция вывода после того, как прочитала файл редактора.

3. То что видно в исходном коде редактора, когда загружен файл для редактирования его кода.

4. Кусок кода, который отображается в самом редакторе, в textarea .

Из скринов видно, что часть кода прочитана браузером именно до строки закрытия злосчастного textarea . Итог, очевиден, редактор отображается с косяком, и если сохранить то что оказалось в редакторе, редактор перестанет работать, так как там будет только часть нужного кода.

Решение проблемы реализовал обычной заменой конечного тега textarea на сложное выражение вида, к примеру: \.*-+*-*06textarea-*/+*-+*/+\ . Функцией str_replace() .

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

А при сохранении отредактированного кода в рабочий файл, делаем обратную процедуру. И файл, который будет отображаться на вашем сайте будет с нормальным конечным тегом textarea . Метод не лучший но, на худой конец и то решение.


АРМ генерации ключей ошибка при открытии word документа.

При работе с программой АРМ генерации ключей часто возникают проблемы. Некоторые из них описаны на официальном сайте казначейства, однако проблема с ошибкой при распечатке заявления, в конце процедуры генерации, так […]

Установка и активация Корел Дро/Corel Draw X4

Многие сталкиваются с проблемой установки и активации программы Корел Дро. Данная статья расскажет Вам, как решить проблему с установкой и активацией. Материал подготовлен исключительно для ознакомления, устанавливая программы таким способом, […]

Посвящается всем, кто имеет дело с «холодным» кошельком luna wallet и монетой Expanse. В данной статье хотелось бы разобрать проблему отправки средств с кошелька на биржу. Дело в том, что […]

в форме textarea необходимо создать текстовый редактор с самыми простыми тэгами форматирования (болд, италик). но сами тэги при этом, как сделано в большинстве подобных редакторов, не должны вписываться в textarea, а выделенный текст после форматирования должен прямо в textarea становиться болдом, италиком или болдом и италиком одновременно.
и вторая проблема, которую не удаётся решить. необходимо, чтобы текст, введённый с клавиатуры или вставленный из буфера обмена, ограничивался 11 строками, при этом в каждой строке должно быть не более 40 символов. перенос на следующую строку, разумеется, должен быть автоматическим.

заранее спасибо!!!

посмотрел некоторые wysiwyg-редакторы, но это огромные программы с кучей наворотов. а мне нужно лишь форматировать текст тэгами bold, italic и underline. ну, может быть ещё вставка текста из буфера и копирование в буфер, но это не обязательно. сам я вырезать нужные функции из wysiwyg-редакторов не могу. конечно, можно использовать найденные js целиком, не вырезая нужные функции, но файлы весят очень много.
и как сделать ограничение количества символов в textarea? через maxlength не работает. желательно, чтобы пользователь видел сколько символов он ещё может набрать, т.е. необходим счётчик.

Ну народ ломанулся за онлайновыми юзерконтролами... :)
Хочу вам сказать, уважаемый самовар, что я над этим дурацким wysiwyg редактором, с базовыми функциями, работаю уже второй месяц, и если у меня все волосы не выпадут от этой работенки, то это будет счастьице... :)
А в сети есть пока только один текстовый редактор - TinyMCE. Но грузовик к нему, увы, не прилагается...

Дмитрий, я вот сейчас и копаюсь с этим самым TinyMCE. но вырезать минимальные функции, которые мне нужны, я не в состоянии.

ок, тогда давайте сузим поставленную задачу. мне необходимо ввести или вставить из буфера в textarea текст, который будет ограничиваться 10-ю (или 11-ю, я ещё не решил сколько строк будет точно) строками. как сделать так, чтобы после набора последней строки текст не набирался или обрезался, если ввод идёт из буфера обмена? и необходим счётчик, который говорит пользователю сколько строк у него ещё осталось. т.е. после перехода на следующую строку счётчик показывает n=n-1, где n - общее количество строк. перевод строки идёт при wrap="hard".

заранее спасибо!!!

Полезные функции (Фильтр ввода для текстового поля) . Посчитать количество символов в value сами сможете.
А вообще, дорогой самовар[досье] , постановка задачи странная - я сам не умею, а вы сделайте. Или научитесь, или предлагайте желающим решить это за денги. Альтруизму есть предел.

уважаемый, Евгений Петров! количество символов я посчитать могу, но нужно посчитать количество строк, которое должно быть не более 10. в каждую строку помещается разное количество символов, т.к. перенос на следующую строку идёт не при достижении определённого лимита по количеству символов, а перед словом, которое не помещается в строку.
с вашим замечанием я абсолютно согласен, всему есть предел. поэтому я и не стал наглеть в случае с wysiwyg-редактором, а копаюсь с ним сам. но я совершенно не представляю как посчитать количество строк в textarea. я не прошу полностью всё сделать, но хотя бы натолкнуть на определённое решение.

Самовар, есть в принципе вариант решения со скрытым дивом (т.е. visibility="hidden"), которому задана фиксированная ширина.
Когда пользователь вводит текст в textarea, при каждом изменении текста вы выводите весь текст в этот самый скрытый див. Поскольку ширина у него фиксирована, то растягиваться он будет по высоте. Далее, зная высоту стандартной строки (Ну скажем шрифт 10pt это 16px высоты) и высоту растянувшегося дива (clientHeight), вы можете посчитать количество введенных строк. Если строк больше чем надо, выводите алерт с сообщением об ошибке.
Таким вот образом.

самовар[досье] "строка" понятие растяжимое в прямом и переносном смысле этого слова. Почему именно 11 строк? Это ограничение технологическое или бизнес-логики? Если технологическое, то его всегда можно выразить в кол-ве символов. Если бизнес-логики, то опять же проще не бороться с пользователями и браузерами а изменить бизнес-логику - от этого все только выиграют.

всем большое спасибо за ответы!
Евгений Петров, э-э-э... спасибо, буду думать, но пока не придумал)))
Дмитрий, такая идея у меня тоже была, но в том-то и дело, что размер шрифта и сам шрифт могут меняться.
GRAy, я не знаю чем определяется такое задание, общей картины концепции текстового редактора у меня нет. попросили сделать ограничение по количеству строк. а по количеству введённых символов не получается зафиксировать количество строк, т.к. символов в каждой строке будет разное количество. поэтому количество строк при одинаковом количестве символов может быть +-2.

я тут нашёл следующий кусок кода для эксплорера, автор говорит, что можно подсчитать количество строк, но я не пойму как этот кусок кода состыковать с формой textarea:

function textareaCurLineNum(obj)
{
var rowHeight = obj.clientHeight/obj.rows;
var curHeight = obj.createTextRange().boundingHeight;

return parseInt(curHeight/rowHeight)+(obj.value!=""?1:0);
}

самовар[досье] Я как раз говорил о том, что надо бы понять почему введено это ограничение. Его реализация

  1. нетривиальна а потому займёт кучу времени и неизвестно будет ли достигнут результат
  2. реальные ситуации в которых это ограничение может пригодиться представить себе трудно и значит что потраченные усилия вряд-ли когда-то окупятся.

GRAy, я выяснил почему задача должна быть поставлена именно в таком виде. из различных статей собирается минигазета, которая потом переводится в pdf. статьи могут быть 1/4, 2/4, 3/4 или на всю страницу. наборщик сам выбирает нужную форму с фиксированным количеством строк. я в тонкости не вдавался, но думаю, что на странице вписывается много форм с разным количеством строк. если статья не помещается в форму, то берётся другая, с большим количеством строк... я так понял, это автоматизируется работа наборщика. хотя я могу и ошибаться, в тонкости, как я уже говорил, не вдавался, просто знакомые попросили помочь.
главное - это ввести ограничения по строкам и установить счётчик строк.

Ну вы же сами сказали что строк может быть не более 11 и в каждой строке не более 40 символов. Таким образом максимальное кол-во символов будет 440. Посчитать символы в строке не проблема, алгоритм усложняется только за счёт того, что надо встреченные символы переноса строк считать как "длиные" символы длина которых равна разнице между 40 и остатка от деления кол-ва набитых символов до данного переноса строки (без учёта самого символа переноса) на 40. C отловом вставки из буфера - это отдельная тема. Для IE есть onbeforepaste а для мозил нечто подобное ожидается только в FF 3. На вашем месте я бы сделал пост-валидацию введённых данных с последующей нотификацией пользователя и не морочился с real-time подсчётами.

А также есть различные типы шрифтов (моноширинные, например)... Для них количество символов будет отличаться при одинаковом заполнении. IMHO проще эмулировать полосу визуально, а не заниматься подсчетами, которые не соответствуют реальности.

Евгений Петров[досье] По-хорошему, если уж говорить о некой "помощи наборщику" надо учитывать всё ;) и кегль, и ширину каждого символа в выбранном шрифте и межстрочный интервал и т.д и т.п. Автор всё-таки оговорил в начале, что строки фиксированой по кол-ву символов длины и в таком случае можно пренебречь размером и типом шрифта.

самовар[досье] , дались Вам эти строки. Зачем их считать, если речь идет о том, влезет или не влезет материал в прямоугольник заданного размера. Евгений Петров[досье] совершенно прав, это зависит от того, каким шрифтом набирать материал.
Дмитрий[досье] дал Вам мудрый совет, копировать весь текст в div, смотреть на его размеры и делать выводы.

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

самовар[досье] Вы всё-таки доведите до сознания заказчика что длина строки в символах это не то же самое что длина строки в сантиметрах. Пусть они выдадут вам чёткие указания по поводу того как они будут разруливать ситуацию с разными шрифтами, размерами шрифтов и т.п. Если принебречь параметрами шрифта зная длину строки вашу задачу можно решить так, как я описывал, в противном случае - нет.

самовар[досье] Тогда пиши пропало;) HTML не предназначен для вёрстки с точностью до миллиметров. Реализовать то что они хотят на базе браузера может и возможно но так геморройно что лучше и не начинать - людей не удовлетворите и себе репутацию подмочите.

Ок, HTML не предназначен, но PDF - да. Можно организовать фоновый/по требованию пререндеринг HTML в PDF, например с помощью AJAX, и даже автоматом корректировать HTML по его результатам. Для 10х40 символов тормозить не будет. PDF библиотеки как правило поддерживают функции расчета длины строки заданным шрифтом и пр. сопутствующие вкусности. Несколько извратно конечно, но задаче соответствует.

Thirteensmay[досье] Ухохочетесь реализовывать;) Тем более как строчки-то подсчитывать? Отправлять через каждый введённый символ весь текст на сервер, там его рендерить в PDF (или не рендерить а каким-либо другим способом считать), каким-то образом понимать из этого сколько строк занял отрендерённый контент, отправлять результат обратно. Кхм. Не перебор ли? :) А при общей любви пользователя к wysiwyg`у прогнуть его на работу с таким интерфейсом будет, скажем так, сложно;)

Ну не ухохочетесь, а так, похохотать пару дней... ;) Этот вариант конечно получится с проверкой по требованию, зато четко. А если нужен полный реалтайм то можно извратиться так: подобрать размеры HTML элемента ввода под соответствующий абзац PDF, на среднестатистической фразе, т.е. приблизительно. Если шрифт не меняется то можно жестко, если меняется то придется сделать процедуру синхронизации (тотже AJAX). В любом случае думаю на статбазе из десятка предложений можно добиться точности +/- 1..2 символа от среднестатистического значения. Тут есть только один хреновый момент: если пользователь начинает извращаться с форматированием, например для установки большого межсловного интервала пробелами, или рисования псевдографикой, т.е. куча однотипных и значительно отличающихся от среднего значения длины символов подряд, то точность уйдет. Да в этом случае будет косяк, нельзя так форматировать, ведь мы же не поступаем так в Word, даже там с этим заморочки, да и задача у нас вроде как ввод простого абзаца текста без извращений, так что этот момент думаю можно опустить. Так вот, в результате получим набор строк "точно" влазящих в HTML, по длине, которые при рендеринге в PDF будут слегка отличатся. Исходя из некоторого опыта могу предположить что для "нормального" текста можно обеспечить точность в пару символов. Теперь самое главное, как же нам вписать эти стоки в PDF чтобы они там не отличались? - просто. PDF библиотеки поддерживают такую операцию как вписывание строки в заданную область (за счет матриц трансформации читай масштабирования), а т.к. исходные строки у нас мало отличались это масштабирование практически незаметно на глаз. Для истинных "любителей" можно предложить подход c изменением межсловного/межсимвольного интервала. Естественно это потребует доработки конвертера HTML->PDF, вместо одной стандартной функции вывода текста в PDF придется написать с килобайт масштабирующего кода. Все это не так страшно как кажется. Подумайте сами все что надо сделать это синхронизировать размеры контейнеров и применить масштабирование при выводе в PDF.

[досье] , а что конкретно Вам жаль?
Проблема далеко не тривиальна. Автор откровенно написал, что в тонкостях не разбирался. Но, если Вы обладаете достаточным уровнем знаний, готовы разобраться и потратить заявленные 2 дня, удачи в реализации.

мои знакомые изменили постановку задачи и существенно её упростили. фонт теперь будет постоянным и количество строк можно вычислять относительно высоты строки.
сделал следующее:

8 - это примерная высота строки в textarea. но как обрезать текст, если он не вводится с клавиатуры, а вставляется из буфера? пробовал сделать с onpaste тоже самое, что и с onkeydown, но не получается.
решение можно делать только для эксплорера, наборщики всё равно работают только с ним.

Введение и понимание сути проблемы

Зачем это нужно

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

Примечание 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), таблицам, линиям


.
Очистка форматирования, не подходящего под заданную таблицу стилей (полезно при копировании текста с документов 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 нужно, чтобы в документе был тег , иначе нечему будет присваивать innerHTML. Выдачу движком не пустого HTML, а редактируемого текста считаем нецелесообразным, т. к. это излишне усложняет движок без какой бы то ни было пользы. Текст мы будем получать из textarea, все равно необходимой для интерфейса редактора.

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

,

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

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

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

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

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

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

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

HTML

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

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

Сейчас мы не задумываемся над особой функциональностью. Можно сделать 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 (выделить все) - вы увидите, что выделение картинки будет выглядеть по-разному - в первом случае она выделена как картинка (объект), во втором - как часть текста.

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

Наш редактор должен уметь получать список выделенных узлов документа, при необходимости создавая новые (если выделена часть узла, к которому надо применить 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 // браузер, не поддерживающий работу с выделением }

Получаем список всех узлов с определенным именем тега, лежащих между началом и концом выделения
Когда мы применяем к блокам или инлайнам команду форматирования определенным тегом с определенным классом, у нас может получиться много узлов, поэтому нам интересны не только начальный и конечный. Банальный проход по nextSibling’ам не подходит, т.к. нам пожет понадобится в процессе обхода подниматься и опускаться по дереву узлов. Посему алгоритм такой — получив ближайшего общего родителя (root), из поддерева его потомков, ограниченного начальным и конечным узлами, выбираем все нужные нам узлы.

функция, скорее всего, нуждается в оптимизации, а то она через глобальную переменную написана…

var global_stage // некрасивая глобальная переменная // bounds - массив // tag_name - имя тега // остальные аргументы не указываем, используются для рекурсии function find_tags_in_subtree(bounds, tag_name, stage, second){ var root = bounds["root"] var start = bounds["start"] var end = bounds["end"] if(start == end) return if(!second) global_stage=stage if(global_stage == 2) return if(!global_stage) global_stage = 0 tag_name = tag_name.toLowerCase() var nodes= for(var node = root.firstChild; node; node = node.nextSibling){ if(node==start && global_stage==0){ global_stage = 1 } if(node.nodeName.toLowerCase() == tag_name && node.nodeName != "#text" || tag_name == ""){ if(global_stage == 1){ nodes.push(node) } } if(node==end && global_stage==1){ global_stage = 2 } nodes=nodes.concat(find_tags_in_subtree({root:node, start:start, end:end}, tag_name, global_stage, true)) } return nodes }

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

// Ближайший родитель с нужным тегом function closest_parent_by_tag_name(node, tag_name){ tag_name = tag_name.toLowerCase() var p = node do{ if(tag_name == "" || p.nodeName.toLowerCase() == tag_name) return p }while(p = p.parentNode) return node } Массив всех узлов с нужным тегом, попавших в выделение function get_selected_tags(editor_window, tag_name){ if(tag_name){ tag_name = tag_name.toLowerCase() }else{ tag_name = "" } var bounds = get_selection_bounds(editor_window) if(!bounds) return null bounds["start"] = closest_parent_by_tag_name(bounds["start"], tag_name) bounds["end"] = closest_parent_by_tag_name(bounds["end"], tag_name) return find_tags_in_subtree(bounds, tag_name) }

Форматирование блоков

В API есть команда formatBlock, ей на вход дается имя блочного тега и она оформляет текущее выделение этим тегом.
Например:
document.execCommand("formatBlock", false, "

").

// Оформляем выделение нужным блочным тегом с нужным классом function wysiwyg_format_block(iframe_id, tag_name, class_name){ var iframe = document.getElementById(iframe_id) var wysiwyg = iframe.contentWindow.document // Оформляем нужным блочным тегом wysiwyg.execCommand("formatblock", false, "<" + tag_name + ">") // Выбираем из выделения все теги нужного имени и ставим им класс var nodes = get_selected_tags(iframe.contentWindow, tag_name) for(var i = 0; i < nodes.length; i++){ if(class_name){ // Устанавливаем класс nodes[i].className = class_name }else{ // Убираем класс, если он нам не нужен nodes[i].removeAttribute("class") nodes[i].removeAttribute("className") } } iframe.focus() }

Форматирование слов (inline)

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

Перспективным решением выглядит использование команды ForeColor, которая вставляет . Сам по себе нам в нашем идеологически выдержанном редакторе не нужен абсолютно, но это позволит нам штатным образом создать строковые узлы, которые мы опять же сможем выбрать из выделения и поменять им tagName и className (и убрать атрибут color). Для надежности можно вставлять и потом искать какой-то конкретный цвет, подобранный таким образом, чтобы практически исключить ситуацию, когда он попадется нам в скопированном с другого документа тексте, например, #00ff01 (хотя его все равно бы уничтожил задуманный нами очиститель HTML).

// "Магический" неиспользуемый цвет var magic_unusual_color="#00f001" // Оформляем выделение нужным строковым (инлайновым) тегом с нужным классом function format_inline(iframe_id, tag_name, class_name){ var iframe = document.getElementById(iframe_id) var wysiwyg = iframe.contentWindow.document // Убираем все существующее форматирование wysiwyg.execCommand("RemoveFormat", false, true) // В MSIE после RemoveFormat остаются span-ы, удалим их тоже clean_nodes(get_selected_tags(iframe.contentWindow, "span")) // Если имя тега не указано (применяется, когда мы хотим просто убрать форматирование) if(tag_name!=""){ // Вставляем наш wysiwyg.execCommand("ForeColor", false, magic_unusual_color) // Заменяем узлы, образованные font"ами, на новые с нужным именем и классом var nodes=get_selected_tags(iframe.contentWindow, "font") var new_node for(var i=0;i= 0 ; i--){ if(!classname || nodes[i].className == class_name){ nodes[i].removeNode(false) } } }

Себе: закроссбраузерить clean_nodes

BUGS:
1: MSIE:пропадают граничные пробелы (попавшие в выделение и крайние в нем. По-видимому, из-за переприсвоения innerHTML)
2: иногда в MSIE при применении инлайн-форматирования на несколько абзацев сразу часть текста остается зеленой (magic_unusual_color). В мозиле, кстати, тоже иногда, но при других обстоятельствах… Может, выбирать все fontы из всего документа, а не только из выделения? // Круглов
Работа со списками
Имеется в виду работа с нумерованными и маркированными списками. На наше счастье в API уже почти есть (и даже больше чем нужно, но не будем забегать вперед).

Мы хотим преобразовывать абзацы в оба вида списков и обратно, а также управлять вложенностью списков (менять отступы).

Вот список имеющихся в designMode API команд:

InsertOrderedList - вставить

    InsertUnorderedList - вставить
      Indent - увеличить отступ (сделать подсписок) Outdent - уменьшить отступ (выйти из подсписка)



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: