If you have long wanted to make the Internet a little better (
for yourself ), but everything was once
lazy , then today is your happy day. For 21 steps, I will show you how to achieve this cherished goal.
Free bonus! In addition to the internet, Google search is also doing better. Maybe Google will appreciate the idea and offer a hand and a heart (if anything, I agree, I will even give up my studies for the sake of it).
</ joke>
')
In this article, I will talk about how to make a plugin for the Chrome browser, FireFox and MS Edge using the example of my own experience in running a rake.
The plugin will allow you to easily implement your own JavaScript / CSS on any pages viewed in the browser. Those. A sort of browser extensions API version of lite.
In early spring, we were given a task for a term paper on the subject of “Internet technology”.
As it seemed to me, I chose the easiest of the suggested topics: “User interface and navigation in modern browsers”. What I later regretted about later, but alas, it was already too late.
TL; DR
If someone does not want to read about all 21 steps, then you can do with five.
The plugin code can be downloaded from
GitHub .
And after that:
- Open chrome: // extensions in Chrome browser
- Put a tick in front of "Developer mode"
- Click on “Load unpacked extensions” and select the directory where you saved the source code.
- After that, for the “CustomActions” plugin, select “options”
- On the form of options, click "Demo config" and "Save"

That's all, you can use, for example open google.com or Habr, correct the configuration or play around with the scripts.
Next comes the expanded version, with lyrical digressions.If you believe the research of British scientists, then the spherical Internet user, fleeing from the amount of information they want to dump on him, creates for himself a cozy Internet world from 5-10 sites that he visits more or less constantly, and he goes back to the “big” Internet by chance.
It would not be sad to admit, but looking in the mirror, I saw there this very spherical (granny, if you read this article, please do only diet cakes, but my geometry suffers) of the user.
But it was even sadder to realize that my 9 and a half sites are not perfect. Therefore, in the framework of the course work, it was decided to make them (well, along with the rest of the Internet) better.
In most cases, this required writing only 3-4 lines in JavaScript and / or 2-3 in CSS.
After the initial research and consultations, it became clear that browser vendors provide such an opportunity through functionality extensions.
We define our Wishlist:
- When opening a site, a custom script associated with this site is introduced.
- Custom script configured on preference page
- Add a button to the browser toolbar to quickly recall the configuration
Terms of reference defined, starting to code.
Step 1.I read
getstarted to enlightenment, or blue, depending on which side to look.
Step 2.I define the project structure
options.html
options.js
popup.html
popup.js
background.js
manifest.json
icon.png
images
images\icon128.png
images\icon16.png
images\icon48.png
Step 3.I create the manifesto.
{ "name": "CustomActions", "description": "plugin for CustomActions", "version": "1.0", "background" : { "scripts": ["background.js"] }, "icons": { "128": "images/icon128.png", "16": "images/icon16.png", "48": "images/icon48.png" }, "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", "permissions": [ "webRequest", "tabs", "activeTab", "http://*/*", "https://*/*", "storage", "unlimitedStorage", "contextMenus", "<all_urls>" ], "browser_action": { "default_title": "Custom Actions Injection plugin", "default_icon": "icon.png", "default_popup": "popup.html" }, "commands": { "cmd-exec-1": { "suggested_key": { "default": "Ctrl+Q" }, "description": "Custom Action #1" }, "cmd-exec-2": { "suggested_key": { "default": "Ctrl+B" }, "description": "Custom Action #2" }, "cmd-exec-3": { "suggested_key": { "default": "Ctrl+Y" }, "description": "Custom Action #3" } }, "options_page": "options.html", "manifest_version": 2 }
Step 4.I am working on the options.html form.
No wonder visited the previous session. Knowledge of knockout.js from the course “Design of Web pages 2.0” was useful
I add the knockout-3.4.1.js and knockout.mapping-latest.js files to the project structure.
Nothing works. I dig. Does not help. Roy. Naryl that chrome extensions does not like knockout.
Forcing love, update manifest.
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"
Step 5.After pain and suffering in an attempt to realize the user interface comes insight.
We pull everything from chrome: // settings
We got on the shoulders of a giant and it went:
Step 6.Need to save data.
Read about
chrome.storageThe dilemma of choosing between chrome.storage.sync and chrome.storage.local
chrome.storage.sync is more attractive, but hard limits.
Another insight - I have the same test project, so local is our everything.
I rule the manifest so that there are no problems with limits.
And I use chrome.storage.local.set to save the configuration.
chrome.storage.local.set(items, function () { self.status('Items saved.'); setTimeout(function () { self.status(''); }, 750); });
Step 7.I understand that the saved configuration should be reloaded in background.js, but how ???
Yeah, sweet couple chrome.runtime.sendMessage:
chrome.storage.local.set(items, function () { self.status('Items saved.'); setTimeout(function () { self.status(''); }, 750); chrome.runtime.sendMessage({ command: 'refreshConfig' }); });
and chrome.runtime.onMessage:
chrome.runtime.onMessage.addListener( function (request, sender, sendResponse) { onCommand(request.command); });
Step 8.I open popup.js. I'm closing. I open it again. And so 7 times. Nothing is clear.
Is this me alone or where? A long-awaited morning is coming and here I stumbled over the
samples - thank you, invisible Google workers.
Raking rubble precious knowledge. In the end, I find the right example and use the magic ACV combination (Ctrl-A / Ctrl-C / Ctrl-V). 13 hours later - it's done.
Step 9.It's time to get down to the main part, intercept and deploy.
Interceptor - chrome.tabs.onUpdated:
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { var url = ''; if (changeInfo && changeInfo.url) url = changeInfo.url.toLowerCase(); else if (tab && tab.url) url = tab.url.toLowerCase();
and implementer - chrome.tabs.executeScript:
if (item.sourceType == 'InjectCSS') chrome.tabs.insertCSS(item.output == 'Owner tab' ? tabId : null, { code: item.data }); else chrome.tabs.executeScript(item.output == 'Owner tab' ? tabId : null, { code: item.source });
Everything went easier than it could, but still longer than desired.
Discovered a new, amazing world called
regular expressions . It turns out difficult can be done easily.
Step 10.All is ready. You can go for honestly earned five ...
… not so fast. Instead, the five brought a bunch of comments and improvements.
Step 11.Let's start with the easiest, in popup.js I add support for the function "add current site"
No so easy as it seems, you need to transfer data from popup.js to options.js.
I spent 3 hours trying to catch the messages, save them to the extension storage and read them back, but in the end I decided that the smart one wouldn't go uphill, he would bypass it.
Query string is our answer to this grief.
Step 12.The next function is more difficult, add context menu support.
Examples save again. Everything is simpler than it could be - chrome.contextMenus.create:
chrome.contextMenus.create({ id: item.id, contexts: ["page", "frame", "selection"], title: item.name, onclick: function (info, tab) { onCommand(info.menuItemId); } });
Step 13.I do not believe in omens, but it was not a happy step.
It is necessary to make support for the call through hot keys
I do everything as in the examples, I correct the manifest:
"commands": { "cmd-exec-1": { "suggested_key": { "default": "Ctrl+Q" }, "description": "Custom Action #1" } }
and use chrome.commands.onCommand:
chrome.commands.onCommand.addListener(onCommand);
Does not work. After two days and nights and medical intervention, scrolling down the settings page I find an inconspicuous link to the desired settings.

That's where the dog has buried. Itself to blame, too many examples instructed, here's a reference and ran off to the next screen.
Step 14.Our head has a teacher of English. Requires translate interface. Have to learn English.
Technology comes to the rescue. Google translate vs English teacher - 1-0 in favor of life-giving Google!
Step 15.Again we go for the top five, but in our heart we already agree to a four.
I come back with a new word refactoring (before that I knew only the word FAKtoring. I spoke it especially often at the beginning of the project).
And an additional requirement to make the program friendly to the user, to create a demonstration configuration and examples.
It seems that the head of the department, in addition to his English wife, has a mistress from the UX / UI department.
But remembering the good people from the Google examples, almost did not resist.
Step 16.Refactoring is complete. It's amazing how many bugs are hidden under the cover of the original version of the code, we should better remember this word.
Step 17.Add an example for lorem ipsum. Who advised this? Latin teacher?
Requirement: when you press a key combination, fill in the fields on the form with lorem sentences.
I did, it seems to work.
var loremDemoData = { names: [ { firstName: "Victoria", lastName: "Veit", email: "Victoria.Veit@noreply.ru" }, { firstName: "Gisele", lastName: "Gillard", email: "Gisele.Gillard@noreply.ru" }, { firstName: "Edmund", lastName: "Edelson", email: "Edmund.Edelson@noreply.ru" }, { firstName: "Joey", lastName: "Janelle", email: "Joey.Janelle@noreply.ru" } ], lorem: [ "Orem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet purus condimentum, porta nulla sed, consequat felis. Phasellus quis condimentum odio. Maecenas scelerisque vehicula leo, sit amet tristique tellus molestie sed. Aenean lacus lorem, feugiat semper imperdiet a, vehicula ac orci. Pellentesque ac nisi commodo, pellentesque lorem quis, fringilla tellus. Fusce bibendum erat sit amet libero maximus rutrum. Integer dictum nibh sodales efficitur congue. Mauris nulla libero, hendrerit eget dictum nec, aliquam eu mi. Donec ipsum nisi, bibendum et consequat eu, imperdiet eget nisl. Duis tincidunt nibh et nibh tempor, quis mattis mi vulputate.", "Suspendisse quis eleifend lectus. Sed nec vehicula elit. Praesent ac sollicitudin diam. Nam at venenatis lectus. Fusce condimentum tortor nec augue vestibulum tempus. Nullam faucibus vehicula lorem, et mollis justo dapibus a. Proin sagittis velit in lectus vehicula, id eleifend urna hendrerit. Integer rhoncus dui sed enim sollicitudin, a finibus magna fermentum.", "Fusce at urna vitae magna semper scelerisque id volutpat tellus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed ut elit nisl. Duis sit amet ante accumsan nibh ultricies pharetra at vitae purus. Donec a felis eget ipsum euismod tempus. Donec elementum vel tortor vel efficitur. Nunc tristique, magna hendrerit sagittis placerat, odio sem commodo ligula, eu aliquam arcu elit sit amet diam. Etiam ultrices vehicula auctor." ], loremShort: [ "Morbi nec sollicitudin augue.", "Suspendisse sagittis fringilla aliquam.", "Curabitur malesuada dolor.", "Praesent quis lacus neque. Duis vitae vehicula felis" ] };
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min)) + min; } var name = data.names[getRandomInt(0, data.names.length)]; var hadEmail = false; var t = document.querySelectorAll('input[type=text], textarea'); for (var i = 0, l = t.length; i < l; i++) { var e = t[i]; var ro = e.getAttribute('readonly'); if (e.disabled || ro === '' || ro === 'true' || ro == '1') continue; var loremTxt = data.lorem[getRandomInt(0, data.lorem.length)]; var loremShort = data.loremShort[getRandomInt(0, data.loremShort.length)]; var na = ('' + e.name).toLowerCase(); var ia = ('' + e.id).toLowerCase(); if (na == 'firstname' || ia == 'firstname' || na == 'fname' || ia == 'fname') e.value = name.firstName; else if (na == 'lastname' || ia == 'lastname' || na == 'lname' || ia == 'lname') e.value = name.lastName; else if (!hadEmail && (na.indexOf('email') >= 0 || ia.indexOf('email') >= 0)) { e.value = name.email; hadEmail = true; } else { e.value = (e.tagName == 'TEXTAREA' ? loremTxt : loremShort); } }
Step 18.The following example for the site habrahabr.ru
Requirement: when opening the site, hide articles for hubs and companies listed in the configuration. For articles written on behalf of the company, make them seemingly different from articles written by independent authors.
In the process, I learned about the existence of Habr and Hiktaims. I would have known about them before, everything would have been at least twice as fast. Spent three days reading without stopping. I have been overwhelmed by articles only in the last year, there are still many interesting things ahead!
At the next reception, my psychotherapist advised to write an article about his torment. He says that this is an important step in the process of treating psychological trauma caused by the work on this coursework.
On the proposal to replace the creation of the article by taking the correct pills, he refused. In the end, agreed to the tablet, but with the condition of writing the article. I sit. Writing.
It turned out that the article is easier to write than the code. Maybe the Chukchi is not a reader, but a writer?
{ "showCompanies": [ "yandex", "mosigra" ], "hideCompanies": [ "hashflare" ], "hideHubs": [ "lib" ] }
function hideParent(el) { if (el.classList && el.classList.contains('post_teaser')) el.style.display = 'none'; else if (el.parentElement) hideParent(el.parentElement); } function sanitizeParent(el) { if (el.classList && el.classList.contains('post_teaser')) { el.querySelectorAll('img').forEach(function (img) { img.style.display = 'none'; }); el.querySelectorAll('.post__body_crop').forEach(function (chld) { chld.style.maxHeight = '4em'; chld.style.overflow = 'hidden'; el.addEventListener('mouseover', function () { chld.style.maxHeight = "inherit"; chl d.querySelectorAll('img').forEach(function (img) { img.style.display = 'block'; }); }, false); el.addEventListener('mouseout', function () { chld.style.maxHeight = "4em"; chld.querySelectorAll('img').forEach(function (img) { img.style.display = 'none'; }); }, false); }); el.querySelectorAll('.post__title a').forEach(function (titl) { titl.style.color = '#707040'; }); } else if (el.parentElement) sanitizeParent(el.parentElement); } document.querySelectorAll('a[href*="https://geektimes.ru/hub/"]').forEach(function (el) { var hub = el.getAttribute('href').replace(/^.*\.ru\/hub\//, '').replace(/\/.*$/, ''); if (data && data.hideHubs && data.hideHubs.indexOf(hub) >= 0) hideParent(el); }); document.querySelectorAll('a[href*="https://geektimes.ru/company/"], a[href*="https://habrahabr.ru/company/"]').forEach(function (el) { var company = el.getAttribute('href').replace(/^.*\.ru\/company\//, '').replace(/\/.*$/, ''); if (data) { if (data.hideCompanies && data.hideCompanies.indexOf(company) >= 0) { hideParent(el); return; } else if (data.showCompanies && data.showCompanies.indexOf(company) >= 0) return; } sanitizeParent(el); });
Step 19.Wandering around the Internet, I saw an interesting idea and decided to implement it as an example for google search.
Maybe he will appreciate the initiative and stop quibbling over trifles.
Functionality: when opening Google, we show a hint with a list of the most frequently used words and sites for quick search.
After the example for Harb, this is a couple of trivia.
var googleDemoData = [ { "keywords": "python", "title": "python" }, { "keywords": "javascript", "title": "javascript" }, { "keywords": "php", "title": "php" }, { "keywords": "mysql", "title": "mysql" }, { "keywords": "site:stackoverflow.com", "title": "at stackoverflow.com" }, { "keywords": "site:developer.mozilla.org", "title": "at developer.mozilla.org" }, { "keywords": "site:developer.chrome.com", "title": "at developer.chrome.com" }, { "keywords": "site:habrahabr.ru", "title": "at habrahabr.ru" } ];
function ggSetTimeRange() { var elemId = this.getAttribute('data-range'); var timeLimit = document.querySelector('#' + elemId + ' a'); if (timeLimit) timeLimit.click(); } function ggReplaceAndSearch() { var kw = this.getAttribute('data-search'); if (document.location.href.indexOf('chrome-search://') == 0 || document.location.href.indexOf('https://www.google.com/_/chrome/newtab?') == 0) { document.location.href = "https://www.google.com/search?q=" + encodeURIComponent(kw); return; } var inputText = document.querySelector('input[name="q"]'); if (inputText) { setTimeout(function () { var keyword = '' + inputText.value; if (kw.indexOf('site:') >= 0 && keyword.indexOf('site:') >= 0) { keyword = keyword.replace(/ *site:[^ ]+/, ''); } else if (keyword.indexOf(kw) >= 0) return; kw = ' ' + kw; if (kw.indexOf('site:') >= 0) { inputText.value = keyword + ' ' + kw; setTimeout(function () { var btn = document.querySelector('form[action="/search"]'); if (btn) { btn.submit(); } else { btn = document.querySelector('button[name="btnK"]'); if (btn) { btn.click(); } } }, 100); } else { inputText.value = kw + ' ' + keyword; var strLength = ('' + inputText.value).length; inputText.setSelectionRange(strLength, strLength); } }, 200); setTimeout(function () { inputText.focus(); }, 100); }; }; var ggHelper = document.getElementById('ggHelper'); if (!ggHelper) { var helperHtml = '<div id="ggHelper" style="position: fixed; ' + 'box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); ' + 'background-color: #f0f0f0; border-radius: 2px; flex: 1; ' + 'padding: 0.7em 1em 0.3em 1em; right: 1em; top: 12em; width: 13em; height: ' + (data.length + 4) + 'em; ' + 'font-size: 13px;">' + '<ul style="list-style-type: none; margin: 0; padding: 0;">'; data.forEach(function(dataItem ) { helperHtml += '<li style="text-align: left; cursor: pointer;"><a href="javascript: return false;" data-search="' + dataItem.keywords + '" class="gg-keyword">' + dataItem.title + '</a></li>'; }); helperHtml += '<li style="margin: 0.5em;"><hr size="1" style="height: 1px; border-color: #e0e0e0;"></li>'; helperHtml += '<li style="cursor: pointer;">' + '<a href="javascript: return false;" data-range="qdr_w" class="gg-range">week</a> :: ' + '<a href="javascript: return false;" data-range="qdr_m" class="gg-range">month</a> :: ' + '<a href="javascript: return false;" data-range="qdr_y" class="gg-range">year</a> :: ' + '<a href="javascript: return false;" data-range="qdr_" class="gg-range">any</a></li>'; helperHtml += '</ul></div>'; var bodyTag = document.querySelector('body'); if (bodyTag) { var e = document.createElement('div'); e.innerHTML = helperHtml; bodyTag.appendChild(e.firstChild); document.querySelectorAll('#ggHelper .gg-keyword').forEach(function (el) { el.addEventListener('click', ggReplaceAndSearch); }); document.querySelectorAll('#ggHelper .gg-range').forEach(function (el) { el.addEventListener('click', ggSetTimeRange); }); } }
Step 20.According to the precepts of Habr, I sent the code for review to a group of experts. Verdict - rewrite everything. Crying, already sobbed.
After the correspondence, I found at least 6 more bugs. Review is power! It is necessary to remember this word.
It was nice to look at the code, I even began to understand what was written there and why.
Step 21All is ready.
I follow the five, counting on the four, but I agree on the three. No more strength.
Three weeks of life thrown into the fight against modern technology.
Wish good luck!
Ps:. In the end, I would like to express heartfelt thanks to the creators and users of the site stackoverflow.com
I can not imagine how the work looked like before its creation. And before the advent of the web (the glory of TNB, I was born after its creation), the profession of a programmer was probably one of the most depressed and with the highest suicide rate per unit area.
Even now, sometimes you still want to tear out all the vegetation on your head, and sometimes even worse.
Pps: Criticism is welcome. Any.
Update 1 . I made marks, I'm 4 ++.
One ball subtracted with the wording: “Too much plagiarism with StackOverflow”, but added a plus for the article on Habré. On questions about the second plus, mysteriously grins in his mustache.
Update 2 . Two good news.
The first . The network technology teacher promised to put the exam automatically if I added the “share with a friend” function. A group of extras spoke unfamiliar words.
I sit, I smoke node.js, the campaign will have to start not only smoking, but also drinking. No bottle to figure it out.
Head in the clouds. On Heroku I need it? It may be better to hand over as usual - cribs and wonders. And then health is more expensive.
The second . Zablab promised offset course in the next year, if I add three additional features of the voting results in the comments.
So if there is an idea how to improve the functionality - offer, vote. You can new demo function, template or UI tricky or something.
Sound your wildest fantasies.
Third . If so, maybe a diploma on something ride?
Update 3 . Hooray! The first salary is in dollars. In the work & travel program I study English in the kitchen of the local McDonalds.
The first learned idiom: "mania on grass." I feel that by the end of the summer I will be fluent.
I multiply dollars by 60, the number of zeros competes with the best offers for programmers in our regional center. And in our district center, we need to enter the coefficient N.
No N is small, we must take M.
A group of experts, silently envying from afar, but hinting that upon arrival we will buzz. Required to bring a tune. But here those who don’t have 21, do not sell musical instruments, I don’t know what to do.
I think a lot.
Maybe my calling is cooking? I'll write out my grandmother, open the cheburechnuyu, let’s trade in diet pies. And then on a free McDonald's diet, I will definitely stay very spherical user.
Update 4 . Instead of legal rest, I spent two sleepless nights programming (and as a result for those two days I spent the entire first salary on starbucks), but still converted the plug-in to FireFox and MS Edge.
FireFox almost did not resist. You can download the
code or
ready-made plugin from GitHub.
To allow downloading from source code, you need to use web-ext or through settings:

To download the finished plug-in:

And MS Edge rested against all limbs, barely fought. I wanted to quit, but the cherished course machine still did not allow weaknesses to take precedence over strengths.
Words from the documentation: Microsoft Edge extensions currently only support Default Policy Restrictions: script-src 'self'; object-src 'self', put an end to using knockout on the form of options. I had to translate into angular. Also not without incident.
But the miracle happened and in the end everything happened. The code can be picked up from
GitHub .
It is necessary to allow development in about: flags

and after that it can be downloaded for local use:

Have I already thanked StackOverflow? Once again I take off my hat to this treasure trove of knowledge.