На Хабре уже была статья про чудесный плагин для Firefox — Ubiquity. Очень экспериментальный плагин. Советую как минимум посмотреть на него.
Так вот, мне он очень понравился и я написал своё расширение — поиск адреса на картах mapia.ua. О чём и написал статейку.
Превью
Что же примечательного в этом Ubiquity?
- Во-первых этот плагин суть сборище веб-сервисов в одной командной строке (!): Google [Calculator, Mail, Maps, Translate, ...], Twitter, delicious, Wiki, Digg, tinyurl, Yahoo, Flickr и много чего ещё. Список участников постоянно пополняется. И дело тут даже не в количестве и качестве предоставляемых веб-сервисов. Дело в удобстве использования самого Ubiquity: теперь необязательно обвешивать бедный Firefox сотней чудесных тулбаров и плагинов, всё — в одном месте. И это действительно удобно.
- Во-вторых, разрабатывает его не кто-то там, а сам Аза Раскин, сын великого (не побоюсь этого слова) эксперта в юзабилити Джефа Раскина (земля ему пухом), автора великолепной книги «The Human Interface», которую просто обязан прочесть каждый, кто имеет дело с юзер-интерфейсом. Аза Раскин сейчас — ведущий спец по юзабилити в Mozilla Labs. Респект ему и уважуха :-)
Вот ознакомительное видео от автора плагина.
Всё действо происходит в командной строке, прямо в браузере. Достаточно нажать Ctrl+Space. Как говорится, всем любителям POSIX посвящается. Но всё настолько просто и ясно, что и мартышка освоит: команды вводить полностью не надо (suggestions), параметров раз-два и обчёлся. По умолчанию первым аргументом в команду передаётся то, что выделено мышкой на странице. Собственно в этом огромный плюс, скажем, при переводе статей с немецкого (по нажатию Enter, кстати, выделенный кусок текста переведётся прямо на странице). Или чтоб скинуть текст другу на мыло. Вообщем, Аза потрудился на славу.
Велосипед
Чтобы создать свой чудо-сервис, нам понадобится, собственно, Firefox с установленным Ubiquity. Есть? Приступим.
Карты, карты, как мне вас не хватает
В Ubiquity уже есть предустановленные карты Google Maps. Но я, как житель славного города Киева, привык пользоваться другими — http://mapia.ua Да и в качестве примера — сойдёт :-)
Да, специально для тех, кто не любит многабукаф — вот ссылка на уже готовый плагин.
Чтобы начать редактировать, нажмите «Редактировать». Спасибо, Кэп!
Итак, нажимаем Ctrl+Space, набираем help, переходим на страничку настроек Ubiquity, находим и нажимаем «Hack Ubiquity». Мы в редакторе. Если не очень удобно кодить в textarea, то можно подключить любой внешний редактор и работать в нём (я предпочитаю notepad++, к примеру).
Первые шаги
CmdUtils.CreateCommand({
names: ["mapia", "mpi"],
icon: "http://mapia.ua/favicon.ico",
description: _("Kiev map powered by mapia.ua"),
author: {name: "Kottenator", email: "kottenator@gmail.com"},
license: "MIT"
});
* This source code was highlighted with Source Code Highlighter.
Тут всё очень просто:
- names, icon, description, author, license — очевидно :-)
_(...)
— метод для i18n
И мы уже можем запустить и посмотреть нашу команду:
Ctrl+Space -> type «mapia» or «mpi» -> увидим такое:
Прямо гордость берёт за содеянное! :) Но продолжим:
CmdUtils.CreateCommand({
names: ["mapia", "mpi"],
icon: "http://mapia.ua/favicon.ico",
description: _("Kiev map powered by mapia.ua"),
author: {name: "Kottenator", email: "kottenator@gmail.com"},
license: "MIT",
arguments: [
{role: 'object', nountype: noun_arb_text, label: _('address')}
],
preview: function preview(pblock, args) {
var msg = "You'd just typed \"${text}\"";
pblock.innerHTML = _(msg, { text: args.object.text });
},
execute: function execute(args) {
var msg = "You choose address: \"${text}\"";
displayMessage(_(msg, { text: args.object.text }));
}
});
* This source code was highlighted with Source Code Highlighter.
Что же мы наделали?
arguments: [arg1, arg2, ...]
— описание аргументов нашей команды (спасибо, Кэп!).
К примеру,{role: 'object', nountype: noun_arb_text, label: _('address')}
:
role: 'object'
означает, что это первый аргумент, ожидаемый сразу же после имени команды;nountype
означает что-то вроде типа этой переменной;label
— то, что мы увидим в описании команды (н-р «mpi (address)»). Подробнее об аргументах команд можно почитать тут
Это метод для отображения превью в окошке Ubiquity.preview: function preview(pblock, args) {
var msg = "You'd just typed \"${text}\"";
pblock.innerHTML = _(msg, { text: args.object.text });
}pblock
— контейнер превьюхи,args
— текущие аргументы комманды;args.object.text
— тот самый аргумент сrole: 'object'
.
Как видим, метод_(...)
умеет делать не только i18n, но и подставляет параметры в шаблон.
А этот метод вызывается, когда мы нажмём Enter.execute: function execute(args) {
var msg = "You choose address: \"${text}\"";
displayMessage(_(msg, { text: args.object.text }));
}
displayMessage(...)
выводит небольшое slide-up уведомление.
Сохранили, поигрались, красота:
Собственно, а где же карты? Начнём с Mapia Static API — это просто картинка участка карты. Продолжим наш код:
arguments: [
{role: 'object', nountype: noun_arb_text, label: _('address')},
{role: 'goal', nountype: noun_arb_text, label: _('zoom')},
{role: 'source', nountype: noun_arb_text, label: _('map size')}
],
previewDelay: 800,
getStaticURL: function(address, zoom, size) {
var mapUrl = "http://mapia.ua/static?";
address = address || '';
zoom = parseInt(zoom, 10) || 15;
zoom = Math.min(18, Math.max(zoom, 6));
size = size || '400x300';
var params = {
address: address,
zoom: zoom,
size: size
};
return mapUrl + jQuery.param(params);
},
preview: function preview(pblock, args) {
var img = '<img src="${url}" />';
var url = this.getStaticURL(args.object.text, args.goal.text, args.source.text);
pblock.innerHTML = _(img, { url: url });
},
* This source code was highlighted with Source Code Highlighter.
- Во-первых, мы добавили ещё 2 аргумента:
{role: 'goal', nountype: noun_arb_text, label: _('zoom')},
{role: 'source', nountype: noun_arb_text, label: _('map size')}
* This source code was highlighted with Source Code Highlighter.
Первый будет отвечать за zoom карты, второй — за размер рисунка.
role: 'goal'
будет ожидать, что мы напишем что-то вроде «mapia Адресс to 16». Вот это «to 16» будет означать, чтоargs.goal.text == '16'
и это будет наш zoom. Возможные значения — от 6 до 18.
Аналогично «mapia Адресс from 300x200» будет означать, чтоargs.source.text == '300x200'
и это будет размер рисунка. - Метод
getStaticURL(address, zoom, size)
возвращает строку адреса нашей карты. - Метод
preview(...)
теперь делает что-то дельное :) — показывает карту previewDelay: 800
— чтобы не убивать сервера mapia.ua сумасшедшей нагрузкой (по запросу на каждое нажатие клавиши), введём задержку перед запросом в 800 миллисекунд
Вот результат:
Отлично, на этом можно было бы и закончить, но я лично хотел создать действительно удобную штуку. Статический рисунок — маловато для моих потребностей. И вот что я сделал…
Вторые шаги
Собственно, предисловие. Mapia не предоставляет открытого веб-сервиса для поиска по адресу улицы. Но ведь это работает на их сайте! И я подумал — ведь нет ничего преступного в том, что я воспользуюсь их поиском через XSS? В конце концов это не уязвимость, и вреда я не наношу, ничего не ворую, а лишь создаю дополнительную рекламу этим ребятам ;-)
Добавим такой метод:
loadDetails: function(pblock, args, page_num) {
var baseUrl = "http://mapia.ua/search";
var params = {city: "%u041A%u0438%u0435%u0432", "search[query]": args.object.text};
var self = this;
jQuery.ajax({
type: "GET",
url: baseUrl,
data: params,
dataType:'html',
error: function() { },
success: function(html) {
$('#content', pblock).html(html);
}
});
},
preview: function preview(pblock, args) {
var img = '<img src="${url}" /><div id="content"></div>';
var url = this.getStaticURL(args.object.text, args.goal.text, args.source.text);
pblock.innerHTML = _(img, { url: url });
this.loadDetails(pblock, args);
}
* This source code was highlighted with Source Code Highlighter.
Как видим, Ubiquity поддерживает jQuery. Это чудесно!
И вот тот самый поиск по mapia — «mapia.ua/search?city=Киев&search[query]=Введённый адресс»
Что делает метод
loadDetails(...)
? Он просто подгружает в наше превью результаты поиска. Думаю, что код ясен и объяснению не подлежит.Перед нами результаты поиска. Но почему-то мы не можем никуда перейти. Ссылки не работают, всё плохо. На самом деле это нормально :) Как я сказал выше, этот механизм предназначен вовсе не для нас, а лишь для самого сайта mapia.ua. Но Ubiquity позволяет делать крос-доменные запросы, что мы и сделали.
Поэтому придётся поработать напильником (RegExp pre-fix + jQuery post-fix):
mapiaURL: "http://mapia.ua/#popup_class=${cat}&popup_id=${sid}",
clearSearchResultsCode: function(html) {
html = html.replace(/<div id="search_bar_title">((?:.|\s)*?)<\/div>/i, '<h3 class="search_bar_title">$1</h3>')
.replace(/<p>.*?<\/p>/i, '<p><small>(результаты поиска предоставлены сайтом mapia.ua)</small></p>')
.replace(/(<div class="buildings_info")[^>]*>/gi, '$1>')
.replace(/<a href="\/center\/street\/(\d+)[^>]*>/i, '<a href="' + _(this.mapiaURL, {cat: 'Street', sid: "$1"}) + '">')
.replace(/<a[^<>]+onclick="\$\('street_\d+_buildings'\).*?<\/a>/i, '')
.replace(/<a href="\/center\/address\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Address', sid: "$1"}) + '">')
.replace(/<a href="\/center\/feature\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Feature', sid: "$1"}) + '">')
.replace(/<a href="\/center\/station\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'Station', sid: "$1"}) + '">')
.replace(/<a href="\/center\/city\/(\d+)[^>]*>/gi, '<a href="' + _(this.mapiaURL, {cat: 'City', sid: "$1"}) + '">')
.replace(/(<div id="search_bar_title"(?:.|\s)*?)<span(?:.|\s)*?<\/span>/i, '$1')
.replace(/(<div id="search_bar_title"(?:.|\s)*?)<span(?:.|\s)*?<\/span>/i, '$1') /* not a duplicate */
.replace(/href="\/search?[^"]*pages_feature=(\d+)[^"]*"/gi, 'page_num="$1"')
.replace(/<p><a[^<>]*onclick="load_content(?:.|\s)*?<\/p>/i, '')
.replace(/<script>(?:.|\s)*?<\/script>/i, '');
return html;
},
loadDetails: function(pblock, args, page_num) {
var baseUrl = "http://mapia.ua/search";
var params = {city: "%u041A%u0438%u0435%u0432", "search[query]": args.object.text};
if (page_num)
params.pages_feature = page_num;
var self = this;
jQuery.ajax({
type: "GET",
url: baseUrl,
data: params,
dataType:'html',
error: function() {},
success: function(html) {
// Pre-fixes via RegExp
html = self.clearSearchResultsCode(html);
$('#content', pblock).html(html);
// After-fixes via jQuery
$('div.pagination a', pblock).click(function(e){
self.loadDetails(pblock, args, $(this).attr('page_num'));
e.preventDefault();
return false;
})
}
});
}
* This source code was highlighted with Source Code Highlighter.
Тут долго объяснять, как оно работает :) Скажу лишь, что оно чистит html от лишнего и делает ссылки кликабельными.
Вот как это должно выглядеть:
Готово!
Ну вот и всё, пациент здоров и может выписываться. Рад, что кто-то дочитал до сих строк :)
Здесь можно скачать собранный воедино код.