
This article is devoted to writing a simple extension for the Opera browser. Our extension will be primitive, since all its functionality will be in user-JS for habrahabr.ru. The comments feed is equipped with a block that displays the number of new comments in the topic and a button allowing it to be updated. Let's add arrows to navigate through new comments.

Where do we start?
- Create a new directory for the extension files.
- We will create a config.xml file in it.
XML Content:
<?xml version="1.0" encoding="UTF-8"?> <widget xmlns="http://www.w3.org/ns/widgets" id="http://faiwer.ru" version="0.9a" defaultlocale="en"> <name xml:lang="en">HabrCommentSwitcher</name> <description xml:lang="en">Habrahabr. New comment switcher</description> <description xml:lang="ru">Habrahabr. </description> <author href="faiwer.ru" email="faiwer@gmail.com">Faiwer</author> <icon src="icons/64x64.png"/> <icon src="icons/48x48.png"/> <icon src="icons/32x32.png"/> </widget>
Since this article is a tutorial, I will not describe each item in detail, the
documentation will cope better with it. Let's stop on the most important thing:
- <name> The name of our extension </ name>. Don't make it too long
- <description> Short description </ description>. Enough and a pair of lines
- <icon />. Icons are used on the extensions page, on the repository site (if your extension is accepted there), and in the button, which will not be in this extension. It is desirable to make a separate directory in order not to create a mess
Available options are much more. But for starters, that's enough. I think you noticed that in config.xml you can specify several language versions at once for the name and description.
')
As a final touch, you need to create index.html. It is needed for the operation of the off-screen script, which will be launched with the start of the browser, and will not be tied to any of the tabs. We do not need it, but without it, Opera will not allow us to "podobezhit". The file can be left blank. Now with the help of drag-n-drop we drag our config.xml into the browser. If everything went well, a page will open with a list of installed extensions, and ours will be there from above, in the “Developer Mode” section.
Userjs
In the beginning, you should decide what the future expansion should do:
- wait until the end of the page loading and the appearance of the desired block (hereinafter I will call it slider).
- place our arrow buttons in it.
- place the necessary CSS for arrows and highlight the current comment.
- animate the arrows - they have to move the scrolling page on the new comments.
For all this, the 1st file is enough to be executed for each habrahabr page. Those.
UserJS is perfect for us. But if Chrome can convert UserJS to extensions itself, and Firefox needs
Greasemonkey for this, then in the case of Opera we can arrange it as an extension or install it manually (F12 - Settings for the site - Scripts).
Create an includes directory, Opera will search for “embedded” scripts there. In it, we will create the file habr_comment_switcher.js (here you can choose any name). In the beginning of the file we place:
This is not just js-comments, this is a special markup for UserJS, which in our case explains to the opera that this embedded file should be launched only on habrahabr.ru.
Javascript
In addition to this file, we could embed another library like jQuery or Prototype. But I strictly do not recommend doing so. This kind of library is very weighty, and since they will be loaded not only for each tab, but also for each iframe, of which there are many on the page, 5-10 such extensions can cause brakes. Considering that our tasks are very modest, we are not losing much.
UPD 2. Thanks,
kns . If the page already uses 1 of the popular libraries, then we can use it. Read more about this
here . In the case of Opera, it will look something like this:
var $ = window.jQuery;
Let's start writing code. For a start, for convenience, we will put everything in the anonymous function. Because our javascript environment is isolated from the javascript environment of the site, it is not necessary to do this, but in my opinion, this has long been a good tone rule:
+function( w ) { }( window );
I do not know about you, but I used to work within a specific object, and therefore we define it:
var Engine = function(){ this._init(); } Engine.prototype = { _init: function() { } }
Now you need to issue the conditions for its creation:
if( w.location.href.indexOf( 'habrahabr.ru' ) > 0 ) { var engine = false; d.addEventListener( 'DOMContentLoaded', function() { setTimeout( function(){ engine = new Engine(); }, 1500 ); }, false ); }
The check for the page address is not caused by logical arguments, but by paranoia. The fact is that I came across a couple of times on the network that Opera sometimes does not cope with the rules for UserJS. Our object will be launched after all the DOM-tree of the page is built + 1.5 sec. Why 1.5 seconds? The fact is that the slider does not appear immediately, so we will wait for it. This can be realized more gracefully, but so far it will come down.
Job extension
Now Opera is not the most high-tech browser, but still its capabilities are far ahead of IE6,7,8. Therefore, we can take advantage of such things that we would not use in ordinary web programming. Simplify yourself a job:
var d = w.document, $ = d.querySelector.bind( d ), $$ = d.querySelectorAll.bind( d )
The querySelector and querySelectorAll methods allow you to find DOM objects by CSS selectors. This approach is probably familiar to you from the experience of using jQuery. In our case, the $ function will search for one element that satisfies the query, and $$ list.
What are we on the list? It doesn’t matter, let's introduce the CSS we need to the page:
_cssInject: function() { var style = this._createElem( this.elem.style_inject ), text = ''; for( var i = 0, n = this.css.length; i < n; ++ i ) { text += this.css[ i ]; } style.innerHTML = text; d.head.appendChild( style ); }
Here we create a new DOM object <style /> and set the necessary CSS code as content. Because The page is already ready. Document.head is available to us, where we will place our tag. Now about the _createElem function:
_createElem: function( data ) { var item = d.createElement( data.tagName ); if( data.attr ) { for( var rule in data.attr ) { item.setAttribute( rule, data.attr[ rule ] ); } } return item; },
You can organize work with settings as you like, like this:
_initConst: function() { this._extend( this, { css: [ '.__hcsc_button { border-top: 1px solid white; line-height: 22px; height: 22px; ' + 'cursor: pointer; }', '.__hcsc_button:hover { color: white; }', '.info.__hcsc_active { outline: 2px solid #222; }', ], elem: { style_inject: { tagName: 'style', attr: { id: '__habr_comment_switcher_css' } } } } ); }, _extend: function( object, extend ) { for( var name in extend ) if( extend.hasOwnProperty( name ) ) { object[ name ] = extend[ name ]; } },
Let us turn to the main logic. We need to find the slider and add two buttons to it:
_prepareSlider: function() { var slider = $( this.s.slider ); if( !slider ) { return; } this.up_button = this._createElem( this.elem.button ); this.up_button.innerHTML = 'â–˛'; slider.appendChild( this.up_button ); this.down_button = this._createElem( this.elem.button ); this.down_button.innerHTML = 'â–Ľ'; slider.appendChild( this.down_button ); },
Arrows can be set by text. Now we need to revive these buttons:
_observe: function() { this.up_button.addEventListener( 'click', this._slideClick.bind( this, -1 ), false ); this.down_button.addEventListener( 'click', this._slideClick.bind( this, +1 ), false ); },
And finally, the long-awaited listalka:
_checkItems: function() { var items = $$( this.s.info_panel ); if( !this.items || !this.items.length || !items.length || ( this.items[ 0 ] !== items[ 0 ] ) ) { this.position = -1; this.items = items; } return this.items; }, _slideClick: function( diff ) { if( this.current ) { this.current.classList.remove( this.c.active ); } if( !this._checkItems().length ) { return; } this.position += diff; if( this.position < 0 ) { this.position = this.items.length - 1; } else if( this.position >= this.items.length ) { this.position = 0; } this.current = this.items[ this.position ]; this.current.scrollIntoView( true ); this.current.classList.add( this.c.active ); }
Its logic is simple. We are looking for all new comments on the CSS selector specified in this.s.info_panel (".comment_item> .info.is_new"). He finds us all the block headings for new comments. Then, depending on which button we clicked, move the page scroll to the desired comment using scrollIntoView. To make this event more visual, we add a class to it, for which we have defined a dark frame CSS (outline) above.
Given that the update button is available, and our list of new comments may become outdated, the extension checks the old list with the current one each time, and if they are different, the position counter resets.
Separately, I would like to focus on the functions of working with the DOM object class. No need to manually parse the item.className line, since The following methods are available:
this.current.classList.add( 'my_class' ); this.current.classList.remove( 'my_class' );
A bit about “debag”
Let's start with the fact that we have a tool like
Dragonfly (dragonfly), which is called via ctrl + shift + i (or the right mouse button - “inspect the object”). In it on the “Scripts” tab, we can find our habr_comment_switcher.js in the drop-down list. Now we have breakpoints and “tracing” (F8, F10, F11). The console is also available to us, but in order for it to work in the same js environment as our script, we need to first look at its number in the list of scripts.
You can view errors by clicking the "open error console" button on the installed extensions page. The console object for extensions does not work. To update an extension, we need to close the dragonfly, on the extensions page, click “refresh”, open the dragonfly on the required tab and click “refresh” in the browser. In general, the experience of working with a dragonfly extension is the worst. Especially after the experience of developing an extension for Chrome.
Final touch
It seems everything works, so it's time to pack the extension. To do this, compress the
contents of the extension folder in a zip-archive, and change the file extension to oex. All expansion is ready. You can use. If Opera swears at the fact that the extension is damaged, check - maybe you have compressed not the contents of the folder, but her own. Also check for config.xml and index.html files.

Epilogue
HOORAY! Our extension is ready, in alpha version. It can be improved, add support for Chrome and Greasemonkey (although I'm not sure that it will not take off like that), add a settings page (for example, to set colors or change CSS selectors).
You can see the finished extension
here .
The first time I use git, excuse me if something goes wrong.