Updating a Persistent Window from Javascript Part 1: postMessage
I'm trying to update my htmlpreso HTML presentation slide system to allow for a separate notes window.
Up to now, I've just used display mirroring. I connect to the projector at 1024x768, and whatever is on the first (topmost/leftmost) 1024x768 pixels of my laptop screen shows on the projector. Since my laptop screen is wider than 1024 pixels, I can put notes to myself to the right of the slide, and I'll see them but the audience won't.
That works fine, but I'd like to be able to make the screens completely separate, so I can fiddle around with other things while still displaying a slide on the projector. But since my slides are in HTML, and I still want my presenter notes, that requires putting the notes in a separate window, instead of just to the right of each slide.
The notes for each slide are in a <div id="notes">
on each page. So all I have to do is pop up another browser window
and mirror whatever is in that div to the new window, right?
Sure ...
except this is JavaScript, so nothing is simple. Every little thing
is going to be multiple days of hair-tearing frustration, and this
was no exception.
I should warn you up front that I eventually found a much simpler way of doing this. I'm documenting this method anyway because it seems useful to be able to communicate between two windows, but if you just want a simple solution for the "pop up notes in another window" problem, stay tuned for Part 2.
Step 0: Give Up On file:
Normally I use file: URLs for presentations. There's no need to run a web server, and in fact, on my lightweight netbook I usually don't start apache2 by default, only if I'm actually working on web development.
But most of the methods of communicating between windows don't work in file URLs, because of the "same-origin policy". That policy is a good security measure: it ensures that a page from innocent-url.com can't start popping up windows with content from evilp0wnU.com without you knowing about it. I'm good with that. The problem is that file: URLs have location.origin of null, and every null-origin window is considered to be a different origin -- even if they're both from the same directory. That makes no sense to me, but there seems to be no way around it. So if I want notes in a separate window, I have to run a web server and use http://localhost.
Step 1: A Separate Window
The first step is to pop up the separate notes window, or get a handle to it if it's already up.
JavaScript offers window.open()
, but there's a trick:
if you just call
notewin = window.open("notewin.html", "notewindow")
you'll actually get a new tab, not a new window. If you actually
want a window, the secret code for that is to give it a size:
notewin = window.open("notewin.html", "notewindow", "width=800,height=500");
There's apparently no way to just get a handle to an existing window.
The only way is to call window.open()
,
pop up a new window if it wasn't there before, or reloads it if it's
already there.
I saw some articles implying that passing an empty string ""
as the first argument would return a handle to an existing window without
changing it, but it's not true: in Firefox and Chromium, at least,
that makes the existing window load about:blank instead of
whatever page it already has. So just give it the same page every time.
Step 2: Figure Out When the Window Has Loaded
There are several ways to change the content in the popup window from
the parent, but they all have one problem:
if you update the content right away after calling window.open
,
whatever content you put there will be overwritten immediately when
the popup reloads its notewin.html page (or even about:blank).
So you need to wait until the popup is finished loading.
That sounds suspiciously easy. Assuming you have a function called
updateNoteWinContent()
, just do this:
// XXX This Doesn't work: notewin.addEventListener('load', updateNoteWinContent, false);
Except it turns out the "load" event listener isn't called on reloads, at least not in popups. So this will work the first time, when the note window first pops up, but never after that.
I tried other listeners, like "DOMContentLoaded" and "readystatechange", but none of them are called on reload. Why not? Who knows? It's possible this is because the listener gets set too early, and then is wiped out when the page reloads, but that's just idle speculation.
For a while, I thought I was going to have to resort to an ugly hack:
sleep for several seconds in the parent window to give the popup time
to load:
await new Promise(r => setTimeout(r, 3000));
(requires declaring the calling function as async
).
This works, but ... ick.
Fortunately, there's a better way.
Step 2.5: Simulate onLoad with postMessage
What finally worked was a tricky way to use postMessage()
in reverse. I'd already experimented with using postMessage()
from the parent window to the popup, but it didn't work because the
popup was still loading and wasn't ready for the content.
What works is to go the other way. In the code loaded by the popup (notewin.html in this example), put some code at the end of the page that calls
window.opener.postMessage("Loaded");
Then in the parent, handle that message, and don't try to update the popup's content until you've gotten the message:
function receiveMessageFromPopup(event) { console.log("Parent received a message from the notewin:", event.data); // Optionally, check whether event.data == "Loaded" // if you want to support more than one possible message. // Update the "notes" div in the popup notewin: var noteDiv = notewin.document.getElementById("notes"); noteDiv.innerHTML = "Here is some content.
"; } window.addEventListener("message", receiveMessageFromPopup, false);
Here's a complete working test: Test of Persistent Popup Window.
In the end, though, this didn't solve my presentation problem. I got it all debugged and working, only to discover that postMessage doesn't work in QtWebEngine, so I couldn't use it in my slide presentation app. Fortunately, I found a couple of other ways: stay tuned for Part 2.
(Update: Part 2: A Clever Hack.)
Debugging Multiple Windows: Separate Consoles
A note on debugging:
One thing that slowed me down was that JS I put in the popup didn't
seem to be running: I never saw its console.log()
messages.
It took me a while to realize that each window has its own web console,
both in Firefox and Chromium. So you have to wait until the popup has
opened before you can see any debugging messages for it. Even then,
the popup window doesn't have a menu, and its context menu doesn't
offer a console window option. But it does offer Inspect element,
which brings up a Developer Tools window where you can click on
the Console tab to see errors and debugging messages.
[ 20:29 Jan 05, 2020 More tech/web | permalink to this entry | ]