При написании одного парсера передо мной встала задача предоставить пользователю возможность самому выбирать ту часть страницы которую он хотел бы спрарсить. Сделать я это решил на базе WYSWYG редактора JWYSWYG, он очень легкий, кросбраузерный, написан на моем любимом JQuery и легко поддается модификации. Кроме того я встроил в него функцию автоперевода выделенного текста при помощи Google Tranlate Ajax API. От слов к делу. Оказалось что задача не настолько тривиальна как кажется с первого взгляда. Получить со страницы надо было именно кусок выделенного HTML а не тот текст, который собственно попадет в буфер обмена если нажать кнопку копирования.

UPD. Судя по поисковым запросам, много людей ищут не теорию, а готовое решение на jQuery. Дабы не смущать Вас лишней информацией можете сразу пройти на страницу с готовым плагином на JQuery и пример его использования

Первое что мне удалось сделать руководствуясь крупицами нагугленной информации это 

var ie = false;
	if ( window.getSelection ) { 
		var selectedText = window.getSelection(); 
	} else if ( document.getSelection ) { 
		var selectedText = document.getSelection(); 
	} else if ( document.selection ) { 
		ie = true;
		var selectedText = document.selection.createRange(); 
	} 
	if(!ie){
		alert(selectedText)//  просто выведет выделенный текст без тегов 
		var theParent = selectedText.getRangeAt(0).cloneContents(); 
		alert(toHTML(theParent)) // собственно выделенный кусок кода 
	}else{
		alert(selectedText.text) // текст
		alert(selectedText.htmlText) // выделенный кусок кода
	}

 Данный код можно протестировать на примере номер 1

  Как мы видим код c намеком на кроссбраузерность, проверяются все возможные варианты, остановимся по подробнее на каждой строчке. Метод window.getSelection поддерживается большинством современных браузеров, кроме  всеми любимого IE. Метод document.getSelection является устаревшей версией window.getSelection и проверяется для устаревших  браузеров. Ну и наконец  последняя ветка условия исключительно для браузеров IE.

Собственно объект который получается в результате такого выделения имеет тип selection.  Он представляет собой выделенный текст на странице, возможно, охватывающих несколько элементов. Несколько выделений возможны к примеру при нажатии клавиши ctrl.

Чтобы получить содержимое одного из этих выделений применяется метод  sel.getRangeAt(index) Где index это порядковый номер выделений на странице. Метод возвращает числовой диапазон - начало и конец выделения. Возвращаемый этим методом объект имеет тип range

Как видно из документации range имеет поля в которых записано начало выделения и поле для записи конца выделения. А также тип имеет несколько методов для получения выделенной части страницы.

cloneContents  - Возвращает фрагмент документа(documentFragment) копируя его, т.е. при этом все выделенные узлы дерева DOM остаются на своем месте.
deleteContents - удаляет контент из объекта range. Т.е. выделенный html попросту удалится
extractContents - помещает выделенный дерево узлов DOM в специальный объект documentFragment, при этом из оригинального дерева DOM элементов страницы эти узлы удаляются. Т.е. это эквивалентно последовательному вызову методов cloneContents  и deleteContents соответственно
insertNode - вставляет новые элементы в начало выделенной области
surroundContents - оборачивает выделенные узлы в новый элемент(к примеру в <div></div>)

В примере мы использовали cloneContents, но вам ничего не мешает использовать и другие методы.

Немного подробнее остановимся на объекте documentFragment. Это очень интересный объект так как он может содержать в объектной форме множество узлов DOM. Поясню на примере:

<div id="spabox">
 <span>a</span>
 <span>b</span>
 <span>c</span>
</div>

Тут мы видим один узел DIV с тремя вложенными в него узлами SPAN.

Если получить доступ к элементу DIV при помощи

var spanbox = document.getElementById('spanbox')
alert(typeof spanbox.childNodes) // вернет Array

то метод возвращающий все дочерние объекты DIV chilNodes будет иметь типа Array, что неприемлемо если мы к примеру хотим групповой операцией вставить их в другое место документа. На помощь приходит documentFragment . На просторах интернета мною даже была найдена функция которая конвертирует HTML текст в DOM(как оказалось довольно нетривиальная задача в js программировании)

function toDOM(HTMLstring){
	var d = document.createElement('div');
	d.innerHTML = HTMLstring;
	var docFrag = document.createDocumentFragment();
	while (d.firstChild) {
		docFrag.appendChild(d.firstChild)
	};
	return docFrag;
}

Функция компактно упаковывает весь HTML текст в объект типа DocumentFragment. Кстати метод range.insertNode как и многие другие методы вставки нового нода в дерево DOM прекрасно переваривают DocumentFragment.

Признаться к своему стыду я не обнаружил метода получения HTML из объекта DocumentFragment. Может быть он и есть, но я его не нашел. поэтому пришлось написать обратную toDom функцию. Назовем ее toHTML

 

function toHTML(docFragment){
	var d = document.createElement('div');
	d.appendChild(docFragment);
	return d.innerHTML;
}

Тут все просто. Я же говорил что DocumentFragment прекрасно едят все методы которые работают с обычными элементами дерева DOM

Постепенно мы подошли ко второй части нашей задачи - это перевод.

API Google предельно просто и понятно, сильно на нем заострять внимание не будем лишь для справки покажу как как это делается. Для того чтобы нам было доступно API переводчика нам нужно в код страницы включить вот следующий код:

<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script>
google.load("language", "1");
</script>

Вот и все переводчик готов к работе. Далее собственно сам перевод

 

google.language.translate('hello','en','ru',function (data){
      alert(data.translation) // выводит привет
})

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

Ну и последний шаг нашей "кампании" это замена. Сразу приведу код

 

if(!ie){
 var rang= selectedText.getRangeAt(0);
 rang.deleteContents()
 if($.browser.mozilla){
      var rangeObj = document.createRange();
      var documentFragment = rangeObj.createContextualFragment(data.translation);
 }else{
      var documentFragment = toDOM(data.translation)
 }
 rang.collapse(false)
 rang.insertNode(documentFragment);
}else{
 selectedText.pasteHTML(data.translation)
}

Этот код можно протестировать на примере номер 2

Для Мозилы я использовал метод createContextualFragment у объекта типа range. Данный метод есть и у других представителей рода "Браузерных", но как-то не очень хорошо он у них работает . Метод делает то для чего была написана функция toDOM - она кстати работает и для Мозилы тоже, так что createContextualFragment приведен просто для общего развития ;),

Метод rang.collapse(false), можно опустить если вы хотите просто заменить текст,но я использовал ее не просто так. Если вы к примеру захотите сделать чтобы переведенный текст не заменял выделенный, то удалите вызов метода - rang.deleteContents() и в зависимости от того какой параметр подается в rang.collapse true или false вставка произведется в конец или в начало выделения соответственно. 

Чтобы материал легче усвоился посмотрите готовые примеры

Пример 1 - просто получаем выделенный текст и html со страницы
Пример 2 - автоматически переводим выделенный на странице текст с помощью Google Translate AJAX API
Пример 3 - тоже самое что и пример 2 только работа ведется с фреймом, это актуально для WYSIWYG редакторов, и отличается рядом нюансов

пример 4  - Все то же самое? но с использованием написанного мной плагина на jQuery 
 

Рассказать друзьям

Добавить комментарий


Защитный код
Обновить

Комментарии   

+1
Посетитель
# Посетитель 07.01.2011 19:22
Респектище автору!
-1
Илья
# Илья 16.02.2011 18:06
Уважаемый автор! Внимательно почитал Вашу статью - напал на нее, который день гугля инет в поисках решения своей проблемки. Может, Вы сможете помочь - в целом то у нас общие задачки прослеживаются. хттп://test.standarta.net - здесь я все выложил - посмотрите плиз, если время будет

спасибо
+1
Leroy
# Leroy 17.02.2011 04:06
я бы попробовал сделать так
node = window.getSelection().anchorNode
alert(toHTML(node.parentNode))
0
Darya
# Darya 02.05.2012 07:19
Спасибо!!
0
Rookie
# Rookie 12.06.2012 18:13
Как разбить последний кусок кода на отдельное вырезание и вставку?
0
Attos
# Attos 04.12.2012 18:42
Огромный респект! оч понадобилось)
0
Bill
# Bill 30.12.2012 00:45
Не работает в IE7-IE8, не понимает textarea. А жвль.
0
Алексей
# Алексей 14.06.2013 18:03
Отличная статья, спасибо автору! У меня есть вопрос, возможно ли сохранить модифицированную страницу? Т.е. поменяли текст на "Привет", сохранили страницу с изменениями. Нужно чтобы пользователь мог открыть страницу (html документ) на своем компьютете, внес изменения, описанные выше, сохранил страницу уже с ними. Это вообще возможно?
0
Leroy
# Leroy 14.06.2013 19:29
почему нет? берем исходный код страницы document.body.innetHtml или любого элмента, и сохраняем по ajax
0
Жизнь прекрасна и удивительна а автор блольшой шутник
# Жизнь прекрасна и удивительна а автор блольшой шутник 23.11.2013 04:30
Спасибо автору за интересные статьи
0
Дмитрий1
# Дмитрий1 11.05.2016 17:07
Примеры 2, 3 - не работают, однако. Текст не переводится, а лишь удаляется со страницы.
0
Leroy
# Leroy 12.05.2016 11:38
Да, хак с гугловским переводчиком давно сдох. Юзайте яндекс или бинг, там еще бесплатно
0
Дмитрий1
# Дмитрий1 12.05.2016 20:27
И еще хотел спросить. Когда приведенные в примере 1 строчки заключить в теги абзацев. Если частично выделять текст в пределах не одного, а нескольких абзацев - получается, что в итоге в код добавляются лишние р и /p, т.е. после вывода выделенного фрагмента создаются два новых абзаца (перед началом и после конца фрагмента). В итоге - выделенная часть отображается на странице в новом абзаце (т.е. с новой строки). Есть ли возможность как-то исправить такую ситуацию?