/**
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.livingstories.client.lsp;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.livingstories.client.ui.Link;
import com.google.livingstories.client.ui.ReadMoreLink;
import java.util.ArrayList;
import java.util.List;
/**
* A class to render HTML content from the summary or within content items. It does 2 things
* currently:
* 1. Breaks up the content into chunks separate by 'Read more' links if the text contains
* break tags.
* 2. If the text contains tags for other content items, converts them to links with appropriate
* behavior.
* If the tag contains the id of only 1 item, its content is displayed in a popup panel. If the tag
* has multiple ids, the area below the summary is repopulated with the linked content items.
*/
public class ContentRenderer extends Composite {
private static final String BREAK_TAG = "<!--lsp:break-->";
private static final String PARAGRAPH_END_TAG = "</p>";
private static final String HIGHLIGHT_CLASS = "summaryHighlights";
// Use a code tag for this since it's actually a valid html tag, recognized by JTidy.
private static final String TIMELINE_TAG_NAME = "code";
private static final String TIMELINE_IDENTIFIER = "lsp:timeline";
private FlowPanel container;
private String content;
private int initiallyVisibleChunkCount;
private boolean openHighlightedChunks;
private boolean trackReadMoreWithAnalytics = false;
private static String SHOW_LESS = LspMessageHolder.consts.showLess();
public ContentRenderer(String content, boolean openHighlightedChunks) {
this(content, openHighlightedChunks, false);
}
public ContentRenderer(String content, boolean openHighlightedChunks,
boolean trackReadMoreWithAnalytics) {
super();
this.container = new FlowPanel();
this.content = content;
this.openHighlightedChunks = openHighlightedChunks;
this.trackReadMoreWithAnalytics = trackReadMoreWithAnalytics;
populate();
initWidget(container);
}
/**
* Populates the flow panel with the content after inserting 'Read more' links where
* <break> tags are present. Clicking on the link reveals the next section of the content.
*/
private void populate() {
String[] chunks = content.split(BREAK_TAG);
if (chunks.length == 1) {
container.add(processTagsInChunk(chunks[0]));
return;
}
ReadMoreLink readMoreLink = null;
boolean shouldExpand = false;
boolean reachedExpandedChunk = false;
// Create a link that causes everything to be hidden
Link readLessLink = new Link(SHOW_LESS) {
@Override
protected void onClick(ClickEvent e) {
// Hide all the widgets except the first few, as specified by the
// initiallyVisibleChunkCount variable.
for (int i = 1; i < container.getWidgetCount(); i++) {
container.getWidget(i).setVisible(i < initiallyVisibleChunkCount);
}
}
};
readLessLink.setVisible(false);
container.add(readLessLink);
for (int i = chunks.length - 1; i >= 0; i--) {
String chunk = chunks[i].trim();
if (readMoreLink != null) {
// Insert the previously created "read more" link at the beginning.
container.insert(readMoreLink, 0);
}
// If this is the first chunk or if this chunk contains a highlight for new content,
// expand it.
if (i == 0 || (openHighlightedChunks && chunk.contains(HIGHLIGHT_CLASS))) {
shouldExpand = true;
if (!reachedExpandedChunk && readMoreLink != null) {
// If all the previous chunks were hidden, then we should show the
// read more link after this expanded chunk, if there is one.
readMoreLink.setVisible(true);
}
if (i > 0) {
// If this expanded chunk isn't the first chunk, then we should show
// the read less link.
readLessLink.setVisible(true);
}
reachedExpandedChunk = true;
}
// Process this chunk. First, check if this chunk ends with text in paragraph tags
// followed by non-text content.
int position = 0;
int finalParagraphIndex = chunk.toLowerCase().lastIndexOf(PARAGRAPH_END_TAG);
int finalParagraphEndIndex = finalParagraphIndex + PARAGRAPH_END_TAG.length();
if (finalParagraphIndex > 0 && finalParagraphEndIndex < chunk.length()) {
// There's non-text stuff after the last paragraph tag.
// Split this into two chunks for processing,
// and put the 'read more' link before the non-text content.
HTMLPanel html1 = processTagsInChunk(chunk.substring(0, finalParagraphEndIndex + 1));
html1.setVisible(shouldExpand);
container.insert(html1, position++);
if (readMoreLink != null) {
container.insert(readMoreLink, position++);
}
HTMLPanel html2 = processTagsInChunk(chunk.substring(finalParagraphEndIndex));
html2.setVisible(shouldExpand);
container.insert(html2, position++);
// Create a new readMoreLink that will show the text chunk,
// the next read more link, the non-text chunk, and the read less link.
readMoreLink = new ReadMoreLink(trackReadMoreWithAnalytics,
html1, readMoreLink, html2, readLessLink);
initiallyVisibleChunkCount = 3;
} else {
// Otherwise, process this as a single chunk.
HTMLPanel html = processTagsInChunk(chunk);
html.setVisible(shouldExpand);
container.insert(html, position++);
if (readMoreLink != null) {
container.insert(readMoreLink, position++);
}
// Create a new readMoreLink that will show the chunk,
// the next read more link, and the read less link.
readMoreLink = new ReadMoreLink(trackReadMoreWithAnalytics,
html, readMoreLink, readLessLink);
initiallyVisibleChunkCount = 2;
}
}
}
/**
* Find the custom tags in the HTML and process them.
*/
private HTMLPanel processTagsInChunk(String chunk) {
HTMLPanel contentPanel = new HTMLPanel(chunk);
try {
// Process each type of tag
for (ContentTag tag : contentTags) {
NodeList<Element> tagNodeList =
contentPanel.getElement().getElementsByTagName(tag.getTagName());
List<Element> tagElements = new ArrayList<Element>();
for (int i = 0; i < tagNodeList.getLength(); i++) {
// First iterate over the node list and copy all the elements into a new list. Can't
// iterate and modify them at the same time because the list changes dynamically.
tagElements.add(tagNodeList.getItem(i));
}
for (Element tagElement : tagElements) {
Widget widget = tag.createWidgetToReplaceTag(tagElement);
if (widget != null) {
// To replace the existing tag with the widget created above, the HTMLPanel needs
// to have the id of the element being replaced. Since we can't expect users to assign
// unique ids in every tag, we do this here automatically.
String uniqueId = HTMLPanel.createUniqueId();
tagElement.setId(uniqueId);
contentPanel.addAndReplaceElement(widget, uniqueId);
}
}
}
} catch (Exception e) {
// Just return the panel with the original content
}
return contentPanel;
}
/**
* Interface for the tags within the content that will be processed.
* To add a new type of tag, create an implementation of this interface and also add the tag
* to the contentTags array
*/
private interface ContentTag {
String getTagName();
Widget createWidgetToReplaceTag(Element tagElement);
}
/**
* Processes timeline tags in the content, replacing them with timeline widgets.
*
* To insert a timeline, use the following syntax:
* <code>lsp:timeline</code>
*
* To set the width and height, just set the 'width' and 'height' attributes on the code tag:
* <code width="700" height="125">lsp:timeline</code>
*/
private static class TimelineTag implements ContentTag {
@Override
public String getTagName() {
return TIMELINE_TAG_NAME;
}
@Override
public Widget createWidgetToReplaceTag(final Element tag) {
if (!tag.getInnerHTML().equals(TIMELINE_IDENTIFIER)) {
return null;
}
Integer width = null;
Integer height = null;
try {
width = Integer.valueOf(tag.getAttribute("width"));
height = Integer.valueOf(tag.getAttribute("height"));
} catch (NumberFormatException ex) {
// No size or invalid size specified. Use defaults.
}
return EventTimelineCreator.createTimeline(width, height);
}
}
private static final ContentTag[] contentTags = new ContentTag[] {
new TimelineTag()
};
}