Fallback handling for $image widget

@EricShulman , like your refinements.

One query, your ‘let’ statements wherein you include an ‘else’ alternative, is it possible to do something similar to an $image statement, trying to load an image from, say, local file store and if Not there, load from an http url?

Eg, <$image source="Media/a.jpg" else <baseURL>"Media/a.jpg" />

Bobj

If would be nice if the $image widget had support for “fallback” content when an image fails to load, similar to the way the $transclude widget permits fallback content if the specified tiddler doesn’t exist. For example, something like this:

<$image source="Media/a.jpg">image unavailable</$image>

If such a fallback mechanism existed, it would also be possible show an alternative remote image if the local image doesn’t exist, by writing something like this:

<$image source="Media/a.jpg">
   <$image source={{{ [<baseURL>addsuffix[/Media/a.jpg] }}}/>
</$image>

Unfortunately, the current implementation of the $image widget does NOT support the use of fallback content. The documentation for the $image widget (see https://tiddlywiki.com/#ImageWidget) specifically notes that “Any content of the <$image> widget is ignored.

However, the $image widget DOES currently have the ability to detect when an image loads (or fails to load) and in response, adds either a tc-image-loaded or tc-image-error CSS class to the img DOM element. So, in theory, it should be possible to extend the underlying TWCore javascript code to add handling for fallback content and then only display that content if the tc-image-error class is added to the img DOM element.

I will try to dig into the code a bit and see what can be achieved. If I can make it work, I’ll open a ticket to propose adding it to the TWCore.

-e

4 Likes

Thanks @EricShulman

Bobj

I think it would be useful to have a TW event that can be used to determine the status of of any Load from an external source.

Bobj

OK! I’ve got it working… and it was only 4 additional lines of code in $:/core/modules/widgets/image.js:

in the definition of ImageWidget.prototype.render, find these lines (near the end of the function):

	domNode.addEventListener("error",function() {
		$tw.utils.removeClass(domNode,"tc-image-loading");
		$tw.utils.addClass(domNode,"tc-image-error");
	},false);
	// Insert element
	parent.insertBefore(domNode,nextSibling);
	this.domNodes.push(domNode);

and change to these (adding the lines marked with /* ELS */ comments):

	domNode.addEventListener("error",function() {
		$tw.utils.removeClass(domNode,"tc-image-loading");
		$tw.utils.addClass(domNode,"tc-image-error");
		domNode.self.makeChildWidgets(); /* ELS */
		domNode.self.renderChildren(domNode.parentNode,domNode.nextSibling); /* ELS */
		if(domNode.self.children.length!=0) domNode.style.display="none"; /* ELS */
	},false);
	// Insert element
	parent.insertBefore(domNode,nextSibling);
	this.domNodes.push(domNode);
	domNode.self=this; /* ELS */

Notes:

  • in the event listener “error” function, there are three new lines at the end:
    • domNode.self.makeChildWidgets(); builds the “widget tree” for the fallback content
    • domNode.self.renderChildren(...); renders the child widgets as DOM elements (HTML)
    • If there are rendered child DOM elements, then hide the missing “broken image” display
  • at the very end of the render(), save the image widget object (this) so it can be referenced in the event listener as domNode.self

Results:

  • If the outer $image widget is loaded successfully, then the error handler is never triggered, the image renders normally, and no fallback content is processed.
  • If the outer $image widget fails to load, the error handler is triggered:
    • any enclosed fallback content is rendered.
    • If there is rendered fallback content, the browser’s usual “broken image” is hidden, otherwise the “broken image” remains visible.

Here’s a JSON containing the modified $image widget code, as well as a test image (“jeremy.jpg”) and some wikitext to test the $image widget handling (“Test Image Fallback”).

ImageFallback.json (48.5 KB)

I’ve opened a Github [Idea] “issue” ticket to propose adding this change to the TWCore:

enjoy,
-e

@pmario: How do I split this thread to move this post (and the related “fallback image” discussion posts) to a new thread?

1 Like

See: How to Split a Discourse Thread

@EricShulman , what can I say that I’ve not said before?? Oh I know, its the King’s Birthday here so a proclamation might be in order or maybe even an Copanion Order of Australia for services to Tiddlywiki :slight_smile:

bobj

@EricShulman works but for one funny.

My code


\procedure displayImageCaptionCopyright(ICCTitle,ICCWidth:"100%")

<$transclude $variable="displayimageonly" imageTitle=<<ICCTitle>> imageWidth=<<ICCWidth>> />
<br>
<$transclude $tiddler=<<ICCTitle>> $field="imageCaption" />
<br>
<$transclude $tiddler=<<ICCTitle>> $field="imageCopyright" />

\end

\procedure displayimageonly(imageTitle,imageWidth:"100%")

<$let 
     baseURL={{{ [{$:/TLS/onlineImages!!onlineimageURL}] }}}
     onlineURL={{{ [<imageTitle>get[imageURL]] }}}  
>
<$image source={{{ [<imageTitle>get[imageURL]] }}} width=<<imageWidth>> loading="lazy" >
<$image source={{{ [<baseURL>addsuffix<onlineURL>] }}} width=<<imageWidth>> loading="lazy" />
</$let>
\end

The display is

As you can see, there is a </$let> statement after the image. If I remove that statement from the code, the statement also disappears from the rendered tiddler.

Any idea why it is there?

bobj

You are missing </$image> to close the “outer” $image widget.

<$let 
     baseURL={{{ [{$:/TLS/onlineImages!!onlineimageURL}] }}}
     onlineURL={{{ [<imageTitle>get[imageURL]] }}}  
>
<$image source={{{ [<imageTitle>get[imageURL]] }}} width=<<imageWidth>> loading="lazy" >
   <$image source={{{ [<baseURL>addsuffix<onlineURL>] }}} width=<<imageWidth>> loading="lazy" />
</$image>
</$let>

-e

Bugger…

Maybe, though, it’s a hint to slightly change things a bit. The problem is the widget within a widget and the internal one having the expected /> tag which confused me.

Might it not be more explicit to have a fallback parameter in the first widget tag? That way, only a single close tag.

I’m not being picky but I’m sure others will make the same mistake.

Apart from that it works very well though.

Bobj

Using a “fallback parameter” would only permit specifying an alternative image source (local file or URL). In contrast, by using the $image widget’s inner content, you can render ANY kind of wikitext as the fallback output (e.g. a formatted text message, a table, a form to select a different image, etc.).

In addition, while it might be a little bit confusing at first, the “widget within a widget” code pattern (<$outerwidget><$innerwidget/></$outerwidget>) is already well-established in TiddlyWiki for use with other kinds of widgets.

For example, rendering a tiddler caption with fallback to the tiddler title, like this:

<$view field="caption">
   <$view field="title"/>
</$view>

or, providing a remote URL link as a fallback when transcluding a missing tiddler title, like this:

<$transclude tiddler="TiddlyTools/SaveChanges/Info" mode=block>
   For documentation, see https://TiddlyTools.com/#TiddlyTools/SaveChanges/Info
</$transclude>

(I’ve used the above technique to directly show the …/Info tiddler contents if present, or a fallback message and link to a remote URL if the …/Info tiddler is missing).

-e

Ah, I understand and agree, keep the current format. I had not come across this before.

Thank you for your understanding of my ignorance.

Bobj

currently I use the object tag for image fallback (only works with external images):

			<object data=<<card!!image>> >
				<object data=<<card!!altfavicon>> >
					{{$:/core/images/globe}}
				</object>
			</object>

<<card!!image>> is a url.

Interesting, must look this widget up.

This has opened up so many areas of Tiddlywiki I have not come across before.

Bobj