HST Example of Context Aware Lightbox for all inline images
For the website you are looking at (https://xmdocumentation.bloomreach.com/) we want on detail pages to
-
Always by default show a small threecolumn variant, regardless which variant was selected in the CMS
-
Have the threecolumn variant clickable to get a larger variant
-
Show a ninecolumn variant when there is no leftmenu (contextual part)
-
Show a sixteencolumn variant when there is a leftmenu (contextual part)
Note that the contextual part could also be whether a visitor is using a mobile device, has low bandwidth, etc etc.
Set the context whether the ninecolumn or sixteencolumn should be shown when threecolumn is toggled
Assume your LeftMenu component looks like below. There, we store on the HstRequestContext whether there will be shown a left menu on the page or not. Since all #doBeforeRender(..) calls of every HstComponent is done before any renderer (jsp/freemarker) is invoked, this " wideview" attribute is available in every renderer.
public void doBeforeRender(final HstRequest request, final HstResponse response) { HstSiteMenu menu = request.getRequestContext().getHstSiteMenus().getSiteMenu(menuName); // only if there is a sitemenu item expanded we show the menu : // if there is no expanded item at all, we want to show the // 'wide document' view, and no menu at all. Hence check here whether to // set the sitemenu at all on the request if(menu.getDeepestExpandedItem() != null ) { request.setAttribute("menu", menu); } else { // this is a bit dirty but we need to 'tell' the main component // during the rendering of its jsp it should be a wide listing // style. We do this through an attribute on the requestContext // which is not very clean request.getRequestContext().setAttribute("wideview", true); } }
Use in JSP an hst:imagevariant to switch between nine or sixteen column image when image is toggled
Since we want to get the value for wideview from the HstRequestContext, we first need to add in the template:
JSP
<hst:defineObjects/>
Freemarker
<@hst.defineObjects/>
Then we can do the following:
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>
Also note, that we already set contentRewriter="${lightboxContentRewriter}"
Make sure the 'lightboxContentRewriter' content rewriter is available on every HstComponent
We assume all your HstComponent do a call to MyBaseComponent#doBeforeRender(...). Then, if your base component contains the code below, there will be a lightboxContentRewriter available for every renderer:
MyBaseComponent:
public abstract class MyBaseComponent extends BaseHstComponent { public static final LightboxContentRewriter lightboxContentRewriter = new LightboxContentRewriter(); @Override public void doBeforeRender(HstRequest request, HstResponse response) { // always have the custom content rewriter available request.setAttribute("lightboxContentRewriter", lightboxContentRewriter); }
Implement your custom LightboxContentRewriter
We need to change the html from the rich text to inject behavior for images (not external ones) when clicked. Assume we have the following requirements
-
By default we always show the threecolumn variant
-
When clicked, the imagevariant from <hst:imagevariant> should be shown (thus mygallery:sixteencolumn or mygallery:ninecolumn in our case)
Again, we could only change the behavior for image tags, leave the link ( a) tags untouched, and at the end of #rewrite(...) call SimpleContentRewriter#rewrite(..). However, we'll also handle link tags in our example below as well. We will change the html as follows:
-
For every image, set the src to the threecolumn variant
-
Add the img alt attribute that contains the toggle variant (nine/sixteen column). In all fairness, using alt attribute is a bit of a trick
-
Add a wrapper div around the image that will be the container of the image.
LightboxContentRewriter:
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; } }
Last step : Add some css, javascript and magnifier images
Since all images now are rendered by default as the threecolumn variant, there is a wrapper, there are id attributes, and there is a wrapper div, all that is needed is some css, javascript and two images.
CSS:
CSS for the lightbox toggling:
/** 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 **/
JS:
JS code for the lightbox toggling:
// 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); });
And last thing, make sure you add the images images/magnifier-plus.png and images/magnifier-min.png