Pull to refresh

Google Chrome Extension: Печатаем статьи с habrahabr

Reading time8 min
Views14K
Как-то вечером, уходя с работы, наткнулся здесь на интересную статью. Так как я люблю читать печатный вариант, да и время было уже позднее — хотелось домой, но и хотелось прочесть — решил распечатать и почитать в дороге.

Ну и полез я печатать, браузер мне предложил распечатать более 35 страниц, но откуда там может быть 20 страниц? Дело в том что печать шла вместе со всеми элементами, что делает пост узким, соответственно длинным, та и еще и комментарии ту да же.

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

Так родилось расширение для браузера Google Chrome — HabraPrint.
Все что от вас требуется нажать на одну кнопку и распечатать.


Это мое первое расширения, как и первый пост, надеюсь меня исправят там где я ошибся.
И так что мы имеем внутри:

Создаем папку с названием нашего расширения, куда будем сохранять последующие файлы.
Далее первым делом создаем файл manifest:
Здесь указываем браузеру, что он и с какими разрешениями ставит.

{
  "name": "HabraPrint",
  "version": "0.1",
  
  "description": "Печать в один клик поста с сайта habrahabr.ru", 
  "icons": {
    "128": "img/icon_128.png",
    "64": "img/icon_64.png",
    "48": "img/icon_48.png",
    "32": "img/icon_32.png",
    "16": "img/icon_16.png"
  },
  
  "minimum_chrome_version":"6.0",
  
  "permissions": [ "tabs","http://habrahabr.ru/*", "https://habrahabr.ru/*"],
  "background_page": "background.html",
  "content_scripts": [
    {
      "js": [ "js/jquery-1.7.1.min.js","js/content.js" ],
      "css": ["css/content.css"],
      "run_at": "document_end",
      "matches": [ "http://habrahabr.ru/*", "https://habrahabr.ru/*" ]
    }
  ],
"page_action": {
    "default_icon": "img/icon_19.png",
    "default_title": "HabrPrint"
  },
  "options_page": "options.html"
}


Указываем:
name — Имя расширения
version — текущая версия
description — описания вашего расширения
icons — указываем пути к иконкам расширения <key/размер>:<value/путь к изображению>
icon_128
icon_64
icon_48
icon_32
icon_16


minimum_chrome_version-указываем минимальную версию браузера
permissions — здесь указываем к чему вам потребуется доступ
background_page — указываем путь к странице которая будет выполнятся в фоне
content_scripts — указываем пути к файлам которые будут внедрены в страницу
page_action — данное свойство указывает на то что кнопка расширения будет размещена в адресной строке браузера, указываем путь к иконке и название
options_page — путь к странице настроек

с подробным описанием файла можно ознакомится в документации

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

Теперь будем двигаться по списку.
Я разнес файлы относительно их типа по папкам для удобства использования.
Следующим файлом создаем background_page:
<!DOCTYPE html>
<html>
  <head>
    <script src="js/background.js"></script>
  </head>
</html>


Как видите здесь имеется только каркас страницы, и подключение javascript скрипта который отвечает за действия в фоновом режиме

background.js
	chrome.tabs.onCreated.addListener(function(tab){
		urlDetected(tab.id, null, tab);
	});
	chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){
		if(changeInfo.status=='complete'){
			urlDetected(tabId, changeInfo, tab);
		}
	});

	function urlDetected(tabId, changeInfo, tab){
		chrome.tabs.getSelected(null,function(tab) {
			var re=/.+habrahabr.+\/(\d+)\//;
			if(re.test(tab.url)){
				chrome.pageAction.show(tabId);
			}else{
				chrome.pageAction.hide(tabId);
			}
		}); 
	}

	chrome.pageAction.onClicked.addListener(function(tabId) {
		//узнаем какие настройки и выполняем действия
		if(!localStorage["radio"]||localStorage["radio"]=='popup'){
			PrintIt();
		}else if(localStorage["radio"]=='same'){
			chrome.tabs.getSelected(null, function(tab) {
				chrome.tabs.sendRequest(tab.id, {
					type:'print-same'
				});
			});
		}
	});


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

в всплывающем окне

Так как вызов всплывающего окна мне разрешил выполнить только данный файл(background_page), здесь разместилась функция отвечающая за показ того самого окна.

 function PrintIt(){ 
    if(wnd){
	wnd.close();
    }
    stext='';
    chrome.tabs.getSelected(null, function(tab) {
	chrome.tabs.sendRequest(tab.id, {
		type:'returnHtml'
	}, function(response) {
		stext=response.html;
		wnd=window.open("", "habrPrint", 'statusbar=no,toolbar=no,scrollbars=yes,resizable=yes'');
		wnd.document.write("<!DOCTYPE html>\
			<html lang='ru'>\
			<head>\
			<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>\
			<meta content='ru' name='language'>\
			<title>"+response.title+"</title>\
			<link href=\"/css/print.css\"rel=\"stylesheet\"type=\"text/css\" media=\"all\"/></style>\
			</head>\
			<body onclick=\"window.close()\">\
			<div class='post'>");
		wnd.document.write(stext);
		wnd.document.write("</div><body></html>");
		wnd.document.close();
		setTimeout(function(){
			wnd.print();
			wnd.close();
		}, 100)
			
			
	});
    });
	
}


Тут я укоротил немного функцию бы было нагляднее. Как видите используется обычный window.open и в него передается html со страницы с текстом поста. Для получения текста отправляем запрос, с названием действия, в content script, в ответ получаем innerHTML поста и записываем в окно wnd.document.write().

Далее напишем наш content script:
var getElementsByClassName = function(getClass){
	if(document.querySelectorAll) {
		return document.querySelectorAll("." + getClass);
	}
	else if(document.getElementsByClassName) {
		return document.getElementsByClassName(getClass);
	}
	else {
		var list = document.getElementsByTagName('*'), i = list.length,
		classArray = getClass.split(/\s+/), result = [];
		while(i--) {
			if(list[i].className.search('\\b' + classArray + '\\b') != -1) {
				result.push(list[i]);
			}
		}
		return result;
	}
};

function pageCleaner(){
	$('body *').removeClass('habrNoPrint').removeClass('habrPrint');
}
function printSame(){
	$('body *').addClass('habrNoPrint');
	$('#layout, .content_left, .company_post, .post, .post *').removeClass('habrNoPrint');
	$('.content_left').addClass('habrPrint');
	window.print();
	window.setTimeout(pageCleaner, 0);
}

chrome.extension.onRequest.addListener(function(request, sender, sendResponse) {
  if(request.type == 'print-same'){
	printSame();
  }
  if(request.type == 'returnHtml'){
	var elem=getElementsByClassName('post')[0];
	var title=document.getElementsByTagName('title')[0];
	sendResponse({'html':elem.innerHTML,'title':title.innerHTML});
  }
});


Здесь размещаем слушатель запросов, и при получении его выполняем действие указанное в параметрах.
Здесь всего два действия либо отдать html поста, предыдущему скрипту, либо выполнить функции, которая отвечает за выполнение печати. В данной функции использую стили для печати, то есть указываю стилями что выводить на печать а что скрыть.

И в конце напишем страницу настроек.
В options.html делаем каркас страницы

<!DOCTYPE html>
<html lang='ru' xml:lang='ru' xmlns='http://www.w3.org/1999/xhtml'>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
<meta content='ru' name='language'>
<style>@import "css/options.css";</style> 
<script src="js/options.js"></script>
</head>
<body>
        <header>
            <h3>Настройки:</h3><span id="options_callback"></span>
        </header>
	<div id='habrPrint_options'>
                <form name="habr_options_form">
		<div class='options_form'>
			<div>
			  <input id="radio_popup" type="radio" name="window" value='popup' checked="checked"/>
			  <label id="append-label">В сплывающем окне</label>
			  <p>Данный режим открывает всплывающее окно с печатью страницы</p>
			</div>
			<div>
			  <input id="radio_same" type="radio" name="window" value='same' />
			  <label id="append-label">В том же окне</label>
			  <p>Данный режим работает с ограничение, браузер ограничивает количество вызовов, не более одного вызова раза в 5-ть секунд</p>
			</div>
		</div>
                </form>
                <div class="button">
                    <div class="button_blue">
                        <button id="save">Сохранить</button>
                    </div>
                </div>
	</div>
</body>
</html>


Здесь всего пару радио кнопок которые устанавливают режим печати и кнопка сохранения, стили указаны в css/options.css, здесь описывать не стану.

Итого вышла вот такая страница:
options

В файле options.js пишем javascript который будет отвечать за сохранение настроек.
	function getRadioGroupValue(radioGroupObj)
	{
	  for (var i=0; i < radioGroupObj.length; i++)
	    if (radioGroupObj[i].checked) return radioGroupObj[i].value;

	  return null;
	}
	function readProperty(property, defValue)
	{
	  if(localStorage[property] == null)
	  {
	    return defValue;
	  }
	  return localStorage[property];
	}
	window.addEventListener("load", function(){
		
	  chrome.tabs.getSelected(null, function(tab) {
	    var save = document.getElementById("save");
			if(localStorage["radio"]){
				document.getElementById("radio_"+localStorage["radio"]).checked =readProperty("radio", false);
			}
	        save.addEventListener("click", function(){
				var radio_value = getRadioGroupValue(document.habr_options_form.window);
				localStorage["radio"] = radio_value;
				if(localStorage["radio"]){
					var sum=document.getElementById('options_callback');
	                sum.innerHTML='Настройки сохранены'
				}
	        });
	  });
	});


Вешаем обработчик на клик кнопки сохранения и по нему заносим значение в объект localStorage.
localStorage является ассоциативным массивом хранящий пары «название», «значение». Для сохранения значения достаточно написать:

	localStorage["radio"] = radio_value;


Вот, в принципе, и все осталось загрузить наше расширение в браузер.
Переходим на вкладку «Управление расширениями» (Инструменты->Расширения), включаем «Режим разработчика», и загружаем распакованное расширение.

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

Проблемы:
У себя заметил что при первой загрузке расширения, появляется ошибка что то вроде «Вам не разрешено использовать функции для tabs проверьте манифест», с чем это связанно я так и не разобрался, если что объяснит буду признателен.

Источники:
Google Chrome Extension FAQ
Создание расширения для Google Chrome

Расширение:
Скачать

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

UPD: по просьбе bo883, добавил подсветку синтаксиса, и добавил подложку под код (пока что работает только в «В всплывающем окне», в скором времени добавлю и при печати в том же окне). При реализации столкнулся с проблемой что фон, добавленный через css, не печатает Chrome, но зато прекрасно печатает картинки (<img ...>), основываясь на этом добавляем картинку к блоку с кодом устанавливаем ей ширину и высоту равную размерам блока кода, тем самим растягивая ее на весь блок, и меняем им z-index, что бы подложить под код картинки, в итоге при печати у нас область кода имеет подложку. (расширение обновил)
Tags:
Hubs:
Total votes 70: ↑63 and ↓7+56
Comments50

Articles