It all started with this Suggestion.What the OP of the suggestion was doing was using the old Zapimages bookmarklet, made years ago by Jesse Ruderman, and still available at
his site.
I've had that handy little bookmarklet sitting in my toolbar on Firefox for years. What the OP was experiencing was the text in the arrow icons for cuttags not updating. I started looking into why that happened and that led me on a trip through some JavaScript concepts that I hadn't had a practical exposure to. If you're not into code, this will be a boring trip, but I'm chronicling my journey mostly as an exercise in learning and reinforcement.
Original Bookmarklet
First I looked at the original bookmarklet code, which stripped of it's enclosing function and
put through the beautifier looks like this:
The Code
function toArray(c) {
var a, k;
a = new Array;
for (k = 0; k < c.length; ++k) a[k] = c[k];
return a;
}
var images, img, altText;
images = toArray(document.images);
for (var i = 0; i < images.length; ++i) {
img = images[i];
altText = document.createTextNode(img.alt);
img.parentNode.replaceChild(altText, img);
}
What it does
Okay, what this does is two things. First, there's a helper function that takes one object of items and puts it into a new array. This is used on the images node list.
Second, the array of images is iterated over and the image is replaced with its alt text which has been made into text content with no enclosing html element.
This works really well as a blunt-force tool that strips the images out of the document and allows the alt text to become link text if the image is a link, or to sit there as description otherwise.
It does not respond to dynamically changing pages and it will conflict with other scripts that swap images for whatever reason, because the image is actually removed from the document, there is no image there anymore to swap.
My analysis
The first thing I wondered about was the last thing I figured out. Why is that helper function even there? It just moves the nodelist (which is an object, not an array) into an array.
The reason is because nodelists are dynamic. As the removal loop goes through, it is taking things out of the nodelist, but not the array copy. You can't just run the for loop on the nodelist, because the nodelist length keeps getting shorter and the image you just removed is lopped off the front, so the image you really want to work on is always at index 0. Oh, hey!
Revised code with no helper function
var img, altText;
var images = document.images;
var imagesLength = images.length;
for (var i = 0; i < imagesLength; i += 1) {
img = images[0];
altText = document.createTextNode(img.alt);
img.parentNode.replaceChild(altText, img);
}
What it does
Now we just take the nodelist, note its original length, iterate over the list by that length and always work on the image at index 0.
This works fine, is shorter and (not detectable by a human) less of a performance drag. It could be made even more succinct by leaving out those temporary variables:
var images = document.images;
var imagesLength = images.length;
for (var i = 0; i < imagesLength; i+=1){
images[0].parentNode.replaceChild(document.createTextNode(images[0].alt), images[0]);
}
does exactly the same thing, but is a little harder to read. There's a lot less to fuss around with taking out space and linebreaks though, and is likely a "better" coding style, since it doesn't bother with variables that aren't really used for anything. This is something you don't need to care about, I don't think, when you're making a bookmarklet. It is something you need to know to program at a bigger scale (where you can document what that complex expression does).
Zap the image a little more lightly
So, the source of the DW user's problem with the above code was that the image is gone and can't be swapped for the new one when you click on a cuttag expander. The simplest answer to this problem is to not remove the img tag itself, but to make it not display the image. When you do that the browser will just display the alt text for you making the whole process simpler.
var images = document.images;
for (var i=0; i < images.length; i+=1)
images.item(i).removeAttribute("src");
What it does
We just load up the nodelist and take the src attribute off each one. Easy. Peasy, even. On DW, when you click a cuttag expander, the new arrow loads as an image, and you can carry on reading. You can click the bookmarklet again to get rid of any new images that appear in the new page content.
My analysis (or why this fails)
I travelled down this path for quite a while. I added refinements to have it insert some default text when there was no alt text, resize the images so they weren't taking up huge amounts of space anymore, and I looked at ways of automatically detecting new content on the page and running the script again. Most of this works well, although the auto-remove of new content is a bit hacky, but it only works in Firefox (and likely the current Opera).
Bloody Chrome.
Okay, and to be fair to bloody Chrome, my code does violate web standards which say an img tag should have a valid src. The problem is that Chrome (and most other webkit browsers) and IE, reload the image if you strip its src attribute. Also if you set it to an empty string. In addition webkit browsers do a weird thing with images that don't load by displaying alt text only some of the time.
For more information of this if you're curious here's some links I collected:
Down the remove the src attribute path
For posterity, here's my works only in Firefox, souped up bookmarklet:
/*Make a .forEach that works on objects*/
var forEach = Array.prototype.forEach;
/*Get nodelists of these elements*/
var images = document.getElementsByTagName("img");
var links = document.getElementsByTagName("a");
var buttons = document.getElementsByTagName("button");
/*This function removes the src, width and height of each image and inserts default alt text when an image has none and is inside a link tag.*/
function removeImages() {
forEach.call(images, removal);
function removal(img){
img.removeAttribute("src");
img.removeAttribute("width");
img.removeAttribute("height");
var imageAlt = img.getAttribute("alt");
if ((imageAlt === "" || imageAlt === " ")&& img.parentNode.nodeName.toLowerCase() === 'a') {
img.setAttribute("alt", "Image");
}
}
}
/*This runs the image removal function after a delay of 5 seconds*/
function delayedRemoval () {
window.setTimeout(removeImages, 5000);
}
/*This calls the image removal when you click on the bookmarklet*/
removeImages();
/*This puts an event listener on each button or link and then calls the delayed image removal function if any are clicked on*/
forEach.call(links, function(link){
link.addEventListener("click", delayedRemoval);
});
forEach.call(buttons, function(button){
button.addEventListener("click", delayedRemoval);
});
What it does
Okay, this is a big change from the short and pithy start of this concept, but it's not much more complex.
First, there's a new feature in here, the use of the
.forEach
method. Which is an array method that removes the need for all those for loops that look identical and repeating them violates DRY (don't repeat yourself). I already turfed the code where the nodelist gets made into an array, though and I didn't really want it back.
MDN to the rescue. That page contains instructions for ways of using
.forEach
on objects. I picked the one where you
.call
the function defined in the very first line of this code.
Next up, we get nodelists of all the images, links and buttons.
The basic task of changing the attributes on each image is inside a function now. It also has a conditional statement that adds the text, "Image" if there is no alt text present and the image is inside a link. This gives you something to actually click on to use the link. (Very handy on Wikis where the use of alt text is very spotty and weirdly implemented.)
There is another function that calls the removal function with a time delay. This gets invoked when a button or link on the page is clicked. It's a (somewhat) hacky way of running the removal automatically whenever new content gets added to the page. This is usually done by a show/hide link or button. If a page designer uses that horrible technique of making a span act like a button, then this won't work, but it fails silently, and you can always just click the bookmarklet again.
My analysis
Gotta say, I like this code, not least for the way I got that for each thing to work first go, and I'll likely put it in use for myself, but the only on Firefox thing makes it of limited value.
Some improvements could also be made to the resizing of images, to allow for pages that size the image via the CSS or via a parent div. The goal there is to get the alt text to flow better and to not have text cut off. The abomination that is absolutely positioned icons is a problem however.
Also, I think it is a real hole in the Document Object Model that there's no way to trigger a script when the page dynamically changes by having hidden content un-hidden by a script (MutationObserver does not do this) since every other page has this effect on it. Obviously if I was writing a script for a specific website, I could build in the correct event listeners with class names, but a general purpose method would be nice.
So what then?
Someone else on the DW Suggestions post has an idea to use the bookmarklet to toggle images on and off. It doesn't quite work--has a couple of minor bugs, one of which I'm still trying to figure out--but I don't think it's the direction I want to go. If you click on a cuttag, you still don't see the arrow for collaps and the alt text still says expand and you have to click the bookmarklet twice to fix that.
So what do I want? Blue sky the idea first and then see if it's at all possible to do.
I want...
- Images off, but not removed from the DOM so that dynamically added images will show up on the page at least as an image.
- Limiting the text flow issues and overlap you get where the alt text is too long for the image container.
- Works in at least FF, Webkit (Blink!) and current Opera.
- Allows some way to respond to unhidden new content. Either automatically or a quick re-run of the removal script.
- Making sure an image link with no alt text is still clickable.
And I might like...
- Some custom style for image link alt text to avoid it not looking like a link when the native styling removes the regular link styling for the page
- Some customs style for all the alt text to make it clear it is not regular content text.
- Some visual designation that there was an image there--to help with no alt text images and image links.
So back to the drawing board to see if I can get the image gone enough to be invisible, but still there enough to be replaced. Part II comes when I've done that.