Firefox add-on get the tab body content - tabs

Hello everyone i have an question about firefox add-on:
How i can get the body content from a tab, for example.
var content = require("tabs").activeTab.documentContent.body.innerHTML;
Thanks alot.

The Add-on SDK doesn't allow direct access to the tab contents - the idea is that the add-on and the tab might end up living in different processes eventually. What you do is injecting a content script into the tab to get you the necessary data, something like this:
var tab = require("tabs").activeTab;
tab.attach({
contentScript: "self.postMessage(document.body.innerHTML);",
onMessage: function(data)
{
console.log("Tab data received: " + data);
}
});

You can try this:
var tabs = require("sdk/tabs");
var { getTabForId, getTabContentWindow } = require ("sdk/tabs/utils");
var tab = require("tabs").activeTab;
var window = getTabContentWindow (getTabForId(tab.id));
var content = window.document.body.innerHTML;
But maybe this answer is better.

You can get the body of the currently-selected tab using the following (after DOMContentLoaded):
gBrowser.contentDocument.body.innerHTML
Note: This only works in a standard extension, not in the SDK.

Related

Update Google Calendar UI after changing visability setting via Workspace Add-On

I have a very basic Google Workspace Add-on that uses the CalendarApp class to toggle the visabilty of a calendar’s events when a button is pressed, using the setSelected() method
The visabilty toggling works, but the change in only reflected in the UI when the page is refreshed. Toggling the checkbox manually in the UI reflects the change immediately without needing to refresh the page.
Is there a method to replicate this immediate update behaviour via my Workspace Add-On?
A mwe is below.
function onDefaultHomePageOpen() {
// create button
var action = CardService.newAction().setFunctionName('toggleCalVis')
var button = CardService.newTextButton()
.setText("TOGGLE CAL VIS")
.setOnClickAction(action)
.setTextButtonStyle(CardService.TextButtonStyle.FILLED)
var buttonSet = CardService.newButtonSet().addButton(button)
// create CardSection
var section = CardService.newCardSection()
.addWidget(buttonSet)
// create card
var card = CardService.newCardBuilder().addSection(section)
// call CardBuilder.call() and return card
return card.build()
}
function toggleCalVis() {
// fetch calendar with UI name "foo"
var calendarName = "foo"
var calendarsByName = CalendarApp.getCalendarsByName(calendarName)
var namedCalendar = calendarsByName[0]
// Toggle calendar visabilty in the UI
if (namedCalendar.isSelected()) {
namedCalendar.setSelected(false)
}
else {
namedCalendar.setSelected(true)
}
}
In short: Create a chrome extension
(2021-sep-2)Reason: The setSelected() method changes ONLY the data on server. To apply the effect of it, you need to refresh the page. But Google Workspace Extension "for security reason" does not allow GAS to do that. However in an Chrome Extension you can unselect the checkbox of visibility by plain JS. (the class name of the left list is encoded but stable for me.) I have some code for Chrome Extension to select the nodes although I didn't worked it out(see last part).
(2021-jul-25)Worse case: Default calendars won't be selected by getAllCalendars(). I just tried the same thing as you mentioned, and the outcome is worse. I wanted to hide all calendars, and I am still pretty sure the code is correct, since I can see the calendar names in the console.
const allCals = CalendarApp.getAllCalendars()
allCals.forEach(cal => {console.log(`unselected ${cal.setSelected(false).getName()}`)})
Yet, the principle calendar, reminder calendar, and task calendar are not in the console.
And google apps script dev should ask themselves: WHY DO PEOPLE USE Calendar.setSelected()? We don't want to hide the calendar on the next run.
In the official document, none of these two behaviour is mentioned.
TL;DR part (My reason for not using GAS)
GAS(google-apps-script) has less functionality. For what I see, google is trying to build their own eco-system, but everything achievable in GAS is also available via javascript. I can even use typescript and do whatever I want by creating an extension.
GAS is NOT easy to learn. The learning was also painful, I spent 4 hours to build the first sample card, and I can interact correctly with the opened event after 9 hours. The documentation is far from finished.
GAS is poorly supported. The native web-based code editor (https://script.google.com/) is not build for coding real apps, it loses the version control freedom in new interface. And does not support cross-file search. Instead of import, codes run from top to bottom in the list, which you need to find that by yourself. (pass along no extension, no prettier, I can tolerate these)
In comparison with other online JS code editors, like codepen / code sandbox / etcetera it does so less function. Moreover, VSCode also has a online version now(github codespaces).
I hope my 13 hours in GAS are not totally wasted. As least whoever read this can just avoid suffering the same painful test.
Here's the code(typescript) for disable all the checks in Chrome.
TRACKER_CAL_ID_ENCODED is the calendar ID of which I don't want to uncheck. Since it is not the major part of this question, it is not very carefully commented.
(line update: 2022-jan-31) Aware that the mutationsList.length >= 3 is not accurate, I cannot see how mutationsList.length works.
Extension:
getSelectCalendarNode()
.then(unSelectCalendars)
function getSelectCalendarNode() {
return new Promise((resolve) => {
document.onreadystatechange = function () {
if (document.readyState == "complete") {
const leftSidebarNode = document.querySelector(
"div.QQYuzf[jsname=QA0Szd]"
)!;
new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.target) {
let _selectCalendarNode = document.querySelector("#dws12b.R16x0");
// customized calendars will start loading on 3th+ step, hence 3, but when will they stop loading? I didn't work this out
if (mutationsList.length >= 3) {
// The current best workaround I saw is setTimeout after loading event... There's no event of loading complete.
setTimeout(() => {
observer.disconnect();
resolve(_selectCalendarNode);
}, 1000);
}
}
}
}).observe(leftSidebarNode, { childList: true, subtree: true });
}
};
});
}
function unSelectCalendars(selectCalendarNode: unknown) {
const selcar = selectCalendarNode as HTMLDivElement;
const calwrappers = selcar.firstChild!.childNodes; // .XXcuqd
for (const calrow of calwrappers) {
const calLabel = calrow.firstChild!.firstChild as HTMLLabelElement;
const calSelectWrap = calLabel.firstChild!;
const calSelcted =
(calSelectWrap.firstChild!.firstChild! as HTMLDivElement).getAttribute(
"aria-checked"
) == "true"
? true
: false;
// const calNameSpan = calSelectWrap.nextSibling!
// .firstChild! as HTMLSpanElement;
// const calName = calNameSpan.innerText;
const encodedCalID = calLabel.getAttribute("data-id")!; // const decodedCalID = atob(encodedCalID);
if ((encodedCalID === TRACKER_CAL_ID_ENCODED) !== calSelcted) {
//XOR
calLabel.click();
}
}
console.log(selectCalendarNode);
return;
}
There is no way to make a webpage refresh with Google Apps Script
Possible workarounds:
From the sidebar, provide users a link that redirects them to the Calendar UI webpage (thus a new, refreshed version of it will be opened)
Install a Goole Chrome extension that refreshes the tab in specified intervals

How can I relaunch/update/refresh the card again using Apps Script

I'm having a nightmare doing a lot of scenarios using Apps Script, but nothing works! I have a function that makes a GET request returns an array of cards. Now, sometimes I need this card refreshes again to fetch the new content.
function listTemplatesCards(){
var getAllTemplates = getTemplates();
var allTemplates = getAllTemplates.templates;
var theUserSlug = getAllTemplates.user_slug;
var templateCards = [];
//There are templates
if(allTemplates.length > 0){
allTemplates.forEach(function(template){
templateCards.push(templateCard(template, theUserSlug).build());
});
return templateCards;
}
}
This function is called on onTriggerFunction. Now, if I moved to another card and I wanted to back again to the root but in clean and clear way, I use this but it doesn't work:
//Move the user to the root card again
var refreshNav = CardService.newNavigation().popToRoot();
return CardService.newActionResponseBuilder().setStateChanged(true).setNavigation(refreshNav).build();
Simply, what I want is once the user clicks on Refresh button, the card refreshes/updates itself to make the call again and get the new data.
The only way I've found to do this is to always use a single card for the root. In the main function (named in the appscript.json onTriggerFunction), return only a single card, not an array of cards. You can then use popToRoot().updateCard(...) and it works.
I struggled with this for over a day, improving on Glen Little's answer so that its a bit more clear.
I have my root card to be refreshed defined in a funciton called: onHomepage.
I update the appscript.json manifest to set the homepageTrigger and onTriggerFunction to return the function that builds my root card.
"gmail": {
"homepageTrigger": {
"enabled": true,
"runFunction":"onHomepage"
},
"contextualTriggers":[
{
"unconditional":{},
"onTriggerFunction": "onHomepage"
}
]
}
Then it is as simple as building a gotoRoot nav button function that will always refresh the root page.
function gotoRootCard() {
var nav = CardService.newNavigation()
.popToRoot()
.updateCard(onHomepage());
return CardService.newActionResponseBuilder()
.setNavigation(nav)
.build();
}
As far as gmail addons are considered, cards are not refreshed but updated with new cards. And it is pretty simple.
//lets assume you need a form to be updated
function updateProfile() {
//ajax calls
//...
//recreate the card again.
var card = CardService.newCardBuilder();
//fill it with widgets
//....
//replace the current outdated card with the newly created card.
return CardService.newNavigation().updateCard(card.build());
}
A bad hack that works for my Gmail add-on:
return CardService.newActionResponseBuilder()
.setStateChanged(true) // this doesn't seem to do much. Wish it would reload the add-on
.setNotification(CardService.newNotification()
.setText('Created calendar event')
)
// HACK! Open a URL which closes itself in order to activate the RELOAD_ADD_ON side-effect
.setOpenLink(CardService.newOpenLink()
.setUrl("https://some_site.com/close_yoself.html")
.setOnClose(CardService.OnClose.RELOAD_ADD_ON))
.build();
The contents of close_yoself.html is just:
<!DOCTYPE html>
<html><body onload="self.close()"></body></html>
So, it looks like Google has considered and solved this issue for an ActionResponse which uses OpenLink, but not for one using Navigation or Notification. The hack above is definitely not great as it briefly opens and closes a browser window, but at least it refreshes the add-on without the user having to do so manually.

Is that any option for search tabs in chrome?

that is we have opened many tabs.In that tabs i want to search specific tab. Please tell if any ext or option or add-on in chrome or firefox.
Firefox has this functionality built in. If you just start typing in the URL bar and the first character you type is % followed by a space, the rest of what you type will be treated as a search on the titles and urls of open tabs in all Firefox windows.
I'm not sure if this is the site to be asking for help finding extensions that do end user tasks such as this so I'll answer your question explicitly as well as explain how to do it programatically.
The short answer is, yes one extension that will allow you to do this can be found here:
Tab Title Search
The long answer is, in order to find all tabs with a certain name, you need to use the chrome tabs API
I whipped up a short piece of javascript to demonstrate how to have an extension that will create a popup with a search box that you type the desired tab title into. If the tab is found, it will be listed below the search box. If you click on the listing, you will switch to the tab.
// Function to search for tabs
function searchtabs() {
chrome.tabs.query({
title: ""
},
// Callback to process results
function(results) {
// Place holder for the tab to process
var foundTab = null;
// Text to match against
var queryText = document.getElementById("textToSearchInput").value;
// Div to place divs of matched title in
var queryAnswerDiv = document.getElementById("foundTabsDiv");
// Clear the current children
while (queryAnswerDiv.hasChildNodes()) {
queryAnswerDiv.removeChild(queryAnswerDiv.lastChild);
}
// Iterate over all the results
for (var i = 0; i < results.length; i++) {
// Keep track of the tab that is currently being processed
foundTab = results[i];
// If we have a title containing our string...
if (foundTab.title.indexOf(queryText) > -1) {
// Create a new div
var tabDiv = document.createElement("div");
// Set its content to the tabs title
tabDiv.innerHTML = foundTab.title;
// Let it know what the tabs id is
tabDiv.tabToSwitchTo = results[i].id;
// Allow for users to click on the representing div to switch to it
tabDiv.onclick = function() {
// Make the tab selected
chrome.tabs.update(this.tabToSwitchTo, {
selected: true
});
};
// Append the created div to our answer div
queryAnswerDiv.appendChild(tabDiv);
}
}
});
}
document.addEventListener('DOMContentLoaded', function() {
var inputField = document.getElementById("textToSearchInput");
inputField.focus();
inputField.onkeydown = searchtabs;
});
Also, if this is more what you are looking for rather than the extension that I linked, let me know and I can pack this extension.
Edit:
Fixed an error in using the wrong ID to get the input field as well as not getting the first letter of the title (use indexOf() > -1)
An extension that does this is Tab Hero for Chrome ($0.99 Chrome extension). It searches through all of the open tabs (across multiple windows) and offers to switch to the filtered tab. Try and see if it works for you.

Creating a basic chatbar?

Below is old; look at the updated text at the bottom.
So my friends and I use google docs to communicate while in school, and we setup the challenge to create a working and "efficient" chat bar to communicate with better results. I've been around JavaScript for quite some time, but have never fooled around with Google Apps Script before. We are using the document app for our chats; the code I came up with is as below, but I have a few problems with it:
Errors when a user closes it, then goes to Chat -> Open Chat in the toolbar to re-open, saying, "Error encountered: An unexpected error occurred"; does not specify a line or reason
Needs a hidden element somewhere in the document which can allow users to see what others have typed, but that they can't edit without using the chat box (would add event listener to update chat box when text is ammended)
//Main function, ran when the document first opens.
function onOpen() {
var app = UiApp.createApplication(); //Create a Ui App to use for the chat bar
if(getCurrentUser()=="dev1"||getCurrentUser()=="dev2"){ //user-Id's hidden for privacy
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addItem('Elements', 'displayElements') //Hidden as it is not important for regular use
.addItem('MyID', 'showUser')
.addToUi();
}else{
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addToUi();
}
}
//Creates and returns the chats GUI
function createChatBox(){
var app = UiApp.getActiveApplication()
app.setTitle("Chat Bar (not yet working)");
var vPanel = app.createVerticalPanel().setId('chatPanel').setWidth('100%');
var textArea = app.createTextArea().setId('chatBox').setName('chatBox').setReadOnly(true).setText('').setSize('250px', '450px'); //Read only so they can not edit the text, even if it won't affect overall chat
var textBox = app.createTextBox().setId('messageBox').setName('messageBox').setText('Words');
var chatHandler = app.createServerHandler("sayChat").addCallbackElement(textArea).addCallbackElement(textBox);
var chatButton = app.createButton().setId("sayButton").setText("Say!").addMouseUpHandler(chatHandler);
vPanel.add(textArea);
vPanel.add(textBox);
vPanel.add(chatButton);
app.add(vPanel);
DocumentApp.getUi().showSidebar(app);
return app;
}
//The event handler for when the "Say!" (post) button is pressed. Is probably where the conflict stems from.
function sayChat(eventInfo){
var app = UiApp.getActiveApplication();
var parameter = eventInfo.parameter;
app.getElementById("chatBox").setText(parameter.chatBox+"["+getCurrentUser()+"]: "+parameter.messageBox);
app.getElementById("messageBox").setText("");
return app;
}
//A debug function and a function to tell you the unique part of your email (useless, really)
function showUser(){
DocumentApp.getUi().alert("Your userId is: "+getCurrentUser());
}
//Returns the unique part of a person's email; if their email is "magicuser#gmail.com", it returns "magicuser"
function getCurrentUser(){
var email = Session.getActiveUser().getEmail();
return email.substring(0,email.indexOf("#"));
}
//The Auto-color and displayElements methods are hidden as they contain other user-info. They both work as intended and are not part of the issue.
I do not need someone to rewrite the code (although that'd be greatly appreciated!), but instead point out what I'm doing wrong or suggest something to change/add.
Last, before you suggest it, the google docs chat does not work with our computers. It is not the fault of the document, but probably a compatability error with our browser. It is because of this issue that we are going through this fun yet hasty process of making our own chat method.
Update
I decided to give up on my version of the chat using pure Google Apps Script and help improve my friends version using both G-A-S and HTML. I added image thumbnail/linking support with command /img or /image, along with improved time and counter, and some other behind the scenes updates. Here is a quick screenshot of it:
Magnificent chat programmed from scratch, and no buggy update methods, just a casual refresh database to check for messages and set HTML text-area text. No more buggy getText methods. For each new message in the database, whether targeted toward the user or toward everyone in the chat, we load all the database messages up to a limit (50 messages at a time), then display them. The use of HTML in the messages is key to its appearence and features, such as images.
function getChat() {
var chat = "";
var time = getTime();
var username = getCurrentUsername();
var db = ScriptDb.getMyDb();
var query = db.query({time : db.greaterThan(getJoinTime())}).sortBy('time', db.DESCENDING).limit(50);
var flag = query.getSize() % 2 != 0;
while(query.hasNext()) {
var record = query.next();
if(record.showTo == "all" || record.showTo == getCurrentUsername()) {
var text = record.text;
for(var i = 0; i < text.split(" ").length; i++) {
var substr = text.split(" ")[i];
if(substr.indexOf("http://") == 0 || substr.indexOf("https://") == 0) {
text = text.replace(substr, "<a href='" + substr + "'>" + substr + "</a>");
}
}
var message = "<pre style='display:inline;'><span class='" + (flag? "even" : "odd") + "'><b>[" + record.realTime + "]</b>" + text;
message += "</span></pre>";
chat += message;
flag = !flag;
}
}
//DocumentApp.getUi().alert(getTime() - time);
return chat;
}
I am going to re-do his getChat() method to only check for new messages, and not load every message at each refresh.
First thing to to to get rid of your error message is to create the UiApp in the createChat function instead of onOpen.
I also used a client handler to clear the textBox because it's just more efficient. Here is the modified code :
code removed see updates below
As for your second request I'm not sure I understand exactly what you want to do... could you explain more precisely the behavior you expect ? (this is more a comment than an answer but I used the "answer field" to be more readable)
EDIT : I played a little with this code and came to something that -almost- works... it still needs to be improved but it's worth showing how it works.
I used scriptProperties to store the common part of the conversation, I think that's a good approach but the issue it to know when to update its content. Here is the code I have so far, I keep being open to any suggestion/improvement of course.
code removed, new version below
EDIT 2 : here is a version with an auto update that works quite good, the script updates the chat area automatically for a certain time... if no activity then it stops and wait for a user action. please test (using 2 accounts) and let us know what you think.
note I used a checkBox to handler the autoUpdate, I keep it visible for test purpose but of course it could be hidden in a final version.
EDIT 3 : added a message to warn the user when he's been put offline + changed textBox to colored textArea to allow for longer messages + condition to clear the messageBox so that the warning message doesn't go in the conversation. (set the time out to a very short value for test purpose, change the counter value to restore to your needs)
function onOpen() {
if(getCurrentUser()=="dev1"||getCurrentUser()=="dev2"){ //user-Id's hidden for privacy
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addItem('Elements', 'displayElements') //Hidden as it is not important for regular use
.addItem('MyID', 'showUser')
.addToUi();
}else{
DocumentApp.getUi().createMenu('Chat')
.addItem('AutoColor', 'autoColor')
.addItem('Open Chat', 'createChatBox')
.addToUi();
}
}
function createChatBox(){
ScriptProperties.setProperty('chatContent','');
var app = UiApp.createApplication().setWidth(252);
app.setTitle("Chat Bar");
var vPanel = app.createVerticalPanel().setId('chatPanel').setWidth('100%');
var chatHandler = app.createServerHandler("sayChat").addCallbackElement(vPanel);
var textArea = app.createTextArea().setId('chatBox').setName('chatBox').setReadOnly(true).setText('').setSize('250px', '450px');
var textBox = app.createTextArea().setId('messageBox').setName('messageBox').setText('Start chat...').setPixelSize(250,100).setStyleAttributes({'padding':'5px','background':'#ffffcc'}).addKeyPressHandler(chatHandler);
var clearTextBoxClientHandler = app.createClientHandler().forTargets(textBox).setText('');
textBox.addClickHandler(clearTextBoxClientHandler);
var chatButton = app.createButton().setId("sayButton").setText("Say!").addMouseUpHandler(chatHandler);
var chkHandler = app.createServerHandler('autoUpdate').addCallbackElement(vPanel);
var chk = app.createCheckBox().setId('chk').addValueChangeHandler(chkHandler);
vPanel.add(textArea);
vPanel.add(textBox);
vPanel.add(chatButton);
vPanel.add(chk);
app.add(vPanel);
DocumentApp.getUi().showSidebar(app);
return app;
}
function sayChat(e){
var app = UiApp.getActiveApplication();
var user = '['+getCurrentUser()+'] : ';
if(e.parameter.messageBox=="You have been put offline because you didn't type anything for more than 5 minutes..., please click here to refresh the conversation"){
app.getElementById('messageBox').setText('');// clear messageBox
ScriptProperties.setProperty('chatTimer',0);// reset counter
return app;
}
if(e.parameter.source=='messageBox'&&e.parameter.keyCode!=13){return app};
var content = ScriptProperties.getProperty('chatContent');
ScriptProperties.setProperty('chatContent',content+"\n"+user+e.parameter.messageBox)
app.getElementById("chatBox").setText(content+"\n"+user+e.parameter.messageBox+'\n');
app.getElementById('messageBox').setText('');
app.getElementById('chk').setValue(true,true);
ScriptProperties.setProperty('chatTimer',0);
return app;
}
function autoUpdate(){
var app = UiApp.getActiveApplication();
var content = ScriptProperties.getProperty('chatContent');
var counter = Number(ScriptProperties.getProperty('chatTimer'));
++counter;
if(counter>20){
app.getElementById('chk').setValue(false);
app.getElementById('messageBox').setText("You have been put offline because you didn't type anything for more than 5 minutes..., please click here to refresh the conversation");
return app;
}
ScriptProperties.setProperty('chatTimer',counter);
var content = ScriptProperties.getProperty('chatContent');
app.getElementById("chatBox").setText(content+'*'); // the * is there only for test purpose
app.getElementById('chk').setValue(false);
Utilities.sleep(750);
app.getElementById('chk').setValue(true,true).setText('timer = '+counter);
return app;
}
function showUser(){
DocumentApp.getUi().alert("Your userId is: "+getCurrentUser());
}
function getCurrentUser(){
var email = Session.getEffectiveUser().getEmail();
return email.substring(0,email.indexOf("#"));
}

Can't get same origin iframe body on IE10?

I've created a page with an empty iframe on it. I can then select the iframe document and navigate to it's body:
var iframe = document.getElementsByTagName('iframe')[0];
var doc = iframe.contentDocument || iframe.contentWindow.document;
var body = doc.body;
console.log("Body is", body);
In firefox and chrome this gives me the body object. In IE10 it gives me null.
Here is a Jsbin demonstrating the issue. Open up the JS, Console, Output panels and click "Run With JS".
Two questions:
How do I get access to the iframe's body in a cross-browser manner?
Which is the correct "to-spec" behavior?
I had a similar problem earlier today. It seems IE, at least 9 and 10, doesn't create the iframe body correctly (when I used the developer tools I was able to see a body tag inside the iframe, but like you wasn't able to call it), when there's no specified src. It gives you null cause it doesn't exist.
The answer, to whether there is a cross browser manner to access the iframe's body, is no. BUT, you could use a workaround. First, check if the iframe body exist, if not, then create it.
Your code would look like this:
var iframe = document.getElementsByTagName('iframe')[0];
var doc = iframe.contentDocument || iframe.contentWindow.document;
// The workaround
if (doc.body == null) { // null in IE
doc.write("<body></body>");
}
var body = doc.body;
console.log("Body is", body);
Source: http://forums.asp.net/t/1686774.aspx/1
This code is working for me cross-browser:
var doc=ifr.contentWindow||ifr.contentDocument;
if (doc.document) doc=doc.document;
var body=doc.getElementByTagName("body")[0];
Over a year later but I believe the solution was to call
doc.open()
//make any modifications
doc.close()
//at this point doc.body will not be null
This made things work in a fairly consistent manner cross browser