Cannot set data to clipboard after getting data from web worker - javascript

When trigger copy event (cmd + c), our application needs to call and get data from web worker, and then set it to clipboard.
But after the data is ready, setting data to clipboard doesn’t seem to work.
async function process_copy(event) {
let data = await get_data_from_worker();
console.log("DATA FROM WORKER: ", data);
event.clipboardData.setData('text/plain', data);
event.clipboardData.setData('text/html', data);
event.preventDefault();
}
window.addEventListener('copy', process_copy.bind(this));
What I need is to set data to clipboard since the data from web worker is available for use.
The reason why I can’t use this command
document.execCommand('copy’)
Because time to get data from web worker may take more than 5 secs, and the command above doesn’t work in these cases.
Here is an example:
worker.js
onmessage = function(e) {
postMessage('WORKER DATA');
}
index.html
<!DOCTYPE html>
<html>
<body>
<script>
window.onload = function() {
const my_worker = new Worker("worker.js");
let call_back;
my_worker.onmessage = function(e) {
if(call_back){
call_back(e.data);
}
call_back = undefined;
}
function get_data_from_worker() {
return new Promise(
function(resolve, reject) {
call_back = resolve;
my_worker.postMessage("GET DATA");
}
)
}
async function process_copy(event) {
let data = await get_data_from_worker();
console.log("DATA FROM WORKER: ", data);
event.clipboardData.setData('text/plain', data);
event.clipboardData.setData('text/html', data);
event.preventDefault();
}
window.addEventListener('copy', process_copy.bind(this));
};
</script>
</body>
</html>
After users trigger the copy event, It calls to process_copy function, and waits for data.
In get_data_from_worker function, I have created a promise, which sends message to web worker, and then store resolve in call_back for later use.
When the web worker receive the message, it prepares data and send back, through postMessage method.
Then, the web worker message will be returned by call_back (inside my_worker.onmessage).
After that, the data is ready in process_copy function. But We can't set that data to clipboard.

You correctly identified the problem: you need to handle the event synchronously to be able to overwrite its default behavior.
You can workaround that issue by redesigning the workflow.
If your data really needs 5s to be generated, then you will most probably need two clicks from your users:
prepare the data
copy the data to clipboard
You don't need to actually handle their copy event to be able to set the data in there, so clicks will do, however, clicks are needed because the browsers won't let us copy anything in the clipboard without an user gesture, and after 5s most browsers will consider the user-gesture dead already.
btn.onclick = async (evt) => {
// from first click we prepare the data
btn.disabled = true;
btn.textContent = "Please wait";
// simulate waiting for worker
await wait(1000);
const datatext = "data as text";
const datahtml = "<h1>data as html</h1>";
// now that the data is ready
// we wait for the second click
btn.disabled = false;
btn.textContent = "Copy to clipboard";
btn.onclick = async (evt) => {
// we prepare to handle the click event
// so we can overwrite its content
addEventListener("copy", evt => {
evt.preventDefault();
evt.clipboardData.setData("text/plain", datatext);
evt.clipboardData.setData("text/html", datahtml);
}, { once: true });
// we force the copy event (we don't care of the content here)
document.execCommand("copy");
btn.remove();
pastezone.classList.remove("hidden");
};
};
pastezone.addEventListener("paste", (evt) => {
console.log("as text:", evt.clipboardData.getData("text/plain"));
console.log("as html:", evt.clipboardData.getData("text/html"));
});
function wait(ms) {
return new Promise( (res) => setTimeout(res, ms) );
}
.hidden { display: none; }
<button id="btn">Prepare data to copy</button>
<textarea id="pastezone">You can paste here to test
</textarea>
And if you only need to write text and need tosupport IE, you could also use the Async Clipboard API instead of document.execCommand, but this won't work here in StackOverflow's sandboxed snippets.

Related

Failed to execute 'put' on 'IDBObjectStore': The transaction has finished

I am trying to update an entry in my simple to do app with indexedDB, however I am getting Failed to execute 'put' on 'IDBObjectStore': The transaction has finished.
I can't seem to figure out why it won't finish the transaction, I tried the debugger and it stops at this line: var updateNameRequest = tasksStore.put( requestForItem.result.name, Number(requestForItem.result.id)) Please see the snippet I included below. For additional context creating, reading, and deleting work just fine it's just updating data that I'm having trouble with
I also tried to implement the openCursor technique which I got from Mozilla which I commented out since it also doesn't work (I get the same behavior) Check out my repo I know it's still very messy :(
const request = window.indexedDB.open("toDoList", 2);
var db;
request.onsuccess = function (event) {
console.log("check out some data about our opened db: ", request.result);
db = event.target.result; // result of opening the indexedDB instance "toDoList"
getTasks(); //just a function to retrieve data
};
$(document).on("click", ".editBtn", function () {
var transaction = db.transaction("tasks", "readwrite");
var tasksStore = transaction.objectStore("tasks");
console.log(tasksStore);
let taskId = $(this).attr("idNo");
var requestForItem = tasksStore.get(Number(taskId));
requestForItem.onsuccess = function () {
// console.log(requestForItem.result)
var oldData = requestForItem.result;
// prepopulate the input
$(".editInput").val(requestForItem.result.name);
$(".saveBtn").click(function () {
requestForItem.result.name = $(".editInput").val().trim()
console.log( requestForItem.result)
var updateNameRequest = tasksStore.put( requestForItem.result.name, Number(requestForItem.result.id))
console.log("-------------", updateNameRequest.transaction) // doesn't get to this line
updateNameRequest.onerror = function() {
console.log("something went wrong")
console.log(updateNameRequest.error)
};
updateNameRequest.onsuccess = function() {
console.log("here")
$(".editInput").val("")
getTasks();
};
});
};
Indexed DB transactions auto-commit when all requests have been completed and no further requests were made before control returns to the event loop. Put another way - you can make new requests in the success or error callback from a previous request, but not in other asynchronous callbacks such as event handlers.
You need to start a new transaction within the click handler, because any previous transaction will have autocommitted.

Waiting for one event handler before executing another one

I'm writing a Firefox browser extension, and I'm stuck on how to wait for a content script to load before sending a message from the background script.
This is the sequence I'm trying to achieve:
User clicks context menu item (click handler is in background script)
Background script creates new tab
Content script loads fully in new tab
Background script sends message (with data) to content script
Content script uses data
Obviously, the content script needs to be loaded for step 4 to work; otherwise, the message doesn't get received.
I looked at previous similar questions, but most of the answers are incorrect (they wrap the event listener methods in a Promise, which either results in too many listeners or too few Promises), or they seem not-applicable to my scenario (those answers get around the question entirely by putting one callback inside the other; that wouldn't work here).
What I did try so far was to have the content script send a message when it's ready, and that works, but I'm still not sure how to have the click handler (from step 1) wait for a message from the content script (hypothetical step 3.5).
I assume I'd have to define the message handler outside the click handler, as far as I know, unless there's a way to receive the message inside the click handler.
Here's my current code as a minimal working example:
background.js:
let ports = {
'1': null,
'2': null
};
xyz = () => { /*...*/ }
tabHasLoaded = () => { /*...*/ }
browser.runtime.onConnect.addListener(connectHandler);
connectHandler = (p) => {
ports[p.name] = p;
switch (p.name) {
case '1':
ports['1'].addListener(xyz);
break;
case '2':
ports['2'].addListener(tabHasLoaded);
break;
}
};
browser.contextMenus.onClicked.addListener((info, tab) => {
let data, uri;
//...
browser.tabs.create({
url: uri
}).then((tab) => {
// need to wait for tabHasLoaded() to get called
ports['2'].postMessage({
msg: data
})
});
});
1.js (content script for something else):
let myPort = browser.runtime.connect({
name: '1'
});
document.addEventListener("click", (e) => {
myPort.postMessage({
msg: e.target.id
});
});
2.js (content script for new tab, after clicking context menu):
let myPort = browser.runtime.connect({
name: '2'
});
myPort.postMessage({
msg: "READY" // tabHasLoaded() should now get called in background.js
});
myPort.onMessage.addListener((msg) => {
// waiting for background.js to send me data
});
Is there an ideal way to handle this?
i still think promises are the way to go...
update
change code to use your MWE... please note that this is untested/not-optimized code just to outline the idea... it should look something like this:
background.js
let ports = {
'1': null,
'2': null
};
xyz = () => { /*...*/ }
browser.runtime.onConnect.addListener(connectHandler);
connectHandler = (p) => {
ports[p.name] = p;
switch (p.name) {
case '1':
ports['1'].addListener(xyz);
break;
}
};
browser.contextMenus.onClicked.addListener(async (info, tab) => {
let data, uri;
//...
const tab = await LoadAndWaitForPort2(uri)
ports['2'].postMessage({msg: data})
});
function LoadAndWaitForPort2(uri){
return new Promise((resolve, reject)=>{
const tab
const tabHasLoaded = (evt) => {
if(evt.data.msg === "READY"){
ports['2'].removeListener(tabHasLoaded)
resolve(tab)
} else {
reject("error!")
}
}
ports['2'].addListener(tabHasLoaded)
tab = await browser.tabs.create({url: uri})
})
}
2.js
let myPort = browser.runtime.connect({
name: '2'
});
myPort.postMessage({
msg: "READY" // tabHasLoaded() should now get called in background.js
});
myPort.onMessage.addListener((msg) => {
// waiting for background.js to send me data
});

Chrome Extension: Adding event listener not working after site loads

I'm building a Chrome Extension to add some shortcut functionality to a site I regularly work with. I've tried calling my addTypingListeners() to bind the div with 2 inputs that I've added to the title and subtitle of the edit page I'm working on. However, I never seem to get into the document.eventListener closure.
My Chrome Extension is run at document_idle so the content should be loaded by the time my additional code runs. How can I get these listeners to embed on the page?
Even when I don't call addTypingListeners(), I still see a and b log in the console
function addTypingListeners() {
console.log('a')
var meta = {}
document.addEventListener("DOMContentLoaded",()=>{
console.log('listeners added pre')
bind(meta, document.getElementsByTagName('title'), "title");
bind(meta, document.getElementsByTagName('subtitle'), "subtitle");
setInterval(()=>{document.getElementsByTagName('h3')[0].innerText=meta.title});
setInterval(()=>{
console.log(meta)
document.getElementsByTagName('h4')[0].innerText = meta.subtitle
});
console.log('listeners added')
})
console.log('b')
}
const start = async function() {
// var location = window.location.toString()
let slug = window.location.toString().split("/")[4]
let url = `https://example.org/${slug}?as=json`
const _ = await fetch(url)
.then(res => res.text())
.then(text => {
let obj = JSON.parse(text);
const { payload } = obj;
// Container
const root = document.getElementById('container');
var clippyContainer = document.createElement('div');
createShell(clippyContainer, name);
root.appendChild(clippyContainer);
// Inputs
const title = document.getElementsByTagName('h3')[0];
const subtitle = document.getElementsByTagName('h4')[0];
var inputDiv = document.createElement('div');
inputDiv.id = "input-div";
const titleInput = document.createElement('input');
titleInput.id = "title"
titleInput.value = title.innerText;
inputDiv.appendChild(titleInput);
const breaker = document.createElement("br")
inputDiv.appendChild(breaker);
const subtitleInput = document.createElement('input');
subtitleInput.id = "subtitle"
subtitleInput.value = subtitle.innerText;
inputDiv.appendChild(subtitleInput);
clippyContainer.appendChild(inputDiv);
inputDiv.appendChild(breaker);
// addTypingListeners() // tried here, also doesn't work
});
}
start()
.then( (_) => {
console.log('hi')
addTypingListeners()
console.log("done")
})
Probably the event DOMContentLoaded was already fired at the point of time when you set the listener. You can check that document.readyState equals to complete and execute the function without subscribing to the event if it already occurred. In the opposite case if the readyState is loading or interactive you should set the listener as it is currently done in the attached example.
The code you provided should be injected to the page as a content script (for details).
According to the official documentation, the order of events while a page is loading:
document_start > DOMContentLoaded > document_end > load > document_idle.
The difference between load and DOMContentLoaded events is explained here as
The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
Thus, you should add the listeners without waiting for the DOMContentLoaded event, which will never fire.
This is literally all the coded need besides whatever your doing to the dom.
Background.js
let slug = window.location.toString().split("/")[4]
let url = `https://example.org/${slug}?as=json`
fetch(url).then(res => res.text()).then((data) => {
chrome.tabs.sendMessage(tabId, {
message: data
});
})
Content.js
function addTypingListeners(data) {
// Update page dom
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.message) {
addTypingListeners(request.message);
}
});

IndexedDB's callbacks not being executed inside the 'fetch' event of a Service Worker

I'm trying to do a couple of things in the IndexedDB database inside the 'fetch' event of a service worker, when the aplication asks the server for a new page. Here's what I'm going for:
Create a new object store (they need to be created dynamically, according to the data that 'fetch' picks up);
Store an element on the store.
Or, if the store already exists:
Get an element from the store;
Update the element and store it back on the store.
The problem is that the callbacks (onupgradeneeded, onsuccess, etc) never get executed.
I've been trying with the callbacks inside of each other, though I know that may not be the best approach. I've also tried placing an event.waitUntil() on 'fetch' but it didn't help.
The 'fetch' event, where the function registerPageAccess is called:
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
event.waitUntil(function () {
const nextPageURL = new URL(event.request.url);
if (event.request.destination == 'document') {
if (currentURL) {
registerPageAccess(currentURL, nextPageURL);
}
currentURL = nextPageURL;
}
}());
/*
* some other operations
*/
return response || fetch(event.request);
})
);
});
registerPageAccess, the function with the callbacks.
I know it's plenty of code, but just look at secondRequest.onupgradeneeded in the 5th line. It is never executed, let alone the following ones.
function registerPageAccess(currentPageURL, nextPageURL) {
var newVersion = parseInt(db.version) + 1;
var secondRequest = indexedDB.open(DB_NAME, newVersion);
secondRequest.onupgradeneeded = function (e) {
db = e.target.result;
db.createObjectStore(currentPageURL, { keyPath: "pageURL" });
var transaction = request.result.transaction([currentPageURL], 'readwrite');
var store = transaction.objectStore(currentPageURL);
var getRequest = store.get(nextPageURL);
getRequest.onsuccess = function (event) {
var obj = getRequest.result;
if (!obj) {
// Insert element into the database
console.debug('ServiceWorker: No matching object in the database');
const addRes = putInObjectStore(nextPageURL, 1, store);
addRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully added in the Object Store');
}
addRes.onerror = function (event) {
console.error('ServiceWorker error adding element to the Object Store: ' + addRes.error);
}
}
else {
// Updating database element
const updRes = putInObjectStore(obj.pageURL, obj.nVisits + 1, store);
updRes.onsuccess = function (event) {
console.debug('ServiceWorker: Element was successfully updated in the Object Store');
}
updRes.onerror = function (event) {
console.error('ServiceWorker error updating element of the Object Store: ' + putRes.error);
}
}
};
};
secondRequest.onsuccess = function (e) {
console.log('ServiceWorker: secondRequest onsuccess');
};
secondRequest.onerror = function (e) {
console.error('ServiceWorker: error on the secondRequest.open: ' + secondRequest.error);
};
}
I need a way to perform the operations in registerPageAccess, which involve executing a couple of callbacks, but the browser seems to kill the Service Worker before they get to occur.
All asynchronous logic inside of a service worker needs to be promise-based. Because IndexedDB is callback-based, you're going to find yourself needing to wrap the relevant callbacks in a promise.
I'd strongly recommend not attempting to do this on your own, and instead using one of the following libraries, which are well-tested, efficient, and lightweight:
idb-keyval, if you're okay with a simple key-value store.
idb if you're need the full IndexedDB API.
I'd also recommend that you consider using the async/await syntax inside of your service worker's fetch handler, as it tends to make promise-based code more readable.
Put together, this would look roughly like:
self.addEventListener('fetch', (event) => {
event.waitUntil((async () => {
// Your IDB cleanup logic here.
// Basically, anything that can execute separately
// from response generation.
})());
event.respondWith((async () => {
// Your response generation logic here.
// Return a Response object at the end of the function.
})());
});

How to read Object from Firebase using AngularJS

I am developing my app, and one of the features will be messaging within the application. What I did, is I've developed 'send message' window, where user can send message to other user. The logic behind it is as following:
1. User A sends message to User B.
2. Firebase creates following nodes in 'Messaging':
"Messaging"->"User A"->"User B"->"Date & Time"->"UserA: Message"
"Messaging"->"User B"->"User A"->"Date & Time"->"UserA: Message"
Here is the code that I am using for sending messages:
sendMsg: function(receiver, content) {
var user = Auth.getUser();
var sender = user.facebook.id;
var receiverId = receiver;
var receiverRef = $firebase(XXX.firebase.child("Messaging").child(receiverId).child(sender).child(Date()));
var senderRef = $firebase(XXX.firebase.child("Messaging").child(sender).child(receiverId).child(Date()));
receiverRef.$set(sender,content);
senderRef.$set(sender,content);
},
(picture 1 in imgur album)
At the moment, I am trying to read the messages from the database, and sort them in according to date. What I've accomplished so far, is that I have stored the content of "Messaging/UserA/" in form of an Object. The object could be seen in the picture I've attached (picture 2).
http://imgur.com/a/3zQ0o
Code for data receiving:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
My question is: how can I read the object's messages? I would like to sort the according to the date, get the message and get the Id of user who has sent the message.
Thank you so much!
You seem to be falling for the asynchronous loading trap when you're reading the messages:
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
Messages.on("value", function (snapshot) {
var messagesObj = snapshot.val();
return messagesObj;
}, function (errorObject) {
console.log("Error code: " + errorObject.code);
});
}
That return statement that you have in the Messages.on("value" callback doesn't return that value to anyone.
It's often a bit easier to see what is going on, if we split the callback off into a separate function:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var messagesObj = snapshot.val();
return messagesObj;
},
getMsgs: function () {
var user = Auth.getUser();
var userId = user.facebook.id;
var messagesPath = new Firebase("https://xxx.firebaseio.com/Messaging/");
var Messages = messagesPath.child(userId);
console.log('Before adding on-value listener');
Messages.on("value", onMessagesChanged);
console.log('After adding on-value listener');
}
If you run the snippet like this, you will see that the console logs:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
This is probably not what you expected and is caused by the fact that Firebase has to retrieve the messages from its servers, which could potentially take a long time. Instead of making the user wait, the browser continues executing the code and calls your so-called callback function whenever the data is available.
In the case of Firebase your function may actually be called many times, whenever a users changes or adds a message. So the output more likely will be:
Before adding on-value listener
After adding on-value listener
Inside on-value listener
Inside on-value listener
Inside on-value listener
...
Because the callback function is triggered asynchronously, you cannot return a value to the original function from it. The simplest way to work around this problem is to perform the update of your screens inside the callback. So say you want to log the messages, you'd do:
onMessagesChanged(snapshot) {
// when we get here, either the messages have initially loaded
// OR there has been a change in the messages
console.log('Inside on-value listener');
var i = 0;
snapshot.forEach(function(messageSnapshot) {
console.log((i++)+': '+messageSnapshot.val());
});
},
Note that this problem is the same no matter what API you use to access Firebase. But the different libraries handle it in different ways. For example: AngularFire shields you from a lot of these complexities, by notifying AngularJS of the data changes for you when it gets back.
Also see: Asynchronous access to an array in Firebase

Categories