Firefox WebExtensions: How to open a background page as a unique tab? - firefox-addon-webextensions

The question: In Firefox WebExtensions, from arbitrary background origins, how can I open an arbitrary page in a tab, uniquely?
Requirements:
1) Arbitrary background origins. My initial use case is from browser_action contextMenus. However, the technique ought to work from popup, options, or any custom background script. For the moment, not concerned with content pages. Although if the technique works for them too, great.
2) Open an arbitrary page in a tab. My initial use is for the options page, but the technique should work for arbitrary background pages (options, popup, or custom pages).
3) Open uniquely. If it's the first time, open in a new tab. If the tab was previously opened, focus to the existing tab, don't create duplicate tabs.
4) If I close a tab, I need to make sure I remove my reference to the previously opened tab ID.
I've answered my own question with one possible solution.
If the solution could be improved upon, let me know. Thank you.
If the question is a duplicate, I'll remove and post solution elsewhere if appropriate.

This is an attempt to answer my own question, with comments inline.
This simple example assumes all pagers live in the top level directory, but this is arbitrary and easily changed.
Essentially it consists of four parts:
1) A global object tabIDs to hold the page names (without '.html'). You could change this to be full path including extension, or keep the page name as a short name and modify technique to use another option like path for the full path name, etc.
2) An async function (to make use of the new await feature) named tabCreate to determine if one is already open and switch to it or create a new tab.
3) An onRemoved event handler, tabRemove to handle cleanup of tabIDs after a tab is closed (if it was one of interest).
4) A usage example, from a context menu item, passing some a page and a panel option, which have no use in this question, but are part of another question, and just here for demonstration purposes.
background.js:
// tabID contexts
// global var to keep track of different tabs,
// i.e. options.html, popup.html and so on.
var tabIDs = {
'options': null,
'popup': null,
}
// Requires Firefox 52.0+ to use async/await
// opts correspond to contexts above in tabIDs
// of the form { 'page': 'options' } or { 'page': 'popup' }
// note if using Node.js, this may require v7+ and --harmony_async_await
async function tabCreate ( opts ) {
var tab;
if ( tabIDs[ opts.page ] !== null ) {
// should probably bring window to front first... oops
// ..
// switch to tab
tab = await browser.tabs.update( tabIDs[ opts.page ], { active: true } );
} else {
tab = await browser.tabs.create( {
'url': opts.page + '.html'
} );
tabIDs[ opts.page ] = tab.id;
}
console.log( '**** opts.page = ' + opts.page + ', opts.tab = ' + opts.tab + ', tab.id = ' + tab.id );
}
// When tabs are closed, see if the tabID is in tabIDs,
// and if so, set it to null
function tabRemove ( tabID, removeInfo ) {
console.log( 'Closed TAB ' + tabID );
Object.keys( tabIDs ).forEach( function( key, index ) {
if ( tabIDs[ key ] === tabID ) {
tabIDs[ key ] = null;
return;
}
} );
}
browser.tabs.onRemoved.addListener( tabRemove );
/*
* Context Menus
*/
browser.contextMenus.removeAll( );
browser.contextMenus.create( {
title: 'My Web Extension',
contexts: [ 'browser_action' ],
} );
browser.contextMenus.create( {
title: 'Options',
contexts: [ 'browser_action' ],
onclick: myWebExt_Options
} );
function myWebExt_Options ( ) {
tabCreate( {
'page': 'options',
'panel': 1,
} );
}
Another approach might be to add an event listener to each page, and when closed, send a message back to background.js, but that seems much more complicated with little or no benefit.

Related

external redirection in AngularJS erasing navigation stack

We have set up different applications something like micro frontends running in different versions of Angular, one of them which is in AngularJS is required to redirect to a url which is in another project. Everything works until you try to go back using the browsers back button, and it takes you to the previous domain the user has navigated to. It can't go back to the immediately previous URL. It seems the navigation has been replaced but I can't see anything on the project that uses location.replace.
for example:
I am navigating
www.anyurl.com
then i go to my projects dns like
myproject.com/employer/dashboard
after I can click on to desired button which will take me to
myproject.com/employer/job-flow#location
Up to there everything is fine, but If I click on the browsers back button instead of taking me to:
myproject.com/employer/dashboard
It will go to the previous domain:
www.anyurl.com
I hope this is a clear explanation
from here: /employer/search to /employer/job-flow#location
The Process:
Declare a new item in the menu:
..., {
title: 'Post a job',
mainPath: 'job-flow',
fallbackUrl: 'employer/job-flow',
image: '/assets/images/new-job.svg',
notification: false,
subitems: [],
enabled: true
}
In the HTML there is a:
ng-href="{{ item.fallbackUrl }}"
Then there is an angular module which stores an array with all the routes:
var employerWebRoutes = [
...
'employer/job-flow'
];
Afterwards the array is iterated (I think it works like an interceptor for the given routes):
employerWebRoutes.forEach( function ( route ) {
$stateRegistryProvider.register( {
url: '/' + route + '{path:.*}',
name: route + 'ExternalState',
component: 'merlExternalRoutesComponent',
resolve: {
reloadFromServer: reloadFromServerResolver
},
data: {
authAccess: 'private',
isExternalRoute: true
}
} );
} );
And lastly the reloadFromServerResolver method
var reloadFromServerResolver = [
'$transition$',
'$q',
function (
$transition$,
/** angular.IQService */ $q
) {
var /** #type module:angular.ui.IStateService */ stateService = $transition$.router.stateService;
var /** #type string */ toUrl = stateService.href( $transition$.to().name, $transition$.params() );
setTimeout( function () {
window.location.assign( toUrl );
}, 10 );
return $q.reject();
}
];
I am guessing this is how is done, I am new to this project and can't figure very well how to describe this problem, if anyone can point in the right direction I would be highly grateful. Thanks

Detect react event from Tampermonkey

I'm enhancing a React front end with Tampermonkey , by adding highlights to show cursor location in a grid, and allowing users to directly enter data , rather than then enter data.
After 2 or 3 cursor moves or data entry the grid refreshes or updates - no page change - and looses the highlighting I set up.
I'd like to catch the refresh/update and reset the highlighting.
I'm a noob..
The network tab shows post events so I tried https://jsbin.com/dixelocazo/edit?js,console
var open = window.XMLHttpRequest.prototype.open,
send = window.XMLHttpRequest.prototype.send;
to try and use POST events to detect the refresh. No joy !
I also looked at ajax events.
No luck :(
Can someone point me in the right direction here ?
Once I catch the event, I can then reset the highlighting to fix the problem
Since normally the userscripts run in a sandbox, JavaScript functions or objects cannot be used directly by default, here's what you can do:
Disable the sandbox:
// #grant none
You won't be able to use any GM functions, though.
Run in the page context via unsafeWindow:
const __send = unsafeWindow.XMLHttpRequest.prototype.send;
unsafeWindow.XMLHttpRequest.prototype.send = function () {
this.addEventListener('loadend', e => {
console.log('intercepted', e);
}, {once: true});
__send.apply(this, arguments);
};
Use MutationObserver to detect changes in page DOM:
const observer = new MutationObserver(mutations => {
const matched = [];
for (const {addedNodes} of mutations) {
for (const n of addedNodes) {
if (!n.tagName)
continue;
if (n.matches('.prey:not(.my-highlight)')) {
matched.push(n);
} else if (n.firstElementChild) {
matched.push(...n.querySelectorAll('.prey:not(.my-highlight)'));
}
}
}
// process the matched elements
for (const el of matched) {
el.classList.add('my-highlight');
}
});
observer.observe(document.querySelector('.surviving-ancestor') || document.body, {
subtree: true,
childList: true,
});
.surviving-ancestor means the element that isn't replaced/recreated by the page script. In devtools element inspector it's the one that isn't highlighted temporarily during DOM updates.
See also Performance of MutationObserver.

Migrating jQuery selector to angularjs for third party vendor client help functionality

I'm trying to migrate old jQuery code to angularjs.
The issue that I'm having is that I'm not sure on the best approach.
Bascially, depending on the selector a different type of 'event' needs to be pushed into a array called gt.
The purpose of the jQuery code is to provide detailed info of clients having issues while filling in a form. the gt array is picked up by third party software that helps the clients by asking if they want to chat.
Example of how the array is populated:
$('a').live('click', { element: this }, function (element) {
_clickedElement = this;
var linkUrl = element.currentTarget.hostname + element.currentTarget.pathname;
var querystring = window.location.search
var shortLocationUrl = window.location.href.replace(querystring, "").replace("http://", "").replace("https://", "");
if (element.currentTarget.hostname.length > 0 && element.currentTarget.target != "_blank" && linkUrl != shortLocationUrl) { //click on a link that opens in the current window and points to a page external to this part
_gt.push(['event', { eventName: 'Leave_Page_' + chat.name, name: chat.name, pageName: chat.pageName, locale: _locale, isClient: chat.isClient }]);
_pushLeavePageEvent = false;
}
else if (this.id == backButtonId) { //click "previous"
_gt.push(['event', { eventName: 'Go_Back_' + chat.name, name: chat.name, pageName: chat.pageName, locale: _locale, isClient: chat.isClient }]);
_pushLeavePageEvent = false;
}
return true;
});
So for all the a tags inside my page (or form) the above code needs to be executed.
What would be a good approach to have similar behaviour in Angularjs?
I was thinking of a directive but I'm not sure whether to make this a directive at the level of my form or make a directive that I then use throughout my page?
P.S.: similar behaviour is needed (pushing an event into the gt array) for all the input, textarea and select fields on the page as well as the errors on the page caused by the clients and when a client hovers over a tooltip.

Could we add our menu items in Gitkit Starter kit "Sign In cum User Info " ( #navbar )?

Could we add our menu items in Starter kit Gitkit NavBar ?
There are two list items in the drop down : Manage Account and Sign Out.
Is it possible to add a third option with a URL link ( say like Update Profile ) ?
The html for the #navbar gets loaded through Javascript code of Gitkit.
I use GAE Python.
Possible solutions which I could think of are :
After my webpage loads completely, I could add my own <li> items to the list of dropdown menu provided by #navbar.
Or
Write a custom "Sign In cum User Info " ( #navbar ) widget.
Is there a better and simpler approach ?
MY REQUEST TO GITKIT TEAM FOR ENHANCEMENT
It would be great if we could provide our custom menu items along with their URL links as options to below JS code which loads #navbar :
<script type=text/javascript>
window.google.identitytoolkit.signInButton(
'#navbar', // accepts any CSS selector
{
widgetUrl: "http://www.mywebsite.com/oauth2callback",
signOutUrl: "/",
// Example of possible solution ( my suggestion ):
custom_menu_item__1__name : "item_1", // My Custom Menu Item 1
custom_menu_item__1__link : "http://site/link_url_1",
::
custom_menu_item__n__name : "item_1", // My Custom Menu Item n
custom_menu_item__n__link : "http://site/link_url_1",
}
);
</script>
UPDATE
Temporary Fix = I have added the needed menu options using jquery temporarily. Code snippet provided below to help anyone with similar needs till official solution arrives :
On page load,
custom_menu_add_job_id = setInterval(function(){
add_custom_menu();
}, 5000);
function add_custom_menu(){
if ($("#navbar").find(".gitkit-user-card-menu").length){
$(".gitkit-user-card-menu").append($("<li class='gitkit-user-card-menuitem' id='smh_user_profile' tabindex='0'> <img src='/images/person_32x32.png' class='user_profile_menu_icon' > Profile </li>")
.click(function(){
window.location.href = window.location.protocol + "//" + window.location.host + "/user/";
})
);
clearInterval(custom_menu_add_job_id);
}
}
If you want, you could check it live at ShowMyHall.
Customized menu items are now supported in Google Identity Toolkit javascript widget. Examples:
window.google.identitytoolkit.signInButton(
'#navbar', // accepts any CSS selector
{
widgetUrl: "...",
dropDownMenu: [
{
'label': 'Check Configuration',
'url': '/config'
},
{
'label': 'Sign out',
'handler': function() {google.identitytoolkit.signOut();}
},
{
'label': 'Manage Account',
'handler': function() {google.identitytoolkit.manageAccount();}
},
]
};
Until this feature arrives, I also implemented a similar temporary fix that you outlined at the end of your question. I got around using a timer as follows (note that my gitkit is using the div login):
$(window).load(function() {
$("#login").hover(function() {
add_custom_menu_items();
})
});
function add_custom_menu_items(){
if ($("#login").find(".gitkit-user-card-menu").length == 1){
if ($("#my_data_link").length == 0) {
$(".gitkit-user-card-menu li:eq(0)").after($('<li class="gitkit-user-card-menuitem" id="my_data_link" tabindex="0">My data</li>'));
}
}
}
Basically when you hover over the div it adds the menu item, but only if it hasn't already been added.
The navbar drop down menu does not support images but if you really need that, here's a hacky way to do it in jquery:
var config = {...}; // your config which includes the custom drop down menu.
// Render button on load. (now supported)
window.onload = function() {
window.google.identitytoolkit.signInButton(
'#navbar', // accepts any CSS selector
config);
// This will modify the html content of the first option in drop down menu.
// Make menu dom changes.
jQuery('#navbar li:first-child').html('<img src="img.png">My Label');
}

Sencha Touch: Clicking a button rapidly will push a view twice

Say I have a button that triggers a push of a new view.
I noticed that if I click it more than once, fast enough, it will push the same view twice.
You can mimic this behavior using their official docs on this page, where they have a live sample:
http://docs.sencha.com/touch/2-0/#!/guide/navigation_view
the clear question is, simply how to prevent it?
Another method is to check what the active view is, and only push if it is not the same as the view you are about to push. I've tested this and it works.
E.g.
if (this.getNavigationView().getActiveItem().xtype != "someView") {
this.getNavigationView().push({ xtype: "someView" });
}
Extending jayteejee's answer, I've overridden the push method in a custom navigation view, like this:
Ext.define('BT.navigation.View', {
extend: 'Ext.navigation.View',
xtype: 'btnavigationview',
push: function (view) {
if(this.getActiveItem().xtype != view.xtype)
this.callParent(arguments);
else
console.warn("Prevented pushing a potentially duplicate view of xtype: " + view.xtype);
}
});
I'm not totally sure if the xtype assumption is safe enough, but I can't think of any situation in my current app that would require one view pushing another view of the same type onto the navigation stack. So, the solution works for me, and it's pretty neat. The warning is there to save me headache later on and possibly pulling my hair out trying to work out why push wouldn't work!
Masking successfully prevents double tapping problem.
In my code I'm using two functions for mask/unmask navigation container:
/**
* Mask container with rolling wheel. Usually need if Ajax-request is sent to the server and app waiting for response
* Best practice is masking the current navigator container, to prevent blocking whole app. Method warns if no container
* is defined. In some cases warning could be suppress with parameter
*
* #param container
* #param {boolean} [suppressWarning]
*/
startLoading: function(container, suppressWarning) {
var loadingComponent = container;
if (!loadingComponent) {
// <debug>
if (!suppressWarning) {
console.warn('Please define navigator container for non-blocking operation, or define suppressWarning parameter');
}
// </debug>
loadingComponent = Ext.Viewport;
}
// var lastMaskedContainer = container;
this.lastMaskedContainer = container;
loadingComponent.setMasked({
xtype: 'loadmask',
message: 'Loading...'
});
/*
Ext.defer(function() {
lastMaskedContainer.setMasked(false);
}, Pipedrive.app.maskingTimeout * 1000)
*/
},
/**
*
* #param {Ext.Container} container
* #param {boolean} [suppressWarning]
*/
stopLoading: function(container, suppressWarning) {
var loadingComponent = container;
if (!loadingComponent) {
// <debug>
if (!suppressWarning) {
console.warn('Please define either navigator container for non-blocking operation, or define suppressWarning parameter');
}
// </debug>
loadingComponent = Ext.Viewport;
}
var alreadyMasked = loadingComponent.getMasked();
var lastMaskedContainer = this.lastMaskedContainer;
if (!alreadyMasked && !suppressWarning) {
// <debug>
if (lastMaskedContainer != container) {
console.warn('Found Start/Stop Loading inconsistency. Please revise code'
+ (container ? '. Container: ' + container.getId() : 'Ext.Viewport')
+ (lastMaskedContainer ? ', last masked container: ' + lastMaskedContainer.getId() : '')
);
}
// </debug>
loadingComponent = Ext.Viewport;
}
loadingComponent.setMasked(false);
}
than in the tap handler:
onDealDetailsTap: function(ct) {
console.log('onDealDetailsTap', ct);
var form = ct.getReferenceForm(),
navigatorContainer = this.getNavigatorContainer(form),
model = form.getRecord();
UiHelper.startLoading(navigatorContainer);
Ext.Viewport.fireEvent('detailfields', {
title: model.get('title'),
id: model.get('id'),
store: 'DealFields',
navigatorContainer: navigatorContainer
})
},
to cleanup the loading mask:
control : {
activitiesContainer: {
push: 'onPushActivitiesContainer'
},
onPushActivitiesContainer: function(ct) {
//console.log('onPushActivitiesContainer', ct);
UiHelper.stopLoading(ct);
},
especially it is cool for waiting for long-timed ajax requests....
Cheers, Oleg
Just suspend the events on the button when it's tapped and resume them when the view is pushed
button.suspendEvents();
...
button.resumeEvents();
I don't think there is another way. As a developer or a user, when you tap a button twice, you expect the event handler to be called twice.
Hope this helps
simply mask the entire container and then unmask it; create a ref for the container or panel in which the button exists in your controller and on tap set:
ref.setMasked(true)
After the new view is pushed simply unmask by
ref.setMasked(false)
Another way is to flip a parameter once the list item has been tapped once, like this:
{
onListItemTap: function () {
if (!this.tapped) {
this.tapped = true;
...
}
}
}
Of course, that only works if you are destroying the list view as soon as the user goes to a different screen.
I created a method for checking this:
ENSURE_NO_DOUBLE_TAP : function(classNameToPush) {
if (Ext.getClassName(Ext.getCmp('MyViewport').getActiveItem()) == classNameToPush) {
return false;
}
return true;
}
Then from your app before anything that could be double tapped is processed:
if (!ENSURE_NO_DOUBLE_TAP('MyApp.view.View')) {
return;
}
If you are listening to the tap event of a button using listeners,then here is
my solution:
listeners : {
release : function(){
if(this.getDisabled())return false;
this.setDisabled(true);
this.fireEvent('tap');
},
tap : function() {
//do what you want
}
}
Extending on jayteejee's and Merott's answers, I've added some code to intercept on multiple fast pushes to not only prevent duplicates but to prevent pushing of different views as well before the page transition completes. Think of a user tapping different list items.
Also notice the view.destroy(); method in the else block to prevent view instances from heaping up in memory.
Ext.define('Overrides.navigation.View', {
extend: 'Ext.navigation.View',
xtype: 'ovrnavigationview',
interceptPush: false,
push: function (view) {
var activeItem = this.getActiveItem();
// Prevent multiple pushes & duplicates
if (!this.interceptPush && activeItem.xtype !== view.xtype) {
// Set interceptPush
this.interceptPush = true;
// Reset interceptPush after 500 ms
Ext.defer(function() {
this.interceptPush = false;
}, 500, this);
// Handle push
this.callParent(arguments);
} else {
// Warn developer
console.warn("Prevented pushing view of xtype: " + view.xtype);
// Destroy view
view.destroy();
return false;
}
}
});
You can just use the "itemsingletap" event.
If you want to support double taps as well, make a second listener for "itemdoubletap" and invoke the same function, both listeners work fine together.

Resources