WebExtension: React when a a tab is reloaded - firefox

I am working on a Firefox WebExtension which has a popup and uses chrome.storage.local to store state. So far, I have got it to store data for specific tabs using the tab id.
I would like to reinitialize the local storage if the current tab is reloaded. This is because the extension may have made some changes to the DOM which will be lost if the tab is reloaded; what state it has stored is therefore incorrect.
The initial JavaScript code is called from the popup.
How can I run some code when the page is reloaded?
Thanks

Without more information from you as to exactly what you desire to do, it is difficult to determine the best way to do this. In particular, it is hard to know exactly what situations to which you desire to respond.
It looks like you could use a listener to the tabs.onUpdated event. While this fires more often than you actually desire, it does fire on reloading of a page.
Below is code that calls a function completedLoadingURLInTab() when a URL has been loaded, or reloaded on a page. I've left in a bunch of console.log() calls as comments which I would normally remove. They can be uncommented to show in the console all the times the event fires. This can be useful to determine exactly the contents of the data received from the event and the sequence of events that fire during various page navigation.
Note 1: I found that the changeInfo object can be invalid under some circumstances. It was necessary to see if a property existed using hasOwnProperty() and then obtain the value from the tabs.Tab object that is also passed to the event handler.
Note 2: The tabs permission in manifest.json is required.
function completedLoadingURLInTab(tabId, newUrl,oldUrl) {
//We have completed, loading a URL.
if(newUrl === oldUrl) {
//We re-loaded the previous URL
console.log(">>>>>>> Re-load tab=" + tabId + " with URL=" + newUrl);
} else {
//This URL _may_ be just a new position in the same page. That
// is something that needs to be checked for here.
console.log(">>>>>>> New URL loaded in tab=" + tabId + ". URL=" + newUrl);
}
//Remember the newUrl so we can check against it the next time
// an event is fired.
tabsInfo[tabId].previousCompleteUrl = newUrl;
tabsInfo[tabId].loadingUrl = newUrl;
}
let tabsInfo = {};
function InfoForTab(_loadingUrl,_previousUrl,_status) {
this.loadingUrl = _loadingUrl;
this.previousCompleteUrl = (_previousUrl === undefined) ? "" : _previousUrl;
this.status = (_status === undefined) ? "" : _status;
}
function foundNewTab(tabId) {
//Create an object to hold the collected info for the tab.
tabsInfo[tabId] = new InfoForTab();
console.log("New tab. ID=" + tabId);
}
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
if(!tabsInfo.hasOwnProperty(tabId)) {
//This is the first time we have encountered this tab.
foundNewTab(tabId);
}
let output="";
//We use the properties of "tab" instead of "changeInfo" because in testing it was
// clear that changeInfo was not always properly populated. The key(s) may just
// exist, but not have any value associated with them.
/*
//Testing output showing when the event is fired.
// This is used to determine, by experimentation, what events to ignore and which
// combinations and sequence of events occur during page navigation.
output += (changeInfo.hasOwnProperty("status")) ? "\nstatus=" + tab.status : "";
output += (changeInfo.hasOwnProperty("url")) ? "\nurl=" + tab.url : "";
output += (changeInfo.hasOwnProperty("pinned")) ? "\npinned=" + tab.pinned : "";
output += (changeInfo.hasOwnProperty("audible")) ? "\naudible=" + tab.audible : "";
output += (changeInfo.hasOwnProperty("mutedInfo")) ? "\nmutedInfo="+tab.mutedInfo : "";
output +=(changeInfo.hasOwnProperty("favIconUrl"))?"\nfavIconUrl="+tab.favIconUrl : "";
console.log("tabs.updated event: tabId=" + tabId + ":: changeInfo="
+ Object.keys(changeInfo) + output
);
*/
if(changeInfo.hasOwnProperty("status") && changeInfo.hasOwnProperty("url")
&& (tab.status === "loading")) {
//A URL is being loaded into the tab. This can be for the first time,
// or transitioning to a new URL, or reloading the page.
let outputFirst = "";
let outputLoading = "Loading";
if(tabsInfo[tabId].previousCompleteUrl === tab.url) {
//We are re-loading the same URL
outputLoading = "Re-loading";
}
//console.log(outputLoading + " URL=" + tab.url);
//We save the URL which is being loaded, but we really don't do anything with it.
tabsInfo[tabId].loadingUrl = tab.url;
tabsInfo[tabId].status = "loading";
return;
} //else
if(changeInfo.hasOwnProperty("status") && (tab.status === "complete")) {
if( tabsInfo[tabId].status === "loading") {
tabsInfo[tabId].status = "complete";
//console.log("In tabId="+tabId+" completed loading URL="+tab.url);
completedLoadingURLInTab(tabId, tab.url, tabsInfo[tabId].previousCompleteUrl);
return;
} //else
if( tabsInfo[tabId].status === "complete") {
if(tabsInfo[tabId].previousCompleteUrl === tab.url) {
//console.log("In tabId=" + tabId + " got completed status change after"
// + "already complete with URL="
// + tabsInfo[tabId].previousCompleteUrl);
return;
}//else
//console.log("In tabId=" + tabId + " completed directly loading new URL="
// + tab.url);
completedLoadingURLInTab(tabId, tab.url, tabsInfo[tabId].previousCompleteUrl);
return;
}
}//else
if(changeInfo.hasOwnProperty("status") && (tab.status === "loading")
&& ( tabsInfo[tabId].status === "complete")) {
//console.log("In tabId=" + tabId + " leaving page");
return;
}//else
if(changeInfo.hasOwnProperty("status") ) {
if((tab.status === "complete") && (tab.url === "about:newtab")
&& tabsInfo[tabId].loadingUrl === undefined ) {
//We have completed loading about:newtab for the first time in this tab.
tabsInfo[tabId].status = "complete";
completedLoadingURLInTab(tabId, tab.url, tabsInfo[tabId].previousCompleteUrl);
return;
} //else
//console.log("In tabId=" + tabId + " got status change to " + tab.status
// + " prior to loading a URL with current URL=" + tab.url);
return;
}//else
});
Given loading the add-on, opening a new tab and doing a bit of navigation (including a few page re-loads), the output in the console could look like:
New tab. ID=6
>>>>>>> New URL loaded in tab=6. URL=about:newtab
>>>>>>> New URL loaded in tab=6. URL=https://www.google.com/?gws_rd=ssl
>>>>>>> Re-load tab=6 with URL=https://www.google.com/?gws_rd=ssl
>>>>>>> New URL loaded in tab=6. URL=https://www.google.com/?gws_rd=ssl#q=test
>>>>>>> Re-load tab=6 with URL=https://www.google.com/?gws_rd=ssl#q=test
>>>>>>> New URL loaded in tab=6. URL=https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onUpdated
>>>>>>> New URL loaded in tab=6. URL=https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onUpdated#changeInfo
>>>>>>> Re-load tab=6 with URL=https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onUpdated#changeInfo
Alternately, use the webNavigation events
The webNavigation events provide you information directly about web navigation. They will probably provide you with a better solution than tabs.onUpdated.
If you use webNavigation events, you will need to experiment to see which combination of events fire for the situations your are concerned about. Most likely this will be Completed and/or ReferenceFragmentUpdated.
So you can get a feel for what when these events fire, the following code will log to the console all webNavigation events:
var webNavEvents = ['BeforeNavigate',
'Committed',
'Completed',
//'CreatedNavigationTarget', //Not supported by Firefox
'DOMContentLoaded',
'ErrorOccurred',
'HistoryStateUpdated',
'ReferenceFragmentUpdated'
//'TabReplaced' //Not supported by Firefox
];
webNavEvents.forEach(function(navType){
browser.webNavigation['on' + navType].addListener(function(type,details){
console.log('\twebNavigation->' + type
+ ': tadId=' + details.tabId
+ ':: url=' + details.url
+ ((typeof details.transitionType === 'string') ? ':: transitionType='
+ details.transitionType : '')
);
}.bind(undefined,navType));
});

Related

Why does my Teams userscript have no effect?

When I paste URLs pointing to my ticketing system into other systems, I want them to look nice without a lot of manual reformatting. I have a userscript that works in GitHub taking <rootURL>/ticket/1234 and pasting it in GitHub as [#1234](<rootURL>/ticket/1234) which GitHub renders nicely.
I want to do the same thing for Teams. The Teams app for Windows has some programmability but I can't find what I need there so I've resorted to using Teams on the web and writing another userscript. (By all means, I'm happy to hear, "The way to do that in Teams is ...." but this is the best I've come up with so far.)
Whereas GitHub messages are composed in a textarea, Teams has a div that it updates as you type. I have a paste handler that creates what seem to be the right DOM elements but when my function is done, I see the original, unformatted URL in the Teams message.
My userscript is below. The logging tells me its doing the right thing but my new DOM elements don't show up in the UI. Any thoughts?
// ==UserScript==
// #name Teams Trac link
// #namespace http://tampermonkey.net/
// #version 0.1
// #description Reformat pasted link to Trac ticket to look pretty in Teams
// #author Chris Nelson, PE
// #match https://teams.microsoft.com/*
// #icon https://www.google.com/s2/favicons?sz=64&domain=microsoft.com
// #grant none
// ==/UserScript==
// Handle paste event by making custom link, when appropriate.
// If handled, suppress the browser handling of the event.
function handlePaste(event) {
console.log("Handling paste");
var target = event.target;
// Get the text that the user is trying to paste
var text = (event.clipboardData || window.clipboardData).getData('text');
try {
var url = new URL(text);
if (url.pathname.startsWith('/trac/ticket')) {
console.log("Pasting a Trac ticket link: " + text);
var parts = url.pathname.split('/');
var ticketNumber = parts[3];
// Don't let the browser also paste
event.preventDefault();
// Create a link
var link = document.createElement('a');
link.innerHTML = '#' + ticketNumber;
link.href = text;
link.title = 'Trac ticket #' + ticketNumber;
// FIXME - nothing from here on works to insert the link
// Get all divs
var divs = Array.from(document.querySelectorAll('div'));
// Filter to those with an ID that starts 'new-message-'
divs = divs.filter(d => d.id.startsWith('new-message-'));
var parent = divs[0];
console.log('Before adding, div has ' + parent.childNodes.length + ' children');
parent.appendChild(link);
console.log('After adding, div has ' + parent.childNodes.length + ' children');
}
}
catch (error) {
// Nothing, just let the browser do its thing.
}
}
(function() {
console.log("Attaching paste handler");
// Intercept a paste event anywhere on the page but
// only handle it if pasting into a new message.
document.body.addEventListener('paste', (event) => {
var target = event.target;
console.log("Got paste event for " + target);
if (target.tagName.toLowerCase() == 'br'
&& target.dataset.ckeFiller == 'true') {
handlePaste(event);
}
}, false);
})();

Prevent Page Reload on Module Selection DNN 9

When I click on a module inside of the module selection dialog, DNN refreshes the page (before the hovering, draggable element appears on the page). This only happens using our skin (https://github.com/2sic/dnn-theme-bootstrap3-instant).
DNN is looking for the element #dnn_ContentPane_SyncPanel (which seems to be an ajax wrapper) with Teleriks findComponent method. Because the element can't be found, DNN performs a page reload.
Our skins content pane:
<div id="ContentPane" runat="server" containertype="G" containername="Invisible Container" containersrc="default.ascx"></div>
The DNN code, that triggers the reload (last function call):
refreshPane: function (paneName, args, callback, callOnReload) {
var paneId;
if (!paneName) {
paneId = this.getModuleManager().getPane().attr('id');
} else {
paneId = this.getPaneById(paneName).attr('id');
}
var pane = $('#' + paneId);
var parentPane = pane.data('parentpane');
if (parentPane) {
this.refreshPane(parentPane, args, callback);
return;
}
//set module manager to current refresh pane.
this._moduleManager = pane.data('dnnModuleManager');
var ajaxPanel = $find(paneId + "_SyncPanel");
if (ajaxPanel) {
//remove action menus from DOM bbefore fresh pane.
var handler = this;
pane.find('div.DnnModule').each(function () {
var moduleId = handler._moduleManager._findModuleId($(this));
$('#moduleActions-' + moduleId).remove();
});
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(this._refreshCompleteHandler);
this._refreshPaneId = paneId;
this._refreshCallback = callback;
ajaxPanel.ajaxRequest(args);
} else {
//save the args into cookie, after page reload then catch the cookie
//and float the module for drag
if (args && !this._noFloat) {
this._setCookie('CEM_CallbackData', args);
}
if (callOnReload && typeof callback == "function") {
callback.call($('#' + paneId), [true]);
}
location.reload();
}
}
After some time, we found out what the issue was.
Our skin was using the <%=SkinPath%> syntax instead of <%#SkinPath%>, which caused DNN to force a reload. This probably has to do with the lifecycle of the document.

Using Find.BySelector with the :visible filter in WatiN throws error

I am using WatiN 2.1 to drive Internet Explorer for automated integration testing. For one of my tests, I want to select an item in a dynamically created popup menu after clicking on a button that creates the menu. The menu itself is a jQuery plug-in that creates an unordered list with a specific class.
I am using WatiN 2.0's new Find.BySelector method to search via this specific class and that works great. However, in tests where multiple menus are created, I'm having a difficult time selecting the menu that is visible at the time. For this, I thought I would use the :visible filter to limit my results to only those menus that are visible (only one can be visible at a time). However, when I use the following code:
WebBrowser.Current.ElementOfType<List>(Find.BySelector("li.fg-menu:visible"));
I get a WatiN.Core.Exceptions.RunScriptException thrown with the message: "System.Runtime.InteropServices.COMException: Exception from HRESULT: 0x80020101" While searching for this specific HRESULT, people have recommended running Visual Studio as an Administrator, but this does not fix the problem. Without the :visible filter it works fine. When I execute that selector directly in the console window of the browser (using jQuery), it returns what I want.
To fix this, I could use WatiN's built-in ability to execute my own JavaScript to return an element, but I'm wondering if anyone else has been successful at using :visible with Find.BySelector.
For now, I've gone with this solution. It executes the selector in JavaScript using a "real" copy of jQuery already on the page. I've modified it a bit to dynamically insert the helper JavaScript based on whether it's already been inserted. It is also generic so you can specify the type of Element you want returned. Here is all the relevant extension methods for the browser object:
public static bool IsJavascriptEvaluateTrue(this Browser browser, string javascript)
{
var evalResult = browser.Eval("if (" + javascript + ") {true;} else {false;}");
return evalResult == "true";
}
public static TElementType FindViaJQuery<TElementType>(this Browser browser, string cssSelector) where TElementType : Element
{
if (!browser.IsJavascriptEvaluateTrue("typeof('WatinSearchHelper') == 'function'"))
{
var js = "WatinSearchHelper = function () { " +
" var earTagId = 1; " +
" var getElementId = function (cssSelector) { " +
" var resultId = \"_no_element_\"; " +
" var el = $(cssSelector); " +
" if (el.length > 0) { " +
" var firstEl = el[0]; " +
" if (firstEl.id == \"\") { " +
" firstEl.id = \"_watin_search_\" + earTagId++; " +
" } " +
" resultId = firstEl.id; " +
" } " +
" return resultId; " +
" }; " +
" return { getElementId: getElementId }; " +
"} (); ";
browser.Eval(js);
}
var elementId = browser.Eval(string.Format("WatinSearchHelper.getElementId(\"{0}\")", cssSelector));
if (elementId == "_no_element_")
{
throw new ArgumentException("Unable to find element on page with selector '" + cssSelector + "'", "cssSelector");
}
return browser.ElementOfType<TElementType>(Find.ById(elementId));
}

JqGrid next button enabled for no values are returned

We have an issue when no values are returned from Jqgrid. In this case next button of pager (jqGrid) is enabled. We have to disable it when grid is empty. Please help me in finding a solution.
To set the next button as disabled you can do the following:
$('#next_' + yourgridid + '_pager').addClass('ui-state-disabled');
To re-enable the button do:
$('#next_' + yourgridid + '_pager').removeClass('ui-state-disabled');
I suggest doing this in the loadComplete(data) event.
$("#yourgridid").jqGrid({
...
loadComplete: function(data){
var records = $('#yourgridid').jqGrid('getGridParam', 'reccount');
//note: you can also get record count via data.records or data.rows.length in this function
if (records > 0) {
$('#next_' + yourgridid + '_pager').removeClass('ui-state-disabled');
} else {
$('#next_' + yourgridid + '_pager').addClass('ui-state-disabled');
}
},
...
});
loadComplete: function (data) {
var page = $('#mygrid').jqGrid('getGridParam', 'page');
var lastpage = $("#mygrid").getGridParam('lastpage');
//note: you can also get record count via data.records or data.rows.length in this function
if (lastpage > page) {
$('#next_mygridpager').removeClass('ui-state-disabled');
$('#last_mygridpager').removeClass('ui-state-disabled');
} else {
$('#next_mygridpager').addClass('ui-state-disabled');
$('#last_mygridpager').addClass('ui-state-disabled');
}
}
I believe this is a more complete solution because you also do not want the next and last button appear when there are no more pages to move forward.

how to end inline edit started with formatter edit action button if other row is clicked in jqgrid

Inline editing is started using edit formatter action button.
If clicked in other row, old row remains in inline edit mode.
How to end old row indline edit if clickedin other row.
According to
http://www.trirand.com/blog/?page_id=393/bugs/wrong-hovering-effect-in-actions-formatter-of-jqgrid-4-1-0
it looks line this is solved in 4.1.2 but actually the problem persists.
Update
Using Oleg workaround exception occurs if custom element is used.
Line where exception occurs is marked in comment in code below
// this is jqgrid custom_element property value:
function combobox_element(value, options, width, colName, entity, andmetp) {
var elemStr;
if (options.id === options.name)
// form
elemStr = '<div>' +
'<input class="FormElement ui-widget-content ui-corner-all" style="vertical-align:top" size="' +
options.size + '"';
else
elemStr = '<div>' +
'<input class="FormElement ui-widget-content " style="height:17px;vertical-align:top;width:' +
width + 'px" ';
elemStr += ' value="' + value + '"' + ' id="' + options.id + '"/>';
elemStr += '<button style="height:21px;width:21px;" tabindex="-1" /></div>';
var newel = $(elemStr)[0];
setTimeout(function () {
$(newel).parent().css({ display: "inline-block" }).parent().css({ 'padding-bottom': 0 });
// click in edit button in action input variable is undefined, newel does not contain input element:
var input = $("input", newel);
}, 500);
return newel;
}
Update2
I try to explain new issue more clearly.
Before adding
onEdit = #"function (id) {
if (typeof (lastSelectedRow) !== 'undefined' && id !== lastSelectedRow) {
cancelEditing($('#grid'));
}
lastSelectedRow = id;
}
event handler exception on custom element does not occur.
After adding onEdit event handler below custom editing elements are not created anymore. So custom editing elements cannot used in inline editing if this onEdit handler is present.
I commented out cancelEditing code but problem persists.
It looks like this onEdit event handler prevents custom editing element creation.
Update 3
I tried demo provided in Oleg answer. If inline edit is started by double click in row, action buttons do not change. It is not possible to use save and cancel buttons in this case.
You are right. It seems bug in the formatter:"actions" of the current version of jqGrid. If you examine the source code you will find no variable which saves the information about the last editing row. So depend on the implementation of your code which use formatter:"actions" you can has either multiple editing rows:
or at least wrong icons in the old editing row
and you will not be able to edit the previous editing icon ever more (because you have no "edit" action icon).
In the demo I suggest as the workaround to cancel the previous editing unsaved row in both onSelectRow jqGrid event and in the onEdit event of the formatter:'actions'. The corresponding code fragment look as following
var grid=$("#list"),
lastSel,
cancelEditing = function(myGrid) {
var lrid;
if (typeof lastSel !== "undefined") {
// cancel editing of the previous selected row if it was in editing state.
// jqGrid hold intern savedRow array inside of jqGrid object,
// so it is safe to call restoreRow method with any id parameter
// if jqGrid not in editing state
myGrid.jqGrid('restoreRow',lastSel);
// now we need to restore the icons in the formatter:"actions"
lrid = $.jgrid.jqID(lastSel);
$("tr#" + lrid + " div.ui-inline-edit, " + "tr#" + lrid + " div.ui-inline-del").show();
$("tr#" + lrid + " div.ui-inline-save, " + "tr#" + lrid + " div.ui-inline-cancel").hide();
}
};
grid.jqGrid({
// ...
colModel:[
{name:'act',index:'act',width:55,align:'center',sortable:false,formatter:'actions',
formatoptions:{
keys: true,
delOptions: myDelOptions,
onEdit: function (id) {
if (typeof (lastSel) !== "undefined" && id !== lastSel) {
cancelEditing(grid);
}
lastSel = id;
}
}},
...
],
onSelectRow: function(id) {
if (typeof (lastSel) !== "undefined" && id !== lastSel) {
cancelEditing($(this));
}
lastSel = id;
}
});
In the demo I use inline editing on double clicking on the grid row in addition to the action formatter. It is not really required, but both can work together without any conflicts.

Resources