I'm creating a Chrome extension that should interact with the user selection in the Microsoft Word Online documents - add new highlighting instead of the natural selection highlighting and then remove it.
The problem is that I'm not able to get the user selection: the response for the window.getSelection() returns the result like the selection is empty.
Here are files from my extension:
manifest.json
{
"manifest_version": 2,
"name": "The extension name",
"version": "1.0",
"description": "This extension description",
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"icons": {
"128": "icon.png"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content_script.js"],
"run_at": "document_end",
"all_frames": true
}],
"permissions": ["activeTab"]
}
popup.html
<!doctype html>
<html>
<head>
<script src="popup.js"></script>
</head>
<body>
<div id="wrapper">
<form id="settings" name="settings">
<div id="apply" class="form-row">
<input type="submit" name="apply" value="Apply"/>
</div>
</form>
</div>
</body>
</html>
popup.js
document.addEventListener('DOMContentLoaded', function() {
document.getElementById("settings").addEventListener("submit", function (e) {
e.preventDefault();
chrome.tabs.executeScript(null, {file: 'toolbar.js'}, function() {
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {});
});
);
}, false);
});
toolbar.js
function showToolbar() {
var dom_body = document.getElementsByTagName('body')[0];
var tb_wrapper = document.createElement('div');
tb_wrapper.id = "toolbar_wrapper";
var tb_toolbar_play = document.createElement('button');
tb_toolbar_play.id = "toolbar_play";
tb_toolbar_play.title = "Play";
tb_toolbar_play.value = "Play";
tb_wrapper.appendChild(tb_toolbar_play);
dom_body.appendChild(tb_wrapper);
}
showToolbar();
content_script.js
function playButtonOnClickEventListener(request, sender, sendResponse) {
var toolbar = document.getElementById("toolbar_wrapper");
if (toolbar !== null) {
var toolbar_play_button = document.getElementById("toolbar_play");
toolbar_play_button.addEventListener("click", function (e) {
var selection = window.getSelection();
console.log(selection);
});
}
sendResponse({data: "response", success: true});
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
playButtonOnClickEventListener(request, sender, sendResponse);
});
So, what I want to see in the Chrome Developer tools after the console.log(selection) executes:
What I actually get:
P.S. The extension works perfectly with Google Docs.
I'm not sure how the Word Online editor is built or why you can't use window.getSelection(), but one thing I noticed immediately is that when you select text in it, the editor gives that text a Selected class. So maybe you could take advantage of that somehow? Override the style of the relevant elements?
Possibly you could use a MutationObserver and look for this class name being added to elements. You would have to do some experimenting to see if this works for your needs, and obviously if Microsoft change the class name or behaviour, your extension breaks.
Try selecting some text and putting
document.getElementsByClassName("Selected")[0].innerHTML
in the console.
Here's the solution of the issue. Hope it can be useful for somebody once.
Currently I see the whole picture differently, so I changed the number of files and its content.
manifest.json
{
"manifest_version": 2,
"version": "1.1.1",
"name": "Test name",
"description": "Test description",
"browser_action": {
"default_icon": "icon.png"
},
"icons": {
"128": "icon.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content_script.js"],
"run_at": "document_end",
"all_frames": true
}
],
"permissions": ["activeTab"]
}
content_script.js
//Word Online pages contain several different iframes, so, in order
//to find the one where the text that we are going to work with
//is placed, we have to use the Chrome Runtime API and its
//"sendMessage" method that works in every frame.
//https://developer.chrome.com/apps/runtime#method-sendMessage
chrome.runtime.sendMessage("", function() {
if (checkFrameLocation()) {
showToolbar();
}
});
function checkFrameLocation() {
//So as I work with documents in OneDrive (onedrive.live.com)
//and in Sharepoint (<USER_NAME>-my.shrepoint.com), I found out
//that I need iframes with these URLs only.
var trusted_urls = [
"https://euc-word-edit.officeapps.live.com",
"https://word-edit.officeapps.live.com",
"nl1-word-edit.officeapps.live.com"
];
var result = false;
for (var i = 0; i < trusted_urls.length; i++) {
if (window.location.href.indexOf(trusted_urls[i]) > -1) {
result = true;
}
}
return result;
}
function showToolbar() {
var dom_body = document.body;
var tb_wrapper = document.createElement('div');
tb_wrapper.id = "toolbar_wrapper";
tb_wrapper.style.position = "absolute";
tb_wrapper.style.top = "200px";
tb_wrapper.style.left = "200px";
tb_wrapper.style.backgroundColor = "#ffffff";
tb_wrapper.style.height = "30px";
tb_wrapper.style.width = "50px";
var tb_play = document.createElement('button');
tb_play.id = "toolbar_play_button";
tb_play.title = "Play";
tb_play.innerHTML = "►";
tb_play.style.height = "100%";
tb_play.style.width = "100%";
tb_play.style.lineHeight = "12px";
tb_play.addEventListener("click", function() {
playButtonOnClickEventListener();
});
tb_wrapper.appendChild(tb_play);
dom_body.appendChild(tb_wrapper);
}
function playButtonOnClickEventListener() {
//Now we can use the window selection object
var window_selection = window.getSelection();
console.log(window_selection);
//Also, Word Online adds special class name to every selected element
//in the document. So, this is another way to get the selected text.
var elements_selection = document.getElementsByClassName("Selected");
console.log(elements_selection);
}
And here's the proof screenshot:
There are results of accessing the selected text in the Word Online document
Related
I'm wondering if it's possible to allow the user to upload an image to the extension and have that image display as a background image.
Let the user select a file, with <input type="file">
Turn the file into a data URL
Save the data URL with chrome.storage.local.set
Use a content script to replace the background image in web pages
Potential Problem: https://developer.chrome.com/docs/extensions/mv3/declare_permissions/#unlimitedStorage
Note: This permission applies only to Web SQL Database and application
cache (see issue 58985). Also, it doesn't currently work with wildcard
subdomains such as http://*.example.com.
Proof of concept:
https://github.com/GrippenDynamik/Set_Background_Image
manifest.json
{
"manifest_version": 3,
"name": "Set Background Image",
"version": "1.0",
"action": {
"default_title": "Set Background Image"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["/js/content_script.js"]
}
],
"permissions": [
"storage"
]
}
background.js
async function action_onClicked(tab, onClickData) {
chrome.tabs.create({
active: true,
url: chrome.runtime.getURL("/html/file_picker.html"),
});
}
chrome.action.onClicked.addListener(action_onClicked);
/html/file_picker.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<label for="file_picker">Choose an image to upload</label><br>
<input type="file" id="file_picker" accept="image/*"><br>
<br>
<button type="button" id="button_clear">Clear background image</button>
<br>
<img id="image"><br>
<script type="application/javascript" src="/js/file_picker.js"></script>
</body>
</html>
/js/file_picker.js
function storage_onChanged(changes, areaName) {
let settings = {};
if (areaName == "local") {
if (changes.image_data_url) {
settings.image_data_url = changes.image_data_url.newValue ?? "";
}
if (changes.image_filename) {
settings.image_filename = changes.image_filename.newValue ?? "none";
}
}
set_image(settings);
}
function set_image(settings) {
let image = document.getElementById("image");
if (settings.image_data_url !== undefined) { image.src = settings.image_data_url; }
if (settings.image_filename !== undefined) { image.alt = settings.image_filename; }
}
function remove_image() {
chrome.storage.local.remove(["image_data_url", "image_filename"]);
}
function store_image() {
if (this.files.length > 0) {
const reader = new FileReader();
reader.addEventListener("load", () => {
chrome.storage.local.set({"image_data_url": reader.result, "image_filename": this.files[0].name});
});
reader.readAsDataURL(this.files[0]);
}
}
// Initialization
chrome.storage.local.get(["image_data_url", "image_filename"])
.then(items => {
set_image({image_data_url: items.image_data_url ?? "", image_filename: items.image_filename ?? "none"});
}
);
document.getElementById("file_picker").addEventListener("change", store_image);
document.getElementById("button_clear").addEventListener("click", remove_image);
chrome.storage.onChanged.addListener(storage_onChanged);
/js/content_script.js
function storage_onChanged(changes, areaName) {
if (areaName == "local" && changes.image_data_url) {
set_background_image(changes.image_data_url.newValue, true);
}
}
function set_background_image(data_url, changed) {
if (data_url) {
// https://www.w3schools.com/jsref/prop_style_backgroundimage.asp
document.body.style.backgroundImage = "url('" + data_url + "')";
}
else if (changed) {
document.body.style.backgroundImage = "initial";
}
else {
console.log("You haven't 'uploaded' an image yet. Please click the extension action.");
}
}
// Initialization
chrome.storage.local.get("image_data_url")
.then(items => set_background_image(items.image_data_url, false));
chrome.storage.onChanged.addListener(storage_onChanged);
For my Chrome Extension,
I'm searching multiple classes/id's to then include their text with some HTML I'm inserting into the website.
When I run the extension,
the HTML widget I'm inserting is inserted multiple times but should only be done once.
I realize MutationObserver is intended to check for every DOM change and seemingly inserts my widget every time that happens I presume. However, I'm not sure how best to approach making sure the widget is only inserted once.
I tried using observer.disconnect(); which helps on initial load to insert the widget once but then it won't re-insert the widget on return to the page – will only on refresh. Couldn't figure out a way to turn observe back on.
Not sure if disconnect and reconnect is even a smart path but it's what I came up with.
In the attached screenshot, you can see it keeps adding the widget with DOM changes.
How should I approach fixing this?
Thank you!
Link to download the extension
manifest.json
{
"manifest_version": 3,
"name": "Test SO Mutation Answer",
"description": "Example for StackOverflow",
"version": "0.0.1",
"host_permissions": ["<all_urls>"],
"permissions": ["storage", "activeTab", "scripting", "tabs", "webNavigation"],
"content_scripts": [
{
"matches": ["https://www.zillow.com/*"],
"js": ["ballpark.js"],
"css": ["main.css"]
}
],
"web_accessible_resources": [
{
"resources": ["/images/*"],
"matches": ["<all_urls>"]
}
]
}
ballpark.js
const observer = new MutationObserver(function() {
if (document.getElementsByClassName('ds-action-bar')[0]) {
if (document.querySelector('[data-testid="price"] span') && document.querySelector('.ds-expandable-card .eUxMDw')) {
console.log("All data found");
startWidget();
if (document.getElementById('ballpark-container')) {
console.log("Do nothing, BP exists");
} else {
console.log("Adding BP...");
insertWidget();
}
} else {
console.log("Cannot find all data");
}
} else {
offerPrice = undefined;
monthlyPITI = undefined;
}
})
const target = document.querySelector('body');
const config = { childList: true };
observer.observe(target, config);
var offerPrice;
var monthlyPITI;
function startWidget() {
getAskingPrice();
getExpenses();
}
// Get Price from Zillow
function getAskingPrice() {
var askingPrice = document.querySelector('[data-testid="price"] span');
if(askingPrice !== null) {
offerPrice = parseFloat(askingPrice.innerText.replace(/\$|,/g, ''));
console.log(offerPrice + " Offer Price");
} else {
console.log("Null Asking Price Div");
}
}
function getExpenses() {
var monthlyPITIZillow = document.querySelector('.ds-expandable-card .eUxMDw');
if (monthlyPITIZillow !== null) {
monthlyPITI = parseFloat(monthlyPITIZillow.innerText.replace(/\$|,/g, ''));
console.log(monthlyPITI + " PITI");
} else {
console.log("Null Monthly PITI Div");
}
}
// Find Zillow widget to insert the extension widget
function insertWidget() {
const select_div_for_bpd = document.querySelector('div.Spacer-c11n-8-65-2__sc-17suqs2-0');
if(select_div_for_bpd !== null) {
const ballpark_container = document.createElement("div");
ballpark_container.setAttribute("id", "ballpark-container");
select_div_for_bpd.appendChild(ballpark_container);
ballpark_container.innerHTML = `
<div class="ballpark-roi-container">
<div><h1>${offerPrice}</h1> Offer Price</div>
<div><h1>${monthlyPITI}</h1> Monthly PITI</div>
</div>
`;
} else {
console.log("Cannot insert your widget");
}
}
I am trying to read json of post request using Chrome Extension. So i have created a extension with following code:-
menifest.json file:-
{
"name": "numera name",
"version": "1.0",
"description": "numera",
"manifest_version": 2,
"content_scripts": [
{
"matches": ["<all_urls>"],
"run_at": "document_start",
"js": ["contentScript.js"]
}
],
"permissions": [
"activeTab",
"tabs"
]
}
contentScript.js
function interceptData() {
var xhrOverrideScript = document.createElement('script');
xhrOverrideScript.type = 'text/javascript';
xhrOverrideScript.innerHTML = '
(function() {
var XHR = XMLHttpRequest.prototype;
var send = XHR.send;
var open = XHR.open;
XHR.open = function(method, url) {
this.url = url; // the request url
return open.apply(this, arguments);
}
XHR.send = function() {
this.addEventListener('load', function() {
if (this.url.includes('WaitForResponsesData')) {
var dataDOMElement = document.createElement('div');
dataDOMElement.id = '__interceptedData';
dataDOMElement.innerText = this.response;
console.log(this.response);
dataDOMElement.style.height = 0;
//dataDOMElement.style.overflow = 'hidden';
document.body.appendChild(dataDOMElement);
}
});
return send.apply(this, arguments);
};
})();'
document.head.prepend(xhrOverrideScript);
}
function checkForDOM() {
if (document.body && document.head) {
interceptData();
} else {
requestIdleCallback(checkForDOM);
}
}
requestIdleCallback(checkForDOM);
function scrapeData() {
var responseContainingEle = document.getElementById('__interceptedData');
if (responseContainingEle) {
var response = JSON.parse(responseContainingEle.innerHTML);
} else {
requestIdleCallback(scrapeData);
}
}
requestIdleCallback(scrapeData);
But I am not sure why this code is not working i have added console log and alert as well not no luck,
Also i have tried to search __interceptedData ID in page HTML but that is also not there.
I followed this URL https://medium.com/better-programming/chrome-extension-intercepting-and-reading-the-body-of-http-requests-dd9ebdf2348b
please help me out from this
How can I use jQuery if($(this) to update my extensions badge? I've tried the code below and it doesn't work.
jQuery("a[onclick*='ga']").removeAttr('onclick');
if($(this).attr('onclick') == 'ga'){
var add = browser.runtime.sendMessage(message, 'Removed += 1')
}
It should add +1 for each element matching a[onclick='ga'] because that's what it's going to block.
The required scripts for the extension:
manifest.json
{
"manifest_version": 2,
"name": "Anti Click Tracking",
"version": "2.2.4",
"description": "Remove Known HTML <a> and <link> Click Tracking Attributes.",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"js": [
"/jquery/jquery-3.4.0.min.js", "remove.js"
],
"matches": [
"<all_urls>"
]
}
],
"permissions": ["<all_urls>", "webRequest", "webRequestBlocking"],
}
background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (!message) return
if (message <= 0) return
const text = message.toString()
chrome.tabs.get(sender.tab.id, function(tab) {
// Pre-rendered tab has been nuked
if (chrome.runtime.lastError) return
// Tab is visible
if (tab.index >= 0) {
chrome.browserAction.setBadgeText({
tabId: tab.id,
text: text
})
return
}
// Invisible pre-rendered tab
const tabId = sender.tab.id
chrome.webNavigation.onCommitted.addListener(function update(details) {
if (details.tabId == tabId) {
chrome.browserAction.setBadgeText({
tabId: tabId,
text: text
})
chrome.webNavigation.onCommitted.removeListener(update)
}
})
})
})
remove.js
// Remove ping attribute from <a> (somewhat based on PingBlock)
window.onload = function() {
let Removed = 0
const anchorElements = document.getElementsByTagName('A')
for (element of anchorElements) {
if (!element.getAttribute('ping')) continue
console.log("Removed ping: " + element.getAttribute('ping'))
element.removeAttribute('ping')
Removed += 1
chrome.extension.sendMessage(Removed)
}
}
// Remove Link Tracking
link();
function link(){
jQuery("a[onclick*='ga']").removeAttr('onclick');
jQuery("a[onclick*='_gaq.push']").removeAttr('onclick');
jQuery("link[rel*='pingback']").removeAttr('rel');
}
All this code together only updates the badge when the ping attribute is blocked. I need it to also update the badge when jQuery removes the selected attributes. Is it possible to use the Removed += 1 like I did under the first part for jQuery or will I have to change it to something else?
I'm trying to do an extension with this behaviour: When I click the extension button, it parses the current page and executes a function for each found item.
background.js
function downloadPage(urlpage,name){
chrome.tabs.create({
url: urlpage
}, function(tab) {
chrome.tabs.onUpdated.addListener(function func(tabId, changeInfo) {
if (tabId == tab.id && changeInfo.status == 'complete') {
chrome.tabs.onUpdated.removeListener(func);
savePage(tabId, name);
}
});
});
}
function savePage(tabId, name) {
chrome.pageCapture.saveAsMHTML({
tabId: tabId
}, function(blob) {
var url = URL.createObjectURL(blob);
// Optional: chrome.tabs.remove(tabId); // to close the tab
chrome.downloads.download({
url: url,
filename: 'prueba/test.mht'
});
});
}
popup.js
var candidatos = document.getElementsByClassName("user-name");
if(candidatos != null){
for (i = 0; i < candidatos.length; i++) {
chrome.extension.getBackgroundPage().downloadPage(candidatos[i].href, candidatos[i].text)
}
}
manifest.json
{
"manifest_version": 2,
"name": "Test",
"description": "Test",
"version": "1.0",
"author": "Pablo",
"background": {
"scripts": ["background.js"]
},
"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},
"permissions": [
"activeTab",
"tabs",
"webNavigation",
"contextMenus",
"downloads",
"<all_urls>",
"pageCapture"
]
}
I can't get to run anything when I click the extension button.
Thanks in advance.
Updated:
I have run your code at my end, when you are saying "I can't get to run anything when I click the extension button", I guess you mean savePage function in not called. That is because in your callback for chrome.tabs.create function, you register chrome.tabs.onUpdated listener, it is too late. I recommend you just use the tab you get from the callback and do what you want.
function downloadPage(urlpage,name){
chrome.tabs.create({
url: urlpage
}, function(tab) {
savePage(tab.id, name);
});
}
It's recommended to read the Official Guide especially browser action. The basic part should be registering the browser action listener and listening to onClicked event.
chrome.browserAction.onClicked.addListener(function () {
// Do what you want when clicking the 'extension icon'
});
Your fundamental problem is that popup.js refers to the DOM of popup.html, which doesn’t have the class or code that you need. In order to interact with the DOM, you need a content script. The best approach for that would be to have the background page programmatically inject it. Then you can also use the callback parameters to avoid having to use message passing:
chrome.tabs.query({active:true},function(tabArray){
chrome.tabs.executeScript(tabArray[0].id,
{code:"document.getElementsByClassName('user-name');"},
allFrames);
});
// the parameter to allFrames is the result of code in each frame of the tab
function allFrames(frames) {
for ( var i = 0 ; i < frames.length ; i++ ) {
var classes = frames[i];
for ( var j = 0 ; j < classes.length ; j++ ) {
downloadPage(classes[j].href,classes[j].text);
}
}
}
#HaibaraAi’s point is that if the only purpose of the popup is to trigger an action in the background, then it’s much easier to omit the popup and simply have:
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(tab.id,
{code:"document.getElementsByClassName('user-name');"},
allFrames);
});
And because tabs.create gives you a completely loaded tab, you don’t need to listen for its completion. So we can stick with #HaibaraAi’s version of that function. The completed example is therefore
chrome.browserAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(tab.id,
{code:"document.getElementsByClassName('user-name');"},
allFrames);
});
function allFrames(frames) {
for ( var i = 0 ; i < frames.length ; i++ ) {
var classes = frames[i];
for ( var j = 0 ; j < classes.length ; j++ ) {
downloadPage(classes[j].href,classes[j].text);
}
}
}
function downloadPage(urlpage,name){
chrome.tabs.create({
url: urlpage
}, function(tab) {
savePage(tab.id, name);
});
}
With your original savePage. And you no longer need the popup. (Also, the activeTab permission is contained in the tabs permission, so you don’t need that.)