HST Example: Context-Aware Lightbox for Inline Images
This example demonstrates how to implement a context-aware lightbox for all inline images on detail pages at https://xmdocumentation.bloomreach.com/. The requirements are:
- Always display the
threecolumnimage variant by default, regardless of the variant selected in the CMS. - Make the
threecolumnvariant clickable to display a larger variant. - Display the
ninecolumnvariant when there is no left menu present. - Display the
sixteencolumnvariant when a left menu is present.
The context can also be extended to other conditions, such as device type or network bandwidth.
Setting Context for Image Variant Selection
To determine whether to show the ninecolumn or sixteencolumn variant when the threecolumn image is toggled, store the context in the HstRequestContext. The following example shows how to set a wideview attribute based on the presence of a left menu in your LeftMenu component.
public void doBeforeRender(final HstRequest request, final HstResponse response) { HstSiteMenu menu = request.getRequestContext().getHstSiteMenus().getSiteMenu(menuName); // Show the menu only if a site menu item is expanded. if(menu.getDeepestExpandedItem() != null ) { request.setAttribute("menu", menu); } else { // Indicate that a wide view should be used by setting an attribute on the request context. request.getRequestContext().setAttribute("wideview", true); } }
All #doBeforeRender(..) calls for every HstComponent execute before any renderer (JSP/Freemarker) is invoked. This ensures that the wideview attribute is available in every renderer.
Using hst:imagevariant to Switch Image Variants in JSP or Freemarker
To access the wideview attribute in your template, first define the HST objects.
JSP:
<hst:defineObjects/>
Freemarker:
<@hst.defineObjects/>
Then, implement conditional rendering for the image variant:
JSP:
<hst:defineObjects/> <c:choose> <c:when test= "${requestScope.hstRequest.requestContext.attributes['wideview'] == true}"> <hst:html hippohtml="${requestScope.document.html}" contentRewriter="${lightboxContentRewriter}"> <hst:imagevariant name="mygallery:sixteencolumn"/> </hst:html> </c:when> <c:otherwise> <hst:html hippohtml="${requestScope.document.html}" contentRewriter="${lightboxContentRewriter}"> <hst:imagevariant name="mygallery:ninecolumn"/> </hst:html> </c:otherwise> </c:choose>
Freemarker:
<@hst.defineObjects/> <#if hstRequest.requestContext.attributes['wideview'] == true> <@hst.html hippohtml=document.html contentRewriter="${lightboxContentRewriter}"> <@hst.imagevariant name="mygallery:sixteencolumn"/> </@hst.html> <#else> <@hst.html hippohtml=document.html contentRewriter="${lightboxContentRewriter}"> <@hst.imagevariant name="mygallery:ninecolumn"/> </@hst.html> </#if>
The template uses contentRewriter="${lightboxContentRewriter}" to process the rich text content.
Providing the lightboxContentRewriter to All HstComponents
To ensure the lightboxContentRewriter is available in every renderer, add it as a request attribute in your base component's doBeforeRender method. If all your components extend MyBaseComponent, use the following implementation:
public abstract class MyBaseComponent extends BaseHstComponent { public static final LightboxContentRewriter lightboxContentRewriter = new LightboxContentRewriter(); @Override public void doBeforeRender(HstRequest request, HstResponse response) { // Make the custom content rewriter available to all renderers request.setAttribute("lightboxContentRewriter", lightboxContentRewriter); }
Implementing a Custom LightboxContentRewriter
To inject lightbox behavior for images in rich text fields, implement a custom LightboxContentRewriter. The requirements are:
- Always render the
threecolumnimage variant by default. - When the image is clicked, display the variant specified by
<hst:imagevariant>(eithermygallery:sixteencolumnormygallery:ninecolumn). - Only modify image tags; leave external images and existing links unchanged.
- At the end of
#rewrite(...), callSimpleContentRewriter#rewrite(..).
The following implementation rewrites the HTML as follows:
- Sets the
srcattribute of every image to thethreecolumnvariant. - Stores the toggle variant (nine/sixteen column) in the
altattribute (note: usingaltfor this purpose is a workaround). - Wraps each image in a
divcontainer.
public class LightboxContentRewriter extends SimpleContentRewriter { private final static Logger log = LoggerFactory.getLogger(SimpleContentRewriter.class); private static boolean htmlCleanerInitialized; private static HtmlCleaner cleaner; private static synchronized void initCleaner() { if (!htmlCleanerInitialized) { cleaner = new HtmlCleaner(); CleanerProperties props = cleaner.getProperties(); props.setOmitComments(true); props.setOmitXmlDeclaration(true); htmlCleanerInitialized = true; } } protected static HtmlCleaner getHtmlCleaner() { if (!htmlCleanerInitialized) { initCleaner(); } return cleaner; } @Override public String rewrite(final String html, final Node node, final HstRequestContext requestContext, final Mount targetMount) { if (html == null) { return null; } try { TagNode rootNode = getHtmlCleaner().clean(html); TagNode [] links = rootNode.getElementsByName("a", true); for (TagNode link : links) { String documentPath = link.getAttributeByName("href"); if (StringUtils.isBlank(documentPath)) { continue; } if(isExternal(documentPath)) { continue; } else { String queryString = StringUtils.substringAfter( documentPath, "?"); boolean hasQueryString = !StringUtils.isEmpty( queryString); if (hasQueryString) { documentPath = StringUtils.substringBefore( documentPath, "?"); } String rewritterHref; HstLink href = getDocumentLink(documentPath,node, requestContext, targetMount); if (href != null && href.getPath() != null) { rewritterHref = href.toUrlForm(requestContext, isFullyQualifiedLinks()); if (hasQueryString) { rewritterHref += "?"+ queryString; } // override the href attr setAttribute(link, "href", rewritterHref); } else { log.warn("Skip href because url is null"); } } } TagNode [] images = rootNode.getElementsByName("img", true); ImageVariant threeColVar = new DefaultImageVariant( "connectgallery:threecolumn", null, true); int i = 0; for (TagNode image : images) { i++; String srcPath = image.getAttributeByName("src"); if (StringUtils.isBlank(srcPath)) { continue; } if(isExternal(srcPath)) { continue; } else { HstLink binaryLink = getBinaryLink(srcPath, node, requestContext, targetMount); ImageVariant originalVariant = getImageVariant(); // get the 3column variant as well : this is by // default shown setImageVariant(threeColVar); HstLink threeColVarBinaryLink = getBinaryLink(srcPath, node, requestContext, targetMount); // set the imageVariant backl to original one again setImageVariant(originalVariant); if (binaryLink != null && binaryLink.getPath() != null) { String rewrittenSrc = binaryLink.toUrlForm(requestContext, true); String rewrittenSrcThreeCol = threeColVarBinaryLink.toUrlForm(requestContext, isFullyQualifiedLinks()); // remove original one and add new image image.getParent().removeChild(image); // override the src attr TagNode imageCopy = makeCopy(image); setAttribute(imageCopy, "src", rewrittenSrcThreeCol); imageCopy.addAttribute("id", "img-id" + i); final String alternativeText = imageCopy.getAttributeByName("alt"); if (alternativeText != null) { imageCopy.addAttribute("title", alternativeText); } setAttribute(imageCopy, "alt", rewrittenSrc); imageCopy.addAttribute("class", imageCopy.getAttributeByName("class")+ " lightBox"); image.getParent().addChild(imageCopy); } else { log.warn("Skip href because url is null"); } } } // everything is rewritten. Now write the "body" element as // result TagNode [] targetNodes = rootNode.getElementsByName("body", true); if (targetNodes.length > 0 ) { TagNode bodyNode = targetNodes[0]; return getHtmlCleaner().getInnerHtml(bodyNode); } else { log.warn("Cannot rewrite content for '{}' because there is no 'body' element" + node.getPath()); } } catch (Exception e) { throw new RuntimeException(e); } return null; } private void setAttribute(TagNode tagNode, String attrName, String attrValue) { if (tagNode.hasAttribute(attrName)) { tagNode.removeAttribute(attrName); } tagNode.addAttribute(attrName, attrValue); } private TagNode makeCopy(TagNode toCopy) { TagNode copy = new TagNode(toCopy.getName()); for (Map.Entry<String,String> attr : toCopy.getAttributes().entrySet()) { copy.addAttribute(attr.getKey(), attr.getValue()); } return copy; } }
Adding CSS, JavaScript, and Magnifier Images
After implementing the content rewriter, add the required CSS, JavaScript, and magnifier images to enable the lightbox toggle functionality.
CSS for Lightbox:
/** begin lightbox **/ img.lightBox { cursor: pointer; } .img-magnifier { position:absolute; width: 60px; height: 60px; cursor: pointer; background-image: url("../images/magnifier-plus.png"); } .img-magnifier.minimize { background-image: url("../images/magnifier-min.png"); } /** end lightbox **/
JavaScript for Lightbox Toggle:
// lightbox $('.lightBox').each(function() { var img = $(this); img.removeAttr('width'); img.removeAttr('height'); img.parent().append('<span class="img-magnifier" />'); var magnifier = $('.img-magnifier', img.parent()); var alignMagnifier = function() { var imgPosition = img.position(); if (magnifier.hasClass('minimize')) { magnifier.css('left', imgPosition.left + img.width() - magnifier.width()); magnifier.css('top', imgPosition.top + img.height() - magnifier.height()); } else { magnifier.css('left', imgPosition.left + img.width() / 2 - magnifier.width() / 2); magnifier.css('top', imgPosition.top + img.height() / 2 - magnifier.height() / 2); } }; alignMagnifier(); img.load(alignMagnifier); var magnify = function() { console.log('magnify'); var previousSrc = img.attr('src'); img.attr('src', img.attr('alt')); img.attr('alt', previousSrc); magnifier.toggleClass('minimize'); }; img.click(magnify); magnifier.click(magnify); });
Images:
Place the following images in your images directory:
images/magnifier-plus.pngimages/magnifier-min.png
These assets support the visual toggle for the lightbox functionality.
Summary
This implementation enables context-aware lightbox functionality for inline images in Bloomreach Content. The approach uses a custom content rewriter, context attributes, and supporting frontend code to control image variants and lightbox behavior based on page context.