19
Monitoring HTML Elements For Change
While trying to build a simple clipboard copy button, I found the need to in-line styles if they are to survive emailing, and that in-lining could, for very large elements be rather costly (in time), of which was born a need to let UI events happen while doing the job. But because my website is interactive, the element being copied might change while the styles are being in-lined!
In the previous post I introduced the code base to which I begin to refer in these notes. That digression was needed to make sense of the ongoing notes as they refer in greater detail to actual implementation details.
A couple of things are worth noting here:
If I click the copy button, I want a copy of what we see on the browser now, after the change we made. But if the style in-liner is running when I click copy button, some most surprising things can happen, because Copy With Style is nice enough to
await defer_to_UI()
at intervals I've managed to change the element while it's running. Turns out that if I time this right I can get a copy that is half how the element was, and half how it is. A kind of chimera element.
To help visualise this, in a standard use-case of mine I might be presenting a whole pile of tables (they'd be leaderboards in games in case it's important, lists of players and scores), and I might want to highlight certain players, or add or remove columns from these boards and the site has plenty of options to do just that. If I make such a change within 40 odd seconds of the page being presented (during which the initial render of a very large number of such leaderboards is having their styles in-lined ready to arm a copy button) and then click the copy button, I get this chimera of a copy. The first half before the change and the last half after depending on exactly when I change options, or how many times even.
So we need to watch the element for changes, and if it changes, we have to start style in-lining process a new.If we're making changes though and every change we make kick fires a new in-lining of styles, we could see an explosion of in-lining jobs on the event queue. And so we need not just to start style in-lining again we also need some way of telling any running style in-liner to finish, quit, stop, bail.
A MutationObserver is handy for this. We can create one with a handler, that is run every time an element changes. That handler can check to see if a style in-liner is running, and if so ask it to stop, and wait until it has, before starting a new style in-liner.
In the implementation of Copy_With_Style
, #observe_element
instantiates this MutationObserver and associates it with the element (this.element
). The observer is stored as #observer
and #mutation_handler
is the handler called when #observer
notices that element
changed.
A little synchronisation is needed here. If scheduling the preparation for copy (i.e. in-lining of element styles) then we don't want to start watching for changes until the style in-liner has started (there will be a lot of them, changes, that is, to the element, while the page is being rendered).
To wit, even if we're not scheduling a style in-lining when the DOM is ready, we do not want to start observing the element until the DOM is ready (to avoid a flood change observations). So two schedulers are implemented #schedule_preparation
which schedules the style in-liner to start when the DOM is ready and #schedule_observation
which schedules an observer to start when the DOM is ready. These are started by the constructor
based on the configured triggers
in Copy With Style.
All the communications required (for one event-triggered function to request a waiting function to bail, and then waiting for it to do so, before starting it anew) takes place with properties of the Copy_With_Style
instance.
Specifically it uses:
-
#is_being_prepared
which is set to true when the in-liner (prepare_copy
) starts. -
#is_prepared
which is set to true when it's completed successfully andthis.HTML
andthis.text
are set and ready to copied to the clipboard. -
#bail
which is set by the observer whenthis.element
changes and the style in-liner is running (#is_being_prepared
is true), to ask it to drop what it's doing and bail. -
#bailed
is set by the style in-liner to let the observer know it's done that (which it was requested to do).
Remember that JavaScript is (essentially) single threaded and that this requires a to and fro dance with await defer_to_UI()
which takes on a broader meaning now, and could rightly be thought of, or called, defer_to_others()
as we use it for one function to communicate with another function.
The observer when running must set #bail
then await defer_to_UI()
to re-queue itself and give the style in-liner a chance to run and and it in turn checks #bail
whenever it continues after an await defer_to_UI()
. If it sees #bail
set, it bails, setting #bailed
first then returns (and thus comes off the queue and is no longer awaiting, has stopped processing elements). Eventually the observer's turn comes up again and it can check if #bailed
is set. If not of course it can await defer_to_UI()
again as often as needed until it is. In practice I have never seen it not honoured immediately - when the observer defers to UI it goes to the end of the queue and by definition a callback to the awaiting style in-liner is already on the queue ahead of it.
The only way that the observer might need a second check of #bailed
is if the in-liner fails to check #bail
and act on it when it is called back after it's deferral to UI.
Currently that only issues a warning to the console if it's encountered, but I've not seen it happen yet and if it did happen it suggests a bug.
19