...

суббота, 26 октября 2013 г.

[recovery mode] История одного Google Chrome расширения

В один прекрасный день, за пару часов до конца работы, мне приходит задача: «Нужно написать приложение для браузера, которое должно по клику пользователя отправлять данные со страницы на сайт клиента. Что за приложение и какой браузер — полностью на ваш выбор...».

Немного поразмыслив я пришел к варианту google chrome extension:



  • Crome использует Chromium движок, который является форком WebKit (а это Safari), так же не забываем Blink (а это уже новая (хотя я все еще использую старую с bookmarks'ами) Opera). Таким образом, написав расширения для chrome, мы с минимальными переделками (а то и без них) сможем его портировать на еще 2 браузера

  • Нет опыта работы с API Google Chrome

  • Google все-таки компания добра :)


Когда мысли немного улеглись, первое что я сделал — это ввел в поиске харба "расширение Google Chrome". Увидев обширный вариант статей по данной теме, я со спокойной душой ушел домой полностью уверенный в том, что завтра с утра прочитав их, к концу рабочего дня дело будет 'в шляпе' (как же я тогда ошибался). Прочитав парочку их них я имел общее представление о том как это работает, но этого оказалось мало для воплащения моих идей. Что ж, приступим…



Открываем google chrome, вводим chrome://extensions, ставим галочку Developer mode, нажимаем кнопку Load unpacked extension, выбрав папку нажимаем Ok.



В начале было слово манифест. Ниже можете увидеть содержимое этого файла (manifest.json — это обязательное название файла манифеста)


manifest.json


{
"manifest_version": 2,
"name": "My application", // тут надеюсь все понятно
"version": "0.9",

"icons": {
"16": "./16x16.png",
"32": "./32x32.png",
"48": "./48x48.png",
"128": "./128x128.png"
},

"permissions": [
"tabs",
"http://*/*",
"https://*/*"
],

"background" : {
"page": "background.html"
},

"content_scripts":[{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"script_in_content.js"
]
}],

"browser_action": {
"default_title": "Application",
"default_icon" : "./16x16.png"
// "default_popup": "login.html" // это имя html-страницы расширения, которая будет всплывать при нажатии на иконку, можно с помощью JS устанавливать различные html страницы
}
}







manifest_version — на данный момент значение 2 обязательное.

version — версия вашего расширения, может содержать только цифры и `.` (те. '2.1.12', '0.59' и тд)

icons — это список всех иконок которые будут отображатся в браузере в различных местах (16 — в адресной строке, 48 — в списке всех расширений и тд.)

permissions — здесь перечислен массив с разрешениями, мне нужно было только tabs.http и https нужен для ajax обмена с любыми сайтами, а также для того что-бы script_in_content.js мог обмениватся данными с фоновой страницей — background.html.

background — это имя фоновой страницы. Фоновая страницы — важный элемент, хотя для некоторых приложений он и не обязателен. Зачем она нужна немного по-позже.

content_scripts — здесь говорится что файл script_in_content.js, будет автоматически загружатся для страницы открытой во вкладке. Страница должны быть открыта с сайтов http://*/* те, всех сайтов с http, но не https, хотя можно было бы указать и их.

browser_action — существует 2 варанта отображения иконки расширения: browser_action и page_action

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


browser_action наоборот не считаются индивидуальными и отображаются не в адресной строке, а в панеле для расширений. Даную иконку никак нельзя скрыть на JS (но можно заблокировать), она отображается постоянно. У browser_action есть одно преимущество по сравнению с page_action, поверх иконки browser_action можно написать пару красивых символов (у меня влазит только 4).



Я выбрал browser_action, как мне необходимо работать не с одним сайтом, а с несколькими. И да, нанесение красивых символов на иконку.

Вот что Google говорит по этому поводу:



Do use page actions for features that make sense for only a few pages.

Don't use page actions for features that make sense for most pages. Use browser actions instead.



И так, что будет делать наше приложение; скажу сразу, приложение, которое будет далее описано, — это малая часть того что было сделано для клиента. Когда менеджер заходит на сайт hantim.ru для просмотра информации о контракте/вакансии, приложение парсит html код страницы и находит информацию (вакансия, город и тд). При клике на иконку расширения — отображается форма логина, куда менеджер вводит свои данные, а потом может добавить выбранную вакансию/контракт в свой профиль на корпоративном сайте.


Теперь о том как все это работает. Google предоставляет нам такую картину:


1) Inspected window — это то что мы открыли во вкладке, content scripts — это наш script_in_content.js, он имеет полный доступ к DOM страницы.

2) Background page — это сердце приложения, в нашем случае — это background.html.

3) DevTools page — это то, что будет отображатся при клике на иконку расширения (login.html или find.html в нашем случае).


Единственное что меня смущает в данной картинке так это связь DevTools page и Inspected window. Я не нашел решения, что бы передать данные из одной области в другую. Но если выставить Background page как посредника, и через него передавать данный, то все заработает.


И так, настало время кода. Начнем с невидимой стороны.


background.html


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="lib.js"></script>
<script type="text/javascript" src="bg.js"></script>
</head>
<body></body>
</html>







Надеюсь с этим вопросов не должно возникнуть. Одно замечание: «background.html — загружается только один раз за все время работы браузера, а имеено при его запуске». Здесь вы можете увидеть что мы загружаем 2 js файла (lib.js — набор функций, bg.js — 'голова' приложения).
bg.js


/**
* OnLoad function
*
* @return void
*/
window.onload = function(){

// tmp storage
window.bg = new bgObj();

// some variables !!! important
window.bg.api_site_host = 'http://katran.by';

// get all graber hosts: !!!once!!!
new Ajax({
url: window.bg.api_site_host+'/regexp.php',
response: 'json',
async: false,
onComplete: function(data){
if(data && data.status && (data.status === 'ok'))
window.bg.grabber_hosts = data.data;
}
}).send();

// set handler to tabs
chrome.tabs.onActivated.addListener(function(info) {
window.bg.onActivated(info);
});

// set handler to tabs: need for seng objects
chrome.extension.onConnect.addListener(function(port){
port.onMessage.addListener(factory);
});

// set handler to extention on icon click
chrome.browserAction.onClicked.addListener(function(tab) {
window.bg.onClicked(tab);
});

// set handler to tabs
chrome.tabs.onUpdated.addListener(function(id, info, tab) {
// if tab load
if (info && info.status && (info.status.toLowerCase() === 'complete')){
// if user open empty tab or ftp protocol and etc.
if(!id || !tab || !tab.url || (tab.url.indexOf('http:') == -1))
return 0;

// save tab info if need
window.bg.push(tab);

// connect with new tab, and save object
var port = chrome.tabs.connect(id);
window.bg.tabs[id].port_info = port;

// run function in popup.html
chrome.tabs.executeScript(id, {code:"initialization()"});

// send id, hosts and others information into popup.js
window.bg.tabs[id].port_info.postMessage({method:'setTabId', data:id});
window.bg.tabs[id].port_info.postMessage({method:'setHosts', data:window.bg.grabber_hosts});
window.bg.tabs[id].port_info.postMessage({method:'run'});

// if user is logged into application set find.html popup
if(window.bg.user.id)
chrome.browserAction.setPopup({popup: "find.html"});
};
});

window.bg.onAppReady();
};


/**
* Functino will be called when popup.js send some data by port interface
*
* @return void
*/
function factory(obj){
if(obj && obj.method){
if(obj.data)
window.bg[obj.method](obj.data);
else
window.bg[obj.method]();
}
}


/**
* Popup object
*
* @version 2013-10-11
* @return Object
*/
window.bgObj = function(){
};


/**
* Pablic methods
*/
window.bgObj.prototype = {

/**
* some internal params
*/
tabs: {},
user: {},
popup_dom: {},
active_tab: {},
grabber_hosts: {},
done_urls: [],

/**
* init() function
*/
onAppReady: function()
{
// if user not logged into application set login.html popup
chrome.browserAction.setPopup({popup: "login.html"});
},

/**
* Function add tab into $tabs object, if need
*/
push: function(tab)
{
if(tab.id && (tab.id != 0)){
if(!this.tabs[tab.id])
this.tabs[tab.id] = {tab_obj:tab};
}
},

/**
* Function will be called from popup.js
*/
mustParsed: function(data)
{
if(this.tabs[data.tab_id]){
var id = data.tab_id;
this.tabs[id].must_parsed = data.find;

// run parser in popup.js, if need
if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true))
this.tabs[id].port_info.postMessage({method:'parsePage'});
}
},

/**
* Function will be called from popup.js
*/
matchesCount: function(data)
{
if(data.tab_id && this.tabs[data.tab_id]){
var id = data.tab_id;
this.tabs[id].matches = data.matches;
this.tabs[id].matches_count = this.tabs[id].matches.length+'';

if(this.tabs[id].matches_count && this.tabs[id].matches_count != '0'){
chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count});
return 0;
}
}

// show default text
chrome.browserAction.setBadgeText({text:''});
},

/**
* Function will be called when user change active tab
*/
onActivated: function(info)
{
// set active tab
this.active_tab = info;

var data = {};
data.matches = [];

if(info.tabId){
data.tab_id = info.tabId;
if(!this.tabs[data.tab_id])
this.tabs[data.tab_id] = {};
if(!this.tabs[data.tab_id].matches)
this.tabs[data.tab_id].matches = [];

data.matches = this.tabs[data.tab_id].matches;
}

// set actual count of matches for current tab
this.matchesCount(data);

// if user is logged into application set find.html popup
if(this.user.id)
chrome.browserAction.setPopup({popup: "find.html"});
},

/**
* Function will be called when user click on extension icon
*/
onClicked: function(tab)
{
alert('Произошла ошибка. Обратитесь к разработчикам данного приложения.');
return 0;
},

/**
* Function will be called from login.js
*/
loginUser: function(user_data)
{
var self = this;
var json_data = false;

// get all graber hosts: !!!once!!!
new Ajax({
url: window.bg.api_site_host+'/login.php?user='+encodeURIComponent(JSON.stringify(user_data)),
method: 'post',
response: 'json',
async: false,
onComplete: function(data){
if(data && data.status){
// if login - ok
if(data.status === 'ok')
self.user = data.data;

json_data = data;
}
}
}).send();

// return value for login.js
return json_data;
},

/**
* Function will be called from login.js and others places
*/
setPopup: function(popup_file)
{
chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file});
},

/**
* Function will be called from find.js and others places
*/
getMatches: function()
{
// init if need
if(!this.tabs[this.active_tab.tabId])
this.tabs[this.active_tab.tabId] = {};
if(!this.tabs[this.active_tab.tabId].matches)
this.tabs[this.active_tab.tabId].matches = [];

// if user alredy send this url - remove
for(var i = 0, cnt = this.tabs[this.active_tab.tabId].matches.length; i < cnt; i++){
for(var j = 0, len = this.done_urls.length; j < len; j++){
if(this.tabs[this.active_tab.tabId].matches[i].url === this.done_urls[j]){
this.tabs[this.active_tab.tabId].matches[i].url = '';
break;
}
}
}

return this.tabs[this.active_tab.tabId].matches;
},

/**
* Function will be called from find.js and others places
*/
addUrlToGrabber: function(url)
{
// if $url == '' - already used
if(json_data.status && (json_data.status === 'ok')){
var matches = this.tabs[this.active_tab.tabId].matches;
for(var i = 0, cnt = matches.length; i < cnt; i++){
if(matches[i].url && (matches[i].url === url))
matches[i].url = '';
this.done_urls.push(url);
}
}

// return value for login.js
return json_data;
},


/**
* Empty method
*/
empty: function()
{
}
}







Первым делом мы дожидаемся window.onload, потом посылаем запрос на katran.by (получим json данные, с какого сайта и каким RegExp'пом мы дастанем необходимые данные), потом вешаем handler'ы на вкладки браузера (для этого мы и указали в манифесте permissions ~ tabs).

chrome.tabs.onActivated.addListener(function(info) {
window.bg.onActivated(info);
});


onActivated — происходит тогда, когда пользователь перешел на новую вкладку (по клику или по alt+tab).



chrome.tabs.onUpdated.addListener(function(id, info, tab) {
.....
});


onUpdated — происходит тогда, когда страница полностью (загрузился не только DOM, а и все картинки) загрузилась во вкладке.



chrome.browserAction.onClicked.addListener(function(tab) {
window.bg.onClicked(tab);
});


onClicked — происходит тогда, когда пользователь кликает на иконке приложения. Небольшое замечание, если во время клика default_popup установлено, то обработчик onClicked — не запустится. default_popup это html страница которая будет отображаеться после нажатия на иконку расширения. default_popup можно выставить в манифесте, а так же с помощью chrome.browserAction.setPopup({popup: «find.html»}); или chrome.pageAction.setPopup({popup: «find.html»});



chrome.extension.onConnect.addListener(function(port){
port.onMessage.addListener(factory);
});


Эта темная магия конструкция, нужна для приема данных, посланных от script_in_content.js с помощью port.

Обработкой данных занимается factory(obj)



function factory(obj){
if(obj && obj.method){
if(obj.data)
window.bg[obj.method](obj.data);
else
window.bg[obj.method]();
}
}


Что же происходит когда пользователь загружает вкладку, а происходит следующее:



  • Вызывается handler onUpdated

  • if (info && info.status && (info.status.toLowerCase() === 'complete')) если все загружено — продолжаем разбор полетов.

  • if(!id || !tab || !tab.url || (tab.url.indexOf('http:') == -1)) если пользователь открыл не web-сайт (проверку на https — забыл, только сейчас заметил :) ), а к примеру вкладку настройки или ftp и тд., то ничего не делаем

  • window.bg.push(tab); — созраняем информацию об текущей вкладке

  • chrome.tabs.executeScript(id, {code:"initialization()"}); — сейчас мы приказываем script_in_content.js выполнить функцию initialization()

  • window.bg.tabs[id].port_info.postMessage({method:'setTabId', data:id}) — мы посылаем данные в script_in_content.js

  • chrome.browserAction.setPopup({popup: "find.html"}); — устанавливаем popup страницу, если пользователь авторизовался ранее


Есть 2 способа передать данные от background.html к script_in_content.js:



  1. сhrome.tabs.executeScript(integer tabId, InjectDetails details, function callback) — одно но, таким спосабом можно передавать данные только в виде строки (не объект, не массив)

  2. сhrome.tabs.sendMessage(integer tabId, any message, function responseCallback) — так можно передавать что угодно, правдо потребуются дополнитеные настройки


И так, мы послали данные в script_in_content.js, значит настало время рассмотреть его код.


script_in_content.js


// set handler to tabs: need for seng objects to backgroung.js
chrome.extension.onConnect.addListener(function(port){
port.onMessage.addListener(factory);
});


/**
* Function remove spaces in begin and end of string
*
* @version 2012-11-05
* @param string str
* @return string
*/
function trim(str)
{
return String(str).replace(/^\s+|\s+$/g, '');
}


/**
* Functino will be called from background.js
*
* @return void
*/
function initialization(){
window.popup = new popupObj();
}


/**
* Functino will be called when background.js send some data by port interface
*
* @return void
*/
function factory(obj){
if(obj && obj.method){
if(obj.data)
window.popup[obj.method](obj.data);
else
window.popup[obj.method]();
}
}


/**
* Popup object
*
* @version 2013-10-11
* @return Object
*/
window.popupObj = function(){
};


/**
* Pablic methods
*/
window.popupObj.prototype = {

/**
* some internal params
*/
available_hosts: [],
total_host: null,
matches: [],
tab_id: null,
port: null,
cars: [],

/**
* Function will be called from bg.js
*/
setHosts: function(hosts)
{
this.available_hosts = hosts;
},

/**
* Function will be called from bg.js
*/
setTabId: function(id)
{
this.tab_id = id;
},

/**
* Function check total host
*/
run: function()
{
// get total host
if(document.location.host && (document.location.host != ''))
this.total_host = document.location.host;
else if(document.location.hostname && (document.location.hostname != ''))
this.total_host = document.location.hostname;

if(!this.total_host || (this.total_host === ''))
return 0;

var find = false;
// if total host in array $available_hosts - parse page for finde cars
for (host in this.available_hosts) {
if(this.total_host.indexOf(host) != -1){
this.total_host = host;
find = true;
break;
}
};

// create connection to backgroung.html and send request
this.port = chrome.extension.connect();
this.port.postMessage({method:'mustParsed', data:{tab_id:this.tab_id, find:find}});
},

/**
* Function will be called from bg.js
* Parse page
*/
parsePage: function()
{
// reset variable before parse
this.matches = [];

if(!this.available_hosts[this.total_host])
return 0;

var html = window.document.body.innerHTML;
var reg_exp = this.available_hosts[this.total_host];
var matches = {};
var match = [];
var find = false;
for(var i = 0, len = reg_exp.length; i < len; i++) {
var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag);
match = exp.exec(html);

if(match && match.length && reg_exp[i].index){
matches[reg_exp[i].field] = trim(match[reg_exp[i].index]);
find = true;
}
else if(match && match.length){
matches[reg_exp[i].field] = match;
find = true;
}
}

// this url will be send to site
if(find === true){
matches.url = document.location.href;
this.matches.push(matches);
}

// send count of matches
this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}});
}
}







Первое что бросается в глаза — это прием данных от background.html, как можете заметить он такой же как и в bg.js:

chrome.extension.onConnect.addListener(function(port){
port.onMessage.addListener(factory);
});


Как помните, ранее в bg.js мы запустили initialization(), setTabId(), setHosts() и run(). Найбольший интерес представляет window.popup.run(). Там проверяется доменное имя сервера открытой страницы, и если это имя совпадает со списком сайтов которые нам интересны (данные с которых необходимо передать на корпоративный ресурс) — find = true; и отправляем запрос window.bg.mustParsed(obj) в bg.js.



/**
* Function will be called from script_in_content.js
*/
mustParsed: function(data)
{
if(this.tabs[data.tab_id]){
var id = data.tab_id;
this.tabs[id].must_parsed = data.find;

// run parser in popup.js, if need
if(this.tabs[id].must_parsed && (this.tabs[id].must_parsed === true))
this.tabs[id].port_info.postMessage({method:'parsePage'});
}
}


Если совпадение домена было найдено, то запускаем парсер страницы parsePage() в script_in_content.js.



/**
* Function will be called from bg.js
* Parse page
*/
parsePage: function()
{
// reset variable before parse
this.matches = [];

if(!this.available_hosts[this.total_host])
return 0;

var html = window.document.body.innerHTML;
var reg_exp = this.available_hosts[this.total_host];
var matches = {};
var match = [];
var find = false;
for(var i = 0, len = reg_exp.length; i < len; i++) {
var exp = new RegExp(reg_exp[i].reg_exp, reg_exp[i].flag);
match = exp.exec(html);

if(match && match.length && reg_exp[i].index){
matches[reg_exp[i].field] = trim(match[reg_exp[i].index]);
find = true;
}
else if(match && match.length){
matches[reg_exp[i].field] = match;
find = true;
}
}

// this url will be send to site
if(find === true){
matches.url = document.location.href;
this.matches.push(matches);
}

// send count of matches
this.port.postMessage({method:'matchesCount', data:{tab_id:this.tab_id, matches: this.matches}});
}


Если скрипт что-то нашел на странице, то все что он нашел складывает в массивчик, добавляет к нему текущий url страницы и отправляет назад в bg.js, мол: «Смотри что я нашел...». В ответ на это, bg.js анализирует входные данные, и если RegExp'ы нашли что-то — пишет поверх иконки колличество совпадений (1, 2 и тд.) chrome.browserAction.setBadgeText({text: this.tabs[id].matches_count});.


Это вроде все основные моменты рыботы связки bg.js и script_in_content.js.

Теперь поговорим о popup. Когда пользователь кликает по иконке приложения — отображается форма login.html.

Менеджер вводит свои данные от корпоративного сайта, нажимает Login и тут происходит следующее:


login.html


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="login.js"></script>
<link type="text/css" rel="stylesheet" href="login.css">
<title>Grabber popup</title>
</head>
<body>
<div class="body">
<div class="emptyLogin">
<div id="error_message"> </div>
<form name="login_form" action="" method="get" id="popup_login_form">
<table>
<tbody>
<tr>
<td align="right">Ваш E-mail:</td>
<td><input type="text" name="login" value="" tabindex="1"></td>
</tr>
<tr>
<td align="right">Пароль:</td>
<td><input type="password" name="pass" value="" tabindex="2"></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="Login" class="button"></td>
</tr>
</tbody>
</table>
</form>
</div>
<div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div>
</div>
</body>
</html>







login.js


/**
* OnLoad function
*
* @return void
*/
window.onload = function(){

// set some events handlers
document.getElementById('popup_login_form').onsubmit = function(obj){
// fade popup
document.getElementById('loader').style.display = 'block';
document.getElementById('error_message').innerHTML = ' ';

if(obj.target.elements && obj.target.elements.length && (obj.target.elements.length === 3)){
var data = {};
data.login = obj.target.elements[0].value;
data.pass = obj.target.elements[1].value;

setTimeout(function(){
var bg_wnd = chrome.extension.getBackgroundPage();
var result = bg_wnd.bg.loginUser(data);

if(result && result.status && (result.status === 'error'))
document.getElementById('error_message').innerHTML = result.mess;
else{
// set new popup html code and close popup window
bg_wnd.bg.setPopup('find.html');
window.close();
}

// hide fade on popup
document.getElementById('loader').style.display = 'none';
}, 500);
}
return false;
};
}







Задача login.js состоит в том, что бы повесить onsubmit на форму, и отправить логин/пароль в background.html (bg.js),

а делается это с помощью следующей конструкции (как увидите, мы пожем на прямую вызывать методы объекта bg.js):

var bg_wnd = chrome.extension.getBackgroundPage();
var result = bg_wnd.bg.loginUser(data);


bg_wnd.bg.loginUser(data) отправляет данные на сервер, если все хорошо, то popup login.html сменяет find.html,

а данные о пользователе сохраняются в переменной. Смена popup происходит следующим образом:



/**
* Function will be called from login.js and others places
*/
setPopup: function(popup_file)
{
chrome.browserAction.setPopup({tabId: this.active_tab.tabId, popup: popup_file});
},


Небольшое замечание, если пользователь открыл popup login.html поставил курсор в поле 'Ваш E-mail:' и нажимает TAB (первый раз) в надежде перейти к паролю, то его ожидает разачарование, фокус не сменится. Данный баг все еще актуален.


Так, осталось совсем чуть-чуть.

После того как мы успешно авторизовались, мы поменя popup на find.html.


find.html


<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="find.js"></script>
<link type="text/css" rel="stylesheet" href="find.css">
<title>Grabber</title>
</head>
<body>
<div class="body">
<div class="carsRows" id="popup_cars_rows">
<h3 style="text-align: center; margin: 5px 0;">Найденные данный странице</h3>
<form name="cars_form" action="" method="get" id="popup_cars_form">
<table id="popup_cars_table">
<thead>
<tr>
<th class="make">Вакансия</th>
<th class="info">Город</th>
<th class="addBtn"> </th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</form>
</div>
<div class="carsRows" id="popup_cars_rows_none" style="display: none;">
<h3 style="text-align: center; margin: 5px 0;">Ничего не найдено на странице</h3>
</div>
<div id="loader"><img src="ajax-loader.gif" title="Loding" alt="Loading"></div>
</div>
</body>
</html>







find.js


/**
* OnLoad function
*
* @return void
*/
window.onload = function(){

// set new popup html code and close popup window
window.bg_wnd = chrome.extension.getBackgroundPage();
var rows = window.bg_wnd.bg.getMatches();

// function render popup
renderPopup(rows);
}


/**
* Function set cars into html
*
* @param array $rows
* @return void
*/
function renderPopup(rows)
{
if(rows.length === 0){
document.getElementById('popup_cars_rows').style.display = 'none';
document.getElementById('popup_cars_rows_none').style.display = 'block';
return 0;
}
else{
document.getElementById('popup_cars_rows').style.display = 'block';
document.getElementById('popup_cars_rows_none').style.display = 'none';
}

for (var i = 0, cnt = rows.length; i < cnt; i++)
renderRow(rows[i]);
}


/**
* Function set cars into html
*
* @param object $row
* @return void
*/
function renderRow(row)
{
var tbl = document.getElementById('popup_cars_table').children[1];

// add divided row
var td = tbl.insertRow(-1).insertCell(-1);
td.setAttribute('colspan', '3');
td.innerHTML = '<hr style="border: 1px solid #909090; width: 75%">';

var tr = tbl.insertRow(-1);
var td1 = tr.insertCell(-1);
var td2 = tr.insertCell(-1);
var td3 = tr.insertCell(-1);
var vacancy = [];
var city = [];

var hash = {
vacancy: 'вакансия',
city: 'город',
}

var table_row = [];
for(key in row){
if(hash[key]){
if(key == 'vacancy')
vacancy.push(row[key]);
if(key == 'city')
city.push(row[key]);
}
}

td1.innerHTML = vacancy.join(' ');;
td2.innerHTML = city.join(' ');
td3.innerHTML = (row.url === '')?'<b><em>Добавлено</em></b>':'<input type="button" value="Дабавить" name="cars[]" class="button"><input type="hidden" value="'+row.url+'" name="url[]">';
td3.children[0].addEventListener('click', function(){addToGrabber(event)}, false);
}


function addToGrabber(e)
{
// hide fade on popup
document.getElementById('loader').getElementsByTagName('img')[0].style.marginTop = (window.innerHeight/2-10)+'px';
document.getElementById('loader').style.display = 'block';

if(e && e.srcElement){
var url = e.srcElement.parentNode.children[1].value;

setTimeout(function(){
var result = window.bg_wnd.bg.addUrlToGrabber(url);
e.srcElement.parentNode.innerHTML = '<b><em>Добавлено</em></b>';

// hide fade on popup
document.getElementById('loader').style.display = 'none';
}, 500);
}
}







Как только find.html загрузился, в работу вступает find.js. Его задача спросить bg.js: 'Что там у тебя есть на текущую страницу' — и отобразить то, что отдал bg.js.

/**
* OnLoad function
*
* @return void
*/
window.onload = function(){

// set new popup html code and close popup window
window.bg_wnd = chrome.extension.getBackgroundPage();
var rows = window.bg_wnd.bg.getMatches();

// function render popup
renderPopup(rows);
}


Так выглядит готовое решение.



C кнопкой 'Добавить' я думаю сами разберетесь, как она работает. На последок хочу сказать как все это дело отлаживается.

background.html — что бы посмотреть работу скриптов bg.js и lib.js нужно кликнуть на линку background.html на странице chrome://extensions.



script_in_content.js — он выполняется в контексте страницы, поэтому можете смело инспектировать страницу и смотреть консоль с выводом ошибок в нее.

login.html и find.html — что бы вывести их Developer Tools, нужно кликнуть на иконке приложения и правым кликом мышки выбрать инспекцию страницы.


PS. Весь JavaScript должен находится в js файлах, если вы его вставите в html — chrome будет ругаться.

Также пару ссылок:

на документацию: manifest.json, Chrome's API

на github.com: исходный код


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий