понедельник, 11 апреля 2016 г.

CustomButtons часть 7-я. Еще пара слов о GreaseMonkey и букмарклетах.

  1. Фильтрация элементов меню в зависимости от адреса страницы
  2. Выполнение скрипта при загрузке страницы
  3. Работа с закладками

Фильтрация элементов меню в зависимости от адреса страницы



В GreaseMonkey есть такая удобная штука: команды скриптов в соответствующем меню отображаются только на тех страницах для которых одни предназначались. Это позволяет не только экономить рабочее пространство браузера, но и делает список команд более компактным, поскольку он ограничивается только командами, актуальными для текущей страницы. Реализовать такое в своем меню - совсем несложно. Для этого надо всего-навсего обработать событие popupshowing соответствующего menupopup и в обработчике проверять, нужно ли делать menuitem видимым в данный момент.

Для фильтрации адресов по шаблону (как это реализовано в GreaseMonkey) мы воспользуемся способом описанным в следующем топике документации.
Match patterns - Mozilla | MDN
Там нужно будет импортировать два модуля, о других глобальных модулях, которые можно также использовать в своих проектах можно узнать здесь
JavaScript code modules - Mozilla | MDN.

Для того, чтобы сделать процесс создания таких меню более легким я ввел для menuitem, которые должны быть видны не везде, два дополнительных атрибута data-uri-include и data-uri-exclude. В первый мы будем размещать список шаблонов страниц, на которых данный элемент будет виден, во второй - список шаблонов страниц, которые могут соответствовать какому-то шаблону из первого списка, но их все равно надо исключить.

Для примера создадим оверлей следующего содержания
Кликните здесь для просмотра всего текста
Код xml Выделить

<?xml version="1.0" encoding="utf-8" ?>

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <script type="application/javascript">

    <![CDATA[

Components.utils.import("resource://gre/modules/MatchPattern.jsm");

Components.utils.import("resource://gre/modules/BrowserUtils.jsm");

 

function setVisibility(element)

{

    var ma = element.getAttribute("data-uri-include");

    ma = ma ? ma.split(",") : null;

    var mp = new MatchPattern(ma);

    var mipa = element.getAttribute("data-uri-exclude");

    mipa = mipa ? mipa.split(",") : null;

    var mip = new MatchPattern(mipa);

    var uri = BrowserUtils.makeURI(content.document.location.href);

 

    if (mip.matches(uri))

    {

        element.setAttribute("hidden", "true");

    }

    else if (mp.matches(uri))

    {

        element.setAttribute("hidden", "false");

    }

    else

    {

        element.setAttribute("hidden", "true");

    }

 

}

function scriptsCommands_popupshowing(src)

{

    var items = src.querySelectorAll("menuitem");

    for (var i = 0; i < items.length; i++)

    {

        setVisibility(items[i]);

    }

}

  ]]>

  </script>

  <popupset id="mainPopupSet">

    <menupopup id="filterExampleMenu">

      <menu label="Команды скриптов">

        <menupopup onpopupshowing="scriptsCommands_popupshowing(this);">

          <menuitem label="Киберфорум и гугл" data-uri-include="http://www.cyberforum.ru/*,*://*.google.com/*"/>

          <menuitem label="Киберфорум и гугл(кроме блогов киберфорума)" data-uri-include="http://www.cyberforum.ru/*,*://*.google.com/*"  data-uri-exclude="http://www.cyberforum.ru/blogs*"/>

          <menuitem label="Гугл" data-uri-include="*://*.google.com/*"/>

          <menuitem label="Гугл и вконтакте" data-uri-include="*://vk.com/*,*://*.google.com/*"/>

          <menuitem label="Вконтакте и киберфорум" data-uri-include="http://www.cyberforum.ru/*,*://vk.com/*"/>

          <menuitem label="Вконтакте и киберфорум-блоги" data-uri-include="http://www.cyberforum.ru/blogs*,*://vk.com/*"/>

        </menupopup>

      </menu>

    </menupopup>

  </popupset>

</overlay>

 

Создавать специальную кнопку для того, чтобы он грузился всегда - не нужно, поскольку это делается для иллюстрации и можно загрузить его из файла при помощи ранее созданной кнопки загрузки оверлея (код приводил здесь).
Теперь для того, чтобы привязать это к кнопке, создаем новую кнопку и во вкладке "Инициализация" ее редактора размещаем код
Код javascript Выделить

this.type = "menu-button";

this.setAttribute("popup", "filterExampleMenu");

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



Выполнение скрипта при загрузке страницы



Другой полезной возможностью, которой обладает GreaseMonkey является возможность выполнять какой-то код при загрузке страницы, адрес которой соответствует определенному шаблону. О проверке на соответствие шаблону я уже написал, а вот что касается перехвата загрузки страницы, то подробно об этом написано здесь.
Intercepting Page Loads - Mozilla | MDN

Теперь попробуем связать эти темы в одном коде. Воспользуемся простейшим способом отслеживания загрузки страниц - подпиской на событие load объекта gBrowser. Для начала следует упомянуть, что код надо будет размещать во вкладке "Инициализация" кнопки, поэтому нам придется позаботиться о том, чтобы подписка на событие произошла только один раз, иначе при каждой инициализации у нас будет добавляться обработчик события и при загрузке страниц выполняться будут они все. Подписаться с помощью такого кода
Код javascript Выделить

gBrowser.onload = function (evt) {/* Здесь код обработки события*/ };

у меня не получилось, по всей видимости это не работает. А вот при использовании addEventListener, удалить этот обработчик, конечно, можно, но это тоже непросто. Поэтому я пошел следующим путем.
Код javascript Выделить

if (!gBrowser.loadHandlers)

{

    gBrowser.loadHandlers = [];

 

    gBrowser.addEventListener("load", function (evt)

    {

        gBrowser.loadHandlers.forEach(function (h) { h(evt); });

    }, true);

 

}

 

gBrowser.loadHandlers.pop();

gBrowser.loadHandlers.push(function (evt)

{

 

});

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

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

Components.utils.import("resource://gre/modules/MatchPattern.jsm");

Components.utils.import("resource://gre/modules/BrowserUtils.jsm");

function PageScript(func, include, exclude)

{

    this.func = func; this.include = include; this.exclude = exclude;

    this.testPage = function (doc)

    {

        var uri = BrowserUtils.makeURI(doc.location.href);

        var inclMatch = new MatchPattern(this.include);

        var exclMatch = new MatchPattern(this.exclude);

        if (inclMatch.matches(uri) && !exclMatch.matches(uri)) return true;

        return false;

    }

}

Для того, чтобы можно было легко добавлять функции обработки отдельных страниц мы создадим массив под названием scripts и будем добавлять в него объекты PageScript. Обработчик события load при этом будет обходить этот массив и решать для каждого элемента, подходит он для этой страницы или нет, и если подходит - запускать скрипт.
Кликните здесь для просмотра всего текста
Код javascript Выделить

Components.utils.import("resource://gre/modules/MatchPattern.jsm");

Components.utils.import("resource://gre/modules/BrowserUtils.jsm");

function PageScript(func, include, exclude)

{

    this.func = func; this.include = include; this.exclude = exclude;

    this.testPage = function (doc)

    {

        var uri = BrowserUtils.makeURI(doc.location.href);

        var inclMatch = new MatchPattern(this.include);

        var exclMatch = new MatchPattern(this.exclude);

        if (inclMatch.matches(uri) && !exclMatch.matches(uri)) return true;

        return false;

    }

}

 

var scripts = [

    new PageScript(() => alert("Это гугл"), ["*://www.google.ru/*"], []),

    new PageScript(() => alert("Это вконтактик"), ["*://vk.com/*"], []),

    new PageScript(() => alert("Это киберфорум кроме блогов"), ["*://www.cyberforum.ru/*"], ["*://www.cyberforum.ru/blogs*"])

];

 

 

if (!gBrowser.loadHandlers)

{

    gBrowser.loadHandlers = [];

 

    gBrowser.addEventListener("load", function (evt)

    {

        gBrowser.loadHandlers.forEach(function (h) { h(evt); });

    }, true);

 

}

 

gBrowser.loadHandlers.pop();

gBrowser.loadHandlers.push(function (evt)

{

    for(s of scripts)

    {

        if (s.testPage(evt.target))

        {

            s.func();

        }

    }

 

});

В коде использована пара новшеств, которые, возможно, не всем понятны, так что вот справка по ним
for...of - JavaScript | MDN
Стрелочные функции - JavaScript | MDN


Здесь доступ к объекту document загружаемой страницы можно получить через evt.target, к объекту window - evt.target.defaultView.

Некоторые из скриптов могут неожиданно запуститься не на тех страницах, для которых они предназначены, это происходит из-за того, что на страницах могут находиться виджеты или какие-то другие фреймы этих сайтов. Фрейм загружается, скрипт выполняется, алерт выскакивает вроде бы на другой странице. Если скрипт изменяет целевую страницу, то это не страшно, поскольку в нем будет использоваться правильная ссылка на документ, но вот алерт может выскочить и не там где надо. Так что для тех случаев, когда это критично надо проверять, является ли загружаемая страница документом верхнего уровня или фреймом внутри другого документа. Для этого можно воспользоваться свойством Window.top - Web APIs | MDN.

Уж коль скоро начал я со сравнения с GreaseMonkey, думаю излишне объяснять, насколько более широкие возможности открываются, учитывая, что это chrome-код. Изменим немного код
Кликните здесь для просмотра всего текста
Код javascript Выделить

/*Initialization Code*/

Components.utils.import("resource://gre/modules/MatchPattern.jsm");

Components.utils.import("resource://gre/modules/BrowserUtils.jsm");

function PageScript(func, include, exclude)

{

    this.func = func; this.include = include; this.exclude = exclude;

    this.testPage = function (doc)

    {

        var uri = BrowserUtils.makeURI(doc.location.href);

        var inclMatch = new MatchPattern(this.include);

        var exclMatch = new MatchPattern(this.exclude);

        if (inclMatch.matches(uri) && !exclMatch.matches(uri)) return true;

        return false;

    }

}

 

var scripts = [

    new PageScript((evt) => alert("Это гугл"), ["*://www.google.ru/*"], []),

    new PageScript((evt) => alert("Это вконтактик"), ["*://vk.com/*"], []),

    new PageScript(function (evt)

    {

        var selectCode = Array.prototype.filter.call(evt.target.querySelectorAll("a"), a => a.textContent == "Выделить код");

        selectCode.forEach(a => a.addEventListener("click", e => gClipboard.write(e.target.parentElement.parentElement.parentElement.parentElement.querySelector("td.de1 pre.de1").textContent), false));

    }, ["*://www.cyberforum.ru/*"])

];

 

 

if (!gBrowser.loadHandlers)

{

    gBrowser.loadHandlers = [];

 

    gBrowser.addEventListener("load", function (evt)

    {

        gBrowser.loadHandlers.forEach(function (h) { h(evt); });

    }, true);

 

}

 

gBrowser.loadHandlers.pop();

gBrowser.loadHandlers.push(function (evt)

{

    for(s of scripts)

    {

        if (s.testPage(evt.target) && evt.target === evt.target.defaultView.top.document)

        {

            s.func(evt);

        }

    }

 

});



Во-первых, здесь уже выполняется проверка документа, является ли он документом верхнего уровня.
Код javascript Выделить

       if (s.testPage(evt.target) && evt.target === evt.target.defaultView.top.document)

 

Во-вторых, функциям, выполняющимся для страниц передается объект события, чтобы можно было получить доступ к документу. В-третьих, для киберфорума используется скрипт, который ищет на страницах ссылки с текстом "Выделить код" и подвешивает его событию click обработчик, который будет копировать выделяемый код в буфер обмена. С теми правами, которые предоставляет веб-страница такое не сделаешь. Кроме того, в приведенной статье о перехвате загрузки страницы есть и более интересные варианты, позволяющие изменять запрос к серверу и ответ.

Работа с закладками



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

var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Components.interfaces.nsINavBookmarksService);

var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);

/**

Закомментированная версия следующей строки создает букмарклет в точности из кода в буфере обмена,

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

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

чтобы код уже был обернут в (function(){})()

*/

//var uri = ios.newURI("javascript:" + encodeURI(gClipboard.read()), null, null);

var uri = ios.newURI("javascript:(function(){" + encodeURI(gClipboard.read()) + "})();", null, null);

var newBkmkId = bmsvc.insertBookmark(bmsvc.toolbarFolder, uri, bmsvc.DEFAULT_INDEX, prompt("Код javascript из буфера обмена будет добавлен в закладки. Введите имя закладки."));

 

Здесь при нажатии кнопки появляется окошко, предлагающее ввести имя закладки, но можно создавать имя автоматически, будет еще проще.
Подробно все описано здесь
Манипулирование Закладками используя Службу | MDN

С другой стороны может появиться необходимость получить какие-то данные из закладки и выполнить какие-то действия над ними. Ну например скопировать закладку как BB-код для вставки ссылки на форуме или, опять-таки, отредактировать букмарклет, скопировав его код в буфер обмена. Для этого удобно использовать контекстное меню закладок. Контекстное меню закладок имеет id="placesContext". Получить ссылку на элемент, представляющий закладку через контекстное меню - нетрудно, раньше подобный пример приводился, когда рассматривалась работа с контекстным меню области содержимого. Но проблема в том, что triggerNode контекстного меню закладки - это обычный элемент menuitem и его стандартные свойства не содержат сведений о закладке. Зато у него есть дополнительное свойство _placesNode - объект реализующий интерфейс nsINavHistoryResultNode - Mozilla | MDN, а вот из него мы уже можем получить всю интересующую нас информацию.

В качестве примере приведу оверлей контекстного меню закладок, для копирования закладки как BB-кода и кода букмарклета
Кликните здесь для просмотра всего текста
Код xml Выделить

<?xml version="1.0" encoding="utf-8" ?>

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <script type="application/javascript">

    <![CDATA[

function copyAsBB(src)

{

    var node = src.parentElement.triggerNode;

    var pnode = node._placesNode;

    var s = "";

    gClipboard.write("[URL=\"" + pnode.uri + "\"]" + pnode.title + "[/URL]");

}

 

function copyBookmarkletCode(src)

{

    var node = src.parentElement.triggerNode;

    var pnode = node._placesNode;

    var s = "";

    if (pnode.uri.startsWith("javascript:"))

    {

        gClipboard.write(decodeURIComponent(pnode.uri.substring(11)));

    }

}

]]>

  </script>

  <popupset id="mainPopupSet">

    <menupopup id="placesContext">

      <menuitem label="Копировать как BB-код" onclick="copyAsBB(this);" id="copyAsBB"/>

      <menuitem label="Копировать код букмарклета" onclick="copyBookmarkletCode(this);" id="copyBookmarkletCode"/>

    </menupopup>

  </popupset>

</overlay>

 

Конечно, меню, работающее с букмарклетами лучше сделать видимым только для них, но код был приведен для примера, а как сделать меню видимым только там где надо - я уже написал раньше.

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

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