Почему люди любят WordPress? Потому что с ним просто работать. В нём нет гибкости большущих CMS вроде Joomla и Drupal, — а значит, не запутаешься. И ещё он очень популярен — а значит, можно найти плагины на все случаи жизни.
Неспроста несмотря на осуждение со стороны Lurkmore.ru, WordPress-ом пользуются и Герб Саттер, и Марк Шаттлворт, и много кто ещё. Например, я.
В своих записях я очень часто ссылаюсь на чужие блоги. И мне пришла идея — а почему бы не показывать рядом с ником человека, на которого я ссылаюсь, ещё и значок его сервиса? Например, птичку из твиттера или букву B из блогспота? Похожий функционал есть, например, в Википедии, да и многие блогохостинги это позволяют (например, Dreamwidth).
Так и родился плагин для WordPress Rikki's WP Social Icons. Позволяет за один клик мышкой добавить ссылку на эккаунт в каком-нибудь сервисе, от социальной сети до GitHub.
Зачем?
Есть много статей о том, как правильно писать plugin-ы для WordPress, но не в одной из них не было ни слова про те баги, с которыми я столкнулся в процессе работы. Поэтому и записываю свои впечатления — вдруг кто-то столкнётся с тем же, а как решить — не знает. И указываю все доработки, которые обычно опускают в учебных примерах.
Во время работы я довольно много пользовался исходниками чужих плагинов, а также наработками Jenyay (ссылки на его цикл статей приведён в приложении). Но «кустарной» реализации было мало. Plugin тем и удобен, что каждый конечный пользователь может доработать его сам. А значит, нужно сделать так, чтобы добавить к нему поддержку нового сервиса было делом пяти минут и одной переустановки.
Особенно помогало работе то, что я собирался использовать этот плагин сам (т.е., питаться собачьим кормом, как говорят наши американские коллеги). И именно мои мучения с первыми набросками подсказали мне, где и что можно улучшить.
Программируем
Проектируем
Начнём с того, что писать плагины для WordPress не просто, а очень просто. Не нужно создавать ни классов, от чего-то там унаследованных, ни мудрить с форматами, ни следовать каким-то хитрым спецификациям. «События» вынесены в специальные объекты, которые работают примерно как event-ы в «больших» приложениях.
Для начала надо хорошенько обдумать, что нам нужно. А нужен нам способ маркировки отдельных слов, который был бы виден при редактировании, и автоматически переделывался в ссылку в постах, страницах и RSS.
Очень похоже работает shortcode — специальный тег, окружённый квадратными скобками и специально заточенные под обработку плагинами. Они появились ещё версии 2.5, которая вышла в далёком 2008 году, так особые проблемы с совместимостью нам не грозят.
Какой формат нам следует принимать от пользователя? Т.к. конечный HTML-код будет отличаться только иконкой и url-ом, было бы логично завести один shortcode и одну функцию, которая бы его обрабатывала. Дополнительные сведения можно передавать и через параметры, что очень удобно. К тому же, чем меньше shortcode-ов мы создадим, тем меньше шанс, что мы вступим в конфликт с каким-то другим плагином.
Мой shortcode выглядел так:
[userid]
Его параметры:
'id' — необязательный параметр. id, под которым наш герой зарегистрирован на сервисе. Например. torvalds-family.
'type'- сервис
'url'- необязательный параметр. url на который мы хотим сослаться (например, профиль или какой-то отдельный пост в блоге).
А использовать его вот так:
[userid type="blogspot" id="torvalds-family"]Linus Torvalds[/userid]
или так:
[userid type="blogspot" url="http://blogspot.com"]blogspot[/userid]
Пишем ядро
Теперь создаём структуру для нашего плагина (см. на GitHub) и набрасываем в rikkis-wp-social-icons.php наше миниатюрное ядро:
class socialusers
{
var $options = array(
"blogspot" => "http://%s.blogspot.com/",
"ljuser" => "http://%s.livejournal.com/",
"ljcomm" => "http://livejournal.com/community/%s",
"liruboy" => "http://www.liveinternet.ru/users/%s/",
"lirugirl" => "http://www.liveinternet.ru/users/%s/",
"vk" => "http://vk.com/%s",
"twitter" => "http://twitter.com/#!/%s/",
"facebook" => "http://www.facebook.com/%s",
"google_plus" => "https://plus.google.com/%s",
"wordpress" => "http://%s.wordpress.com/",
"habrahabr" => "http://%s.habrahabr.ru/",
"github" => "http://github.com/users/%s/"
);
function socialusers(){
if (!function_exists ('add_shortcode') ) return;
add_shortcode('userid', array (&$this, 'icon_func') );
}
function icon_func($atts, $content="") {
if (!$content) return "";
extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
if (!$type || !array_key_exists($type, $this->options) ) return $content;
if (!$id) $id = $content;
$userinfo_url = ($url) ? $url : sprintf($this->options[$type], trim($id));
$userpic_url = plugins_url( "js/img/$type.gif" , __FILE__ );
return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
}
}
$socialusers = new socialusers();
Чтобы избежать конфликта имён переменных, мы сложили все наши вызовы в один класс socialusers. В переменной options хранятся id и URL-ы сервисов, на которые мы собираемся ссылаться, с заменой имени пользователя на %s. В папке js/img кладём gif-ки с соответствующими иконками.
В конструкторе мы проверяем, поддерживает ли WordPress shortcode, и, если нет, то не делаем уже ничего. Нехорошо обваливать пользователю весь WordPress только потому, что он пользуется старой версией.
Если всё в порядке — мы добавляем новый shortcode, указывая для него в качестве обработчика функцию icon_func из этого же экземпляра класса.
icon_func — она очень короткая и очень интересная. В неё приходит 2 параметра:
$atts — массив атрибутов
$content — текст между тегами
Пользователь увидит вместо shortcode то, что вернёт ему эта функция. Именно здесь (в последнем return) и формируется окончательный код нашего блока.
Заслуживают внимания две строчки:
extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
</<source>
формирует из элементов массива $atts локальные переменные с соответствующими именами
и
<source lang="php">
plugins_url( "js/img/$type.gif" , __FILE__ );
Получает url иконки по типу, относительно текущей директории. Именно для этого нужен атрибут __FILE__. Если же его нет (а авторы некоторых плагинов и примеров явно про него не слышали), то придётся вставлять имя директории плагина и всё равно это может не заработать.
Не менее важно сделать trim() для $id. Дело в том, что если дважды щёлкнуть по слову в editor-е WordPress-а, то он выделит слово вместе с пробелом после него. В результате мы получим имя учётной записи с совершенно неуместным пробелом и ссылка работать не будет.
В принципе, уже в таком виде (17 строк кода + 14 строк настроек) наш плагин можно ставить и использовать. Настроящий хакер презирает оконные интерфейсы :). Вы можете попробовать запаковать php и картинки в zip и установить их в wordpress, как устанавливают обычный плагин.
Очень рекомендую поставить для подобных экспериментов локальный Apache+PHP+mySQL, и к ним впридачу WordPress. Отладка пойдёт намного веселее — например, вместо установки-переустановки можно будет просто подменять файлы в соответствующем каталоге.
Добавляем кнопки
За редактирование постов и страниц в WordPress отвечает отдельный компонент tinyMCE. Чтобы его дёрнуть из основного PHP, нужно немного расширить нашу первночальную форму:
class socialusers
{
var $options = array(
"blogspot" => "http://%s.blogspot.com/",
"ljuser" => "http://%s.livejournal.com/",
"ljcomm" => "http://livejournal.com/community/%s",
"liruboy" => "http://www.liveinternet.ru/users/%s/",
"lirugirl" => "http://www.liveinternet.ru/users/%s/",
"vk" => "http://vk.com/%s",
"twitter" => "http://twitter.com/#!/%s/",
"facebook" => "http://www.facebook.com/%s",
"google_plus" => "https://plus.google.com/%s",
"wordpress" => "http://%s.wordpress.com/",
"habrahabr" => "http://%s.habrahabr.ru/",
"github" => "http://github.com/users/%s/"
);
function socialusers(){
if (!function_exists ('add_shortcode') ) return;
add_shortcode('userid', array (&$this, 'icon_func') );
add_filter( 'mce_buttons_3', array(&$this, 'mce_buttons') );
add_filter( 'mce_external_plugins', array(&$this, 'mce_external_plugins') );
}
function icon_func($atts, $content="") {
if (!$content)
return "";
extract( shortcode_atts ( array('id' => null, 'type' => null, 'url' => null), $atts ) );
if (!$type || !array_key_exists($type, $this->options) )
return $content;
if (!$id)
$id = $content;
$userinfo_url = ($url) ? $url : sprintf($this->options[$type], trim($id));
$userpic_url = plugins_url( "js/img/$type.gif" , __FILE__ );
return "<span style='white-space: nowrap; display: inline !important;'><a href='$userinfo_url' ref='nofollow'><img src='$userpic_url' alt='[info]' width='17' height='17' style='vertical-align: bottom; border: 0; padding-right: 1px;vertical-align:middle; margin-left: 0; margin-top: 0; margin-right: 0; margin-bottom: 0;' /></a><a href='$userinfo_url' ref='nofollow'><b>$content</b></a></span>";
}
function mce_external_plugins($plugin_array) {
$plugin_array['rikkisocialicons'] = plugins_url ('js/rikkis-wp-social-icons-editor_plugin.js', __FILE__ );
return $plugin_array;
}
function mce_buttons($buttons) {
return array_merge($buttons, array_keys($this->options));
}
}
$socialusers = new socialusers();
Тут всё просто — mce_external_plugins подгружает JavaScript, в котором и будут генерировать кнопки, а mce_buttons кладёт туда все ключи из словаря options.
Теперь нам предстоит написать JavaScript для кнопочек. Увы, но tinyMCE вшит в WordPress настолько прочно, что стандартный способ передачи параметров из PHP в JavaScript для плагинов WordPress здесь не сработает. Конечно, можно было бы попытаться получить их через JSon и добиться, чтобы исправление нужно было вносить действительно только в одном месте. Но это тот самый случай, когда малозначительное удобство может вызвать значительные проблемы.
Обрамление у tinyMCE плагина довольно стандартное:
(function() {
tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
init : function(ed, url) {
//здесь добавляем кнопки
}
},
getInfo : function() {
return {
//кто виноват в том, что всё это сделал
};
}
});
tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();
Добавление одной кнопки, которая обрамляет выделенный фрагмент текста каким-тегом нашего shortcode выглядит так:
ed.addCommand('mce-blogspot', function() {
var newcontent = '[userid type="blogspot"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('blogspot', {
title : 'blogspot',
сmd : 'mce-blogspot',
image : url + '/img/blogspot.gif'
});
Разумеется, пользователь, уже привыкший к тому, что наш плагин сам себя настраивает, захочет попытаться сгенерировать кнопки в цикле:
(function() {
tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
var newButtons = ["ljuser", "ljcomm", "liruman", "lirugirl", "ljr", "vk", "twitter"];
tinymce.create('tinymce.plugins.LjusersPlugin', {
init : function(ed, url) {
var newButtonsLength = newButtons.length, i = 0;
while(i < newButtonsLength){
var itemTitle = newButtons[i];
var itemCommand = 'mce'+itemTitle;
ed.addCommand(itemCommand, function() {
var newcontent = '[userid type="'+itemTitle+'"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton(itemTitle, {
title : itemTitle,
cmd : itemCommand,
image : url + '/img/'+itemTitle+'.gif'
});
i++;
}
},
getInfo : function() {
return {
longname : 'Rikki's WP Social Icons',
author : 'Rikki Mongoose',
authorurl : 'http://rikkimongoose.ru',
infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
version : "1.0"
};
}
});
tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();
И то верно — разве не должны за программиста работать роботы?
Если сгенерировать кнопки таким образом, а потом перейти на editor, то сразу почувствуешь гордость за своё мастерство. Кнопки, указанные в array-е, выстроились в ряд, и на каждой — та самая иконка, которую увидит посетитель сайта или читатель RSS-ленты. Очень удобно!
Этот код выглядит замечательно, но у него есть один-единственный недостаток — он не работает. Это первый баг, который подстерегает вас в tinyMCE. На первый взгляд кнопочки выглядят самыми обыкновенными — но если пощёлкать по ним, то оказывается, что каждая из них вставляет shortcode с последним элементом — в нашем случае с twitter.
Придётся писать всё руками. Примерно вот так:
(function() {
var newButtons = ["blogspot", "ljuser", "twitter", "google_plus", "wordpress", "habrahabr", "github"];
tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin', {
init : function(ed, url) {
if(newButtons.indexOf("blogspot") > -1){
ed.addCommand('mce-blogspot', function() {
var newcontent = '[userid type="blogspot"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('blogspot', {
title : 'blogspot',
cmd : 'mce-blogspot',
image : url + '/img/blogspot.gif'
});
}
if(newButtons.indexOf("ljuser") > -1){
ed.addCommand('mce-ljuser', function() {
var newcontent = '[userid type="ljuser"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('ljuser', {
title : 'ljuser',
cmd : 'mce-ljuser',
image : url + '/img/ljuser.gif'
});
}
if(newButtons.indexOf("ljcomm") > -1){
ed.addCommand('mce-ljcomm', function() {
var newcontent = '[userid type="ljcomm"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('ljcomm', {
title : 'ljcomm',
cmd : 'mce-ljcomm',
image : url + '/img/ljcomm.gif'
});
}
if(newButtons.indexOf("liruboy") > -1){
ed.addCommand('mce-liruboy', function() {
var newcontent = '[userid type="liruboy"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('liruboy', {
title : 'liruboy',
cmd : 'mce-liruboy',
image : url + '/img/liruboy.gif'
});
}
if(newButtons.indexOf("lirugirl") > -1){
ed.addCommand('mce-lirugirl', function() {
var newcontent = '[userid type="lirugirl"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('lirugirl', {
title : 'lirugirl',
cmd : 'mce-lirugirl',
image : url + '/img/lirugirl.gif'
});
}
if(newButtons.indexOf("vk") > -1){
ed.addCommand('mce-vk', function() {
var newcontent = '[userid type="vk"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('vk', {
title : 'vk',
cmd : 'mce-vk',
image : url + '/img/vk.gif'
});
}
if(newButtons.indexOf("twitter") > -1){
ed.addCommand('mce-twitter', function() {
var newcontent = '[userid type="twitter"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('twitter', {
title : 'twitter',
cmd : 'mce-twitter',
image : url + '/img/twitter.gif'
});
}
if(newButtons.indexOf("facebook") > -1){
ed.addCommand('mce-facebook', function() {
var newcontent = '[userid type="facebook"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('facebook', {
title : 'facebook',
cmd : 'mce-facebook',
image : url + '/img/facebook.gif'
});
}
if(newButtons.indexOf("google_plus") > -1){
ed.addCommand('mce-google_plus', function() {
var newcontent = '[userid type="google_plus"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('google_plus', {
title : 'google_plus',
cmd : 'mce-google_plus',
image : url + '/img/google_plus.gif'
});
}
if(newButtons.indexOf("wordpress") > -1){
ed.addCommand('mce-wordpress', function() {
var newcontent = '[userid type="wordpress"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('wordpress', {
title : 'wordpress',
cmd : 'mce-wordpress',
image : url + '/img/wordpress.gif'
});
}
if(newButtons.indexOf("habrahabr") > -1){
ed.addCommand('mce-habrahabr', function() {
var newcontent = '[userid type="habrahabr"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('habrahabr', {
title : 'habrahabr',
cmd : 'mce-habrahabr',
image : url + '/img/habrahabr.gif'
});
}
if(newButtons.indexOf("github") > -1){
ed.addCommand('mce-github', function() {
var newcontent = '[userid type="github"]' + tinyMCE.activeEditor.selection.getContent({format : 'raw'}) + '[/userid]';
tinyMCE.activeEditor.selection.setContent(newcontent);
});
ed.addButton('github', {
title : 'github',
cmd : 'mce-github',
image : url + '/img/github.gif'
});
}
},
getInfo : function() {
return {
longname : 'Rikki's WP Social Icons',
author : 'Rikki Mongoose',
authorurl : 'http://rikkimongoose.ru',
infourl : 'http://rikkimongoose.ru/projects/rikkis-wp-social-icons/',
version : "1.0"
};
}
});
tinymce.PluginManager.add('rikkisocialicons', tinymce.plugins.RikkiSocialIconsPlugin);
})();
Я предпочитаю указывать в таких случаях все возможные варианты, чтобы, если вдруг потребуется перенастройка, было достаточно добавить/удалить ещё один элемент в массиве newButtons.
Этот вариант уже намного лучше — каждая кнопка знает своё место и срабатывает, как надо. Но опытные веб-разработчики уже видят второй баг — скрипт не будет работать в Internet Explorer младше 9-ой версии. Ведь с точки зрения прежних версий IE никакого indexOf у array-а быть не может — этот параметр появится только в дополнениях к стандарту ECMA-262.
Поэтому в самое начало скрипта нам следует дописать ставшей уже классической реализацию indexOf для Internet Explorer < 9:
if (!Array.prototype.indexOf)
{
Array.prototype.indexOf = function(elt /*, from*/)
{
var len = this.length;
var from = Number(arguments[1]) || 0;
from = (from < 0)
? Math.ceil(from)
: Math.floor(from);
if (from < 0)
from += len;
for (; from < len; from++)
{
if (from in this &&
this[from] === elt)
return from;
}
return -1;
};
}
Наконец, есть ещё третий баг, который зависит исключительно от вас. И, хотя WordPress, tinyMCE и даже Internet Explorer тут виноваты разве что в не очень удобной обработке ошибки, он может попортить вам немало крови.
Возможная ошибка касается функции tinymce.PluginManager.add(param1, param2). Пожалуйста, пишите её очень внимательно.
param1 должен совпадать с ключом, в который мы добавляли массив id новых кнопок в нашем php-файле. В моём случае там должен быть 'rikkisocialicons' (т.к. в PHP у нас $plugin_array['rikkisocialicons']).
param2 должен совпадать с тем, что создаётся через tinymce.create(). В моём случае это tinymce.plugins.RikkiSocialIconsPlugin, (т.к. в скрипте у нас написано tinymce.create('tinymce.plugins.RikkiSocialIconsPlugin')
Если что-то из этого не будет совпадать — у вашего editor-а пропадёт панель с кнопками, а консоль сообщит про ошибку 'k is undefined', которая произошла… разумеется, в сжатой jQuery.min.js, так что ни отладить, ни посмотреть вызов не будет ни малейшей возможности.
Как добавить иконку для сервиса X?
Сначала неплохо сходить на GitHub и посмотреть — вдруг уже появился fork с нужной иконкой? Если нет — тогда спасаемся своими руками.
- Сохраняем её в формате gif в ту же директорию, где лежат остальные иконки
- Добавляем в $options ещё один параметр, где ключ совпадает с именем gif-а, а URL — с url-ом, который нужно подставлять, причём имя пользователя заменяем на %s (пользуясь случаем, передаю привет всем C++ программистам, которые на этом месте наверняка испытают ностальгию).
- Желательно добавить её поддержку и в JavaScript. Открываете его, и дописываем в инициализаторы кнопок ещё один, по образцу
- Если хотите кнопку — добавляете элемент с тем же id в массив newButtons
- Упаковываете всё в ZIP
- Отключаете и удаляете старую версию плагина. Не беспокойтесь, shortcode в ваших постах при этом не пострадают
- Загружаете обновлённую версию, активируете её и наслаждаетесь
Очерёдность кнопок зависит от очерёдности в PHP-файле.
Вот и всё!
Плагин лежит в каталоге WordPress. Там же, или на GitHub-е, вы можете порадовать автора, сообщив, что теперь и ваш stand-alone блог украшен модными иконками. А ещё можете дополнить проект иконками dreamwidth-а или ещё какой-нибудь xanga.
Добавляем в каталог
Остался последний шаг — добавление плагина в каталог WordPress, чтобы его можно было найти стандартным поиском. Вся процедура подробно описана в большом англоязычном мануале.
А для тех, кому не терпится — вот короткое пошаговое руководство:
- Идём на wordpress.org и создаём там учётную запись
- Получаем пароль. Заходим под ним и идём на страницу добавления plugin-а. Закидываем туда ссылку на ZIP, пишем название и описание, и отправляем Post.
- Теперь нужно подождать. Плагин из мануала рассматривали 18 часов, с моим уложились в часа 4. Когда рассмотрение закончилось и всё получилось, на сервере появится пустая SVN-директория, в которую надо будет залить ваш проект
- Создаём локально папку, делаем туда checkout и копируем наш plug-in в trunc. Сжимать ZIP-ом не надо — после commit-а в trunc скрипт на сервере сожмёт всё автоматически.
- Делаем commit.
- Идём на страницу http://wordpress.org/extend/plugins/rikkis-wp-social-icons/. Вместо rikkis-wp-social-icons подставьте название вашего плагина.
- Идём на страницу http://wordpress.org/extend/plugins/ и смотрим внимательно на список Newest Plugins. Вот он, наш красавец!
Если что-то не заработало — обратитесь к большому мануалу. Там всё очень подробно расписано.
См. также
- В каталоге WordPress
- Официальное представительство на GitHub
- Скачать c GitHub
- Оригинальная статья от Jenyay — часть 1, часть 2, часть 3.
Автор будет благодарен всем, кто возьмёт шефство над github-овской версией проекта и будет развивать его дальше.
Автор: RikkiMongoose