Главная страница статей --> Хитрости при программировании php, заметки по базам данных

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

Источник: realcoding.net

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

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

var global_stage // некрасивая глобальная переменная
// bounds — массив [root, start, end]
// 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 [start]

   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, "<h1>").

Далее все просто — мы применим эту команду и потом выберем из выделения все теги нужного имени (tagName), которым и присвоим нужный className:

// Оформляем выделение нужным блочным тегом с нужным классом
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, которая вставляет <font color=...>. Сам по себе <font> нам в нашем идеологически выдержанном редакторе не нужен абсолютно, но это позволит нам штатным образом создать строковые узлы, которые мы опять же сможем выбрать из выделения и поменять им 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!=){
      
// Вставляем наш <font color>
      
wysiwyg.execCommand(ForeColor, false, magic_unusual_color)

      
// Заменяем узлы, образованные fontами, на новые с нужным именем и классом
      
var nodes=get_selected_tags(iframe.contentWindow, font)
      var
new_node
      
for(var i=0;i<nodes.length;i++){
         if(
nodes[i].getAttribute(color) != magic_unusual_color) continue
         
new_node = wysiwyg.createElement(tag_name)
         if(
class_name) new_node.className = class_name
         new_node
.innerHTML = nodes[i].innerHTML
         nodes
[i].parentNode.replaceChild(new_node, nodes[i])
      }
   }
   
iframe.focus()
}

// Чистка узлов (удаляем тег, оставляем содержимое)
// (Только для MSIE)
function clean_nodes(nodes, class_name){
   if(!
nodes) return
   var
l = nodes.length - 1
   
for(var i = l ; 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 — вставить <ol>
InsertUnorderedList — вставить <ul>
Indent — увеличить отступ (сделать подсписок)
Outdent — уменьшить отступ (выйти из подсписка)

Со сменой отступов есть небольшая проблемка — если мы увеличиваем отступ не у списка, вставляется тег <blockquote>. Он нам тут совершенно не нужен. Однако мы, вооружась недавно описанной функцией clean_nodes, его незамедлительно удалим.

// Работа со списками. Передаем на вход одну из команд:
// ul, ol, indent, outdent
function list(iframe_id, command){
   var
iframe = document.getElementById(iframe_id)
   var
wysiwyg = iframe.contentWindow.document
   
switch(command){
      case
ol:
         
wysiwyg.execCommand(InsertOrderedList)
         break
      case
ul:
         
wysiwyg.execCommand(InsertUnorderedList)
         break
      case
indent:
         
wysiwyg.execCommand(Indent)
         
// удаляем <BLOCKQUOTE>
         
clean_nodes(get_selected_tags(iframe.contentWindow, blockquote))
         break
      case
outdent:
         
wysiwyg.execCommand(Outdent)
         break

   }
   
iframe.focus()

Контакты
Редакция:
[0.002]