Mozilla Web Extensions: Insert UI component into page - firefox-addon-webextensions

I am writing a Web Extension for Firefox that needs to insert a substantial amount of additional functionality inside pages retrieved using certain URLs.
I was able to quickly create a content script that is called whenever a certain page is opened thanks to the tutorial at Mozilla's web site, but now I'm stuck on actually inserting html fragment into the page.
I've been at it for hours but to no avail. Here's what I've considered and tried:
iframe didn't work as apparently some security policy doesn't allow using iframes pointing to local resources and the last comment here even tells that I'm supposed to use panel instead of iframe
Using Panel doesn't work for me for two reasons:
I couldn't find a way to open a Panel using my own custom code (the sample by the link above fails with ReferenceError: require is not defined)
I'm guessing that I can open a panel in a Web Extension only by using a bowserAction but that would put the button on the toolbar while I need it in the page itself
According to documentation I can have only one Panel instance open for the whole browser and it would automatically close upon interacting with any other browser element
Lastly I thought about just loading html from a resource file packed into the extension and feeding it into the page using innerHTML but I couldn't find any API to load text from a resource
Just using DOM API doesn't work for me since it would take forever to code creation of all the elements

I can't believe I didn't notice it for so long, but I finally got it all working as I need. While doing that I even came up with an alternative approach, so here goes.
But first here's the main reason why I dismissed all other possible approaches besides using iframe:
I needed the UI elements added by extension to use their own UI styles and wanted to take advantage of modern frameworks (e.g. jQuery and Bootstrap) and I didn't want to run into problems of conflicting CSS and JavsScript later.
And I actually noticed early on that CSS in the page that I'm embedding into do override Bootstrap styles.
Preferably I also didn't want to affect century old markup of the page that I'm embedding into.
Option A - IFRAME with external source file
In the end it turned out that the only thing I was missing is the web_accessible_resources setting in the manifest.json. Once I added the html file used as source for the iframe into that list, it all just started working.
// manifest.json:
{
"manifest_version": 2,
...
"web_accessible_resources": [
"data/my.html"
],
"content_scripts": [
{
"matches": ["*://*"],
"js": ["js/my.js"]
}
]
}
// js/my.js
var addonFrame = document.createElement ("IFRAME");
addonFrame.style = "position: absolute; bottom: 0; right: 0; width: 150px; height: 38px;";
addonFrame.frameBorder = "0";
addonFrame.src = chrome.extension.getURL ("data/my.html");
document.body.appendChild (addonFrame);
Option B - IFRAME with inline HTML in JS
Before I finally got the first approach working, my experimentation led me to another working approach - inserting HTML into the iframe directly in the content script.
// js/my.js
var addonFrame = document.createElement ("IFRAME");
addonFrame.style = "position: absolute; bottom: 0; right: 0; width: 150px; height: 38px;";
addonFrame.frameBorder = "0";
var addonHtml = "<!DOCTYPE html>\n\
<html>\n\
<head>\n\
<meta charset='UTF-8'>\n\
<title>Title of the document</title>\n\
</head>\n\
<body>\n\
...\n\
</body>\n\
</html>";
addonFrame.src = 'data:text/html;charset=utf-8,' + encodeURI (addonHtml);
document.body.appendChild (addonFrame);
Even though I ended up using option A in the end, let me outline some pros and cons:
Option A is obviously more canon: view (html) is clearly separated from behavior (js) and all files have content appropriate for their type (except for small exception of building iframe element in JS).
So it should be easier to support going forward.
Option A doesn't allow to use inline scripts in the frame (https://developer.chrome.com/extensions/contentSecurityPolicy). This makes prototyping harder but ultimately should be a plus.
For some reason that is still unclear to me, I cannot use # in the inserted html in option B.
Option B makes doing ajax calls from the add-on frame to the original server easier since the frame source is considered to be from the same domain as the original web page.
In option A, I had to use Window.postMessage in the frame in order to ask my content script inserted into the original page to make an ajax request and give me back the response (the second part was especially hard since there's nothing like jQuery or Prototype available there).

Related

How are Javascript widgets made without iFrames?

I have a chat widget that I want to embed it other people's websites. It looks just like Intercom and all the other chat popups. I want to make the chat popup stick to the bottom-right hand corner of the screen regardless of where you scroll. However, when I import the chat app as an iframe and give it position: fixed; bottom: 0px; right: 15px;, the iframe does not go to where I expect it to go.
I realize that iframes are suboptimal for embedded JS widgets, and all the best embedded apps are importing .js files from file storage. After searching online for hours I have yet to find an explanation/tutorial on how to make those JS files that hook onto a and render the widget. How do you even make one of those pure javascript apps, and what are they called? (Not web components I assume, because there have been widgets for a long time).
Sorry if this question is kinda noob. I never knew this was a thing until I tried implementing it myself. Can anyone point me in the right direction on how to get started making JS web widgets? Thank you! (Maybe a ReactJS to VanillaJS converter would be super cool)
A pure Javascript App is called a SPA - Single Page Application - and they have full control over the document (page). But since you ask about embeding a widget, I don't think that is what this question is about (there are tons of info. on the web on SPAs).
I was going to suggest that going forward you do this using Web Components - there are polyfills available today that make this work on nearly all browsers - but since your question mentioned that you wanted to know how it is done without it, I detail below one of my approaches.
When creating a pure JS widget you need to ensure that you are aware that a) you do NOT have control over the global space and b) that it needs to play nice with the the rest of the page. Also, since you are not using Web Components (and are looking for a pure javascript (no libs)), then you also have to initialize the widget "manually" and then insert it to the page at the desired location - as oposed to a declaritive approach where you have an assigned HTML tag name for your widget that you just add to the document and magic happens :)
Let me break it down this way:
Widget Factory
Here is a simple Javascript Widget factory - the create() returns an HTML element with your widget:
const Widget = Object.create({
create(chatId) {
const wdg = document.createElement("div")
wdg.classList.add("chat-box");
wdg.innerHTML = `<h1>Chat: ${ chatId }</h1>`;
// Load your chat data into UI
return wdg;
}
});
To create a new widget (HTML Element) using the above you would:
const myWidgetInstance = Widget.create("chat-12345");
and to insert this widget into the page at a given location (ex. inside of a DIV element with id "chat_box", you would:
document.getElementById("chat_box").appendChild(myWidgetInstance);
So this is the basics of creating a Widget using the native (web) platform :)
Creating a reusable/embeddable Component
One of the key goals when you deliver a reusable and embeddable component is to ensure you don't rely on the global space. So your delivery approach (more like your build process) would package everything together in a JavaScript IIFD which would also create a private scope for all your code.
The other important aspect of these type of singleton reusable/embeddable components is that your styles for the Element needs to ensure they don't "leak" out and impact the remainder of the page (needs to play nice with others). I am not going into detail on this area here. (FYI: this also the area where Web Component really come in handy)
Here is an example of a Chat component that you could add to a page anywhere you would like it to appear. The component is delivered as a <script> tag with all code inside:
<script>(function() {
const Widget = Object.create({
create(chatId) {
const wdg = document.createElement("div");
wdg.classList.add("chat-box");
wdg.innerHTML = `<h1>Chat: ${ chatId }</h1>`;
// Load your chat data into UI
return wdg;
}
});
const myWidgetInstance = Widget.create("chat-12345");
const id = `chat-${ Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) }`;
document.write(`<div id="${ id }"></div>`);
document.getElementById(id).appendChild(myWidgetInstance);
})();</script>
So you could use this in multiple places just by droping in this script tag in the desired locations:
<body>
<div>
<h1>Chat 1</h1>
<script>/* script tag show above */</script>
</div>
...
<div>
<h1>Chat 2</h1>
<script>/* script tag show above */</script>
</div>
</body>
This is just a sample approach of how it could be done. You would have to add more in order to support passing options to each widget (ex. the chat id), defining styles as well other possible improvements that would make the runtime more efficient.
Another approach
You could add your "script" once and wait for the rest of the page to load, then search the document for a "known" set of elements (ex. any element having a CSS Class of chat-box) and then initialize a widget inside of them (jQuery made this approach popular).
Example:
Note how data attributes can be used in DOM elements to store more data specific to your widget.
<div class="chat-box" data-chatid="123"></div>
<script>(function() {
const Widget = Object.create({
create(chatId) {
const wdg = document.createElement("div");
wdg.classList.add("chat-box");
wdg.innerHTML = `<h1>Chat: ${ chatId }</h1>`;
// Load your chat data into UI
return wdg;
}
});
const initWhenReady = () => {
removeEventListener("DOMContentLoaded", initWhenReady);
Array.prototype.forEach.call(document.querySelectorAll(".chat-box"), ele => {
const myWidgetInstance = Widget.create(ele.dataset.chatid);
ele.appendChild(myWidgetInstance);
});
};
addEventListener('DOMContentLoaded', initWhenReady);
})();</script>
Hope this helps.
The best way to create Javascript widget without third-party library is to create Custom Elements.
The following link : Custom Elements v1 is a good introduction to this technology.
See a minimal example below:
class Chat extends HTMLElement {
connectedCallback () {
this.innerHTML = "<textarea>Hello</textarea>"
}
}
customElements.define( "chat-widget", Chat )
<chat-widget>
</chat-widget>

videojs doesn't load when change state with ui-router

I am working on a project with videojs that must work on Firefox and IE 11, and is built with a angular-ui-router. One of the states has a video player, and on the first time loading, videojs properly generates the content. However, if you navigate away from that state and come back, videojs content isn't generated and the default html5 video element is displayed. Is there a way to deal with this problem? In addition, (more often in IE 11), videojs will sometimes randomly fail to generate the content on the first page load. I can't figure out if the problems are related, or what is even causing the problem because there are no errors in the console log.
I'm not sure what code will even be relevant to post. Here is the html for the video:
<video controls preload data-setup="{}" class="video-js vjs-default-skin vjs-big-play-centered full-video" poster="img/CollaborationPoster.png">
<source ng-repeat="src in video.srcs" ng-src="{{src.url | trustUrl}}" type="{{src.mimeType}}"/>
</video>
In addition I have:
window.VIDEOJS_NO_DYNAMIC_STYLE = true;
At the start of my app. However, the problem still exists if I get rid of this.
Feel free to ask for any other code that could help diagnose the problem
(NOTE: I am also open to suggestions on using a different framework/etc for the video component. videojs has been very frustrating overall)
EDIT: In case it's relevant, 'full-video' is the only custom class for the video and its styling is:
.full-video {
width: 100%;
height: auto;
}
I have the same problem, and I find a way to solve it.
Add this code in your controller may help you.
$scope.$on('$destroy', function () {
video.dispose();
});
Video is the player object created by videojs() function.
Can look at this discussion for more infomation.
angular single page app, videoJS ready function only fires on page load
I assume you are using some form of routing? Perhaps you need to reload the controller/directive/component.

Angular + Ionic loading all content via XHR

We have an Angular + Ionic app that we are planning on running through Cordova, but having an issue with performance that we are trying to track down.
What we are seeing in Chrome Dev tools Network tab when running either locally or on the built app, is the following:
Duplicate loading of CSS
XHR requests to get every single template file our Angular UI router links to, without having visited the routes yet
As an example:
And line 3167 (indicated with a star) from the angular.js source:
append: function(element, node) {
var nodeType = element.nodeType;
if (nodeType !== NODE_TYPE_ELEMENT && nodeType !== NODE_TYPE_DOCUMENT_FRAGMENT) return;
node = new JQLite(node);
for (var i = 0, ii = node.length; i < ii; i++) {
var child = node[i];
element.appendChild(child); *
}
},
I've never seen anything like it - we've checked all the basics (duplicate script/css includes, etc), disabled Ionic caching, etc.
I'm stripping things down to the studs to see what could be causing this, but hoping someone else has seen this and can offer some advice on where to start looking.
UPDATE
The duplicate CSS appears to be due to our index.html file which bootstraps our Angular App was incorrectly pointed to as a state in the UI Router config.
So the root issue is the spurious/unexpected XHR pulls to all of the static files in the app (angular ui templates, directive templates).
The way I deal with html templates is to cache them all at compile time using gulp-ng-templates or grunt-angular-templates (depending which flavor of task manager you like nowadays).
Since we are dealing with apps, the content should better be eager-loaded rather than lazy-loaded (as long as you count their total size in MB), thus saving some bandwidth and improving the overall user experience. In addition it might just fix your issue.
The beauty of caching the templates at compile time is that your implementation doesn't need to know where they are coming from (server or the caching layer), and thus you don't need to change any code.
P.S. I understand that my answer will not fix your actual problem, but it might just solve 2 issues at the same time.
Well, when a state is activated, the templates are automatically inserted into the ui-view of its parent state’s template.
You should check how you have defined your states. And/or share your state definitions with us :)

AngularJS - parameters in css files

I'm currently migrating from custom framework to Angular. Since we've got legacy, all front-end resources like stylescheets, images and scripts should be located on a subdomain, and all urls should be absolute. I've got a bunch of css files with a parameter specifying our static domain. I'm looking for a native Angular approach to using parameters in css, so I'll be able to write smth like:
.body {background: "{{domain}}/img/bg.png";}
Currently in our framework styles are loaded with, say, $http.get(), then processed with .replace and then appended to DOM.
Any ideas?
Thank you.
Try the $interpolate service. Inject it in a method, then use like this:
var fn = $interpolate(cssText);
var processedCssText = fn(scope); // scope is whatever obj that contains `domain` and other properties that might be used inside cssText
You can even configure the opening & closing symbols, if needed. See the documentation for $interpolate for more information.
You want LESS.
http://lesscss.org
It's the "dynamic stylesheet language".

possible to disable $locationProvider and $routeProvider?

Can I disable these fellas? I am using angular.js in asp.net mvc app, and I don't need angular to control anything related to address bar or the links...
Right now in html5 mode disabled ($locationProvider.html5Mode(false)) it adds hash and action method's name to the address-bar, for example: you go to \Home\index, it navigates and then address bar text changes into Home\index#\index. ain't that's annoying?
if I enable html5 mode it stops loading pages at all (except the initial). I try going from initialy loaded page to another - it changes the address-bar's text (without adding hashtag thing this time) but won't load the page itself. ain't that frustrating?
A makeshift solution exists here AngularJS 1.1.5 - automatically adding hash tag to URLs
The answer explains the first step (as explained above, with the addition of the new hash-prefix)
yourApp.config(['$locationProvider', function($locationProvider){
$locationProvider.html5Mode(true).hashPrefix('!');
}]);
The first bit handles Angular interfering with your address-bar visually, but now clicking on any links doesn't function properly as (read: history.pushState)
So, the workaround, as pointed out by user #Kevin Beal is some variation of setting the target of the <a> to _self
$('a[href]').attr({'target':'_self'})
or on a case-by-case basis:
Foo
Bar
Although, for the sake of convenience and sanity, I think it's combination of these.
Markup sans target
Foo
Bar
JS
// If `http` protocol present, assume external link
$('a[href^="http://"]').attr({'target':'_blank'});
// Otherwise, assume internal link
$('a:not([href^="http://"])').attr({'target':'_self'});
Worth noting that the above selectors do required jQuery proper.

Resources