From 26832b6947266ce5cffd92558c23e6556278038d Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 19 Dec 2014 01:50:32 +0200 Subject: [PATCH] Option for rendering Calendar event captions as HTML (#9030) Change-Id: Ib7f6e67c242449e58a10359c596489fea2f679f6 --- WebContent/release-notes.html | 3 +- .../src/com/vaadin/client/ui/VCalendar.java | 30 +++++ .../client/ui/calendar/CalendarConnector.java | 2 + .../calendar/schedule/DateCellDayEvent.java | 14 ++- .../ui/calendar/schedule/MonthEventLabel.java | 15 ++- .../calendar/schedule/WeeklyLongEvents.java | 6 +- server/src/com/vaadin/ui/Calendar.java | 37 ++++++- .../shared/ui/calendar/CalendarState.java | 1 + .../calendar/CalendarHtmlInEvents.java | 101 +++++++++++++++++ .../calendar/CalendarHtmlInEventsTest.java | 103 ++++++++++++++++++ 10 files changed, 302 insertions(+), 10 deletions(-) create mode 100644 uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEvents.java create mode 100644 uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEventsTest.java diff --git a/WebContent/release-notes.html b/WebContent/release-notes.html index a060372580..7be74ee1ed 100644 --- a/WebContent/release-notes.html +++ b/WebContent/release-notes.html @@ -112,7 +112,7 @@
  • Declarative layout support for initializing a component hierarchy from an HTML file.
  • Uses GWT 2.7 for improved compilation times when using Super Dev Mode.
  • @Viewport annotation for declaratively defining a mobile viewport definition for a UI.
  • -
  • Captions can be configured to be displayed as HTML.
  • +
  • Component captions, TabSheet/Accordion tab captions and Calendar event captions can be configured to be displayed as HTML.
  • Selects use converters when presenting itemids.
  • Improved performance when server response contains no visual changing (e.g. empty polling responses).
  • Unified JSON library for using the same API in both server-side and client-side code.
  • @@ -134,6 +134,7 @@

    Raw JSON values passed to AbstractJavaScriptComponent.callFunction and AbstractJavaScriptExtension.callFunction should be changed to use elemental.json types.

  • The semantics of empty and required for Field classes has been made more consistent. This mainly affects Checkbox which is now considered to be empty when it is not checked.
  • +
  • The previously inconsistent behavior in HTML vs plain text rendering of Calendar event captions has been made consistent.
  • Support for Opera 12 has been dropped. Newer versions based on the Blink rendering engine are still supported.
  • Known issues

    diff --git a/client/src/com/vaadin/client/ui/VCalendar.java b/client/src/com/vaadin/client/ui/VCalendar.java index c59a78108c..08d4351931 100644 --- a/client/src/com/vaadin/client/ui/VCalendar.java +++ b/client/src/com/vaadin/client/ui/VCalendar.java @@ -1342,6 +1342,7 @@ public class VCalendar extends Composite implements VHasDropHandler { private MouseEventListener mouseEventListener; private boolean forwardNavigationEnabled = true; private boolean backwardNavigationEnabled = true; + private boolean eventCaptionAsHtml = false; /** * Get the listener that listen to mouse events @@ -1467,4 +1468,33 @@ public class VCalendar extends Composite implements VHasDropHandler { public void setDropHandler(CalendarDropHandler dropHandler) { this.dropHandler = dropHandler; } + + /** + * Sets whether the event captions are rendered as HTML. + *

    + * If set to true, the captions are rendered in the browser as HTML and the + * developer is responsible for ensuring no harmful HTML is used. If set to + * false, the caption is rendered in the browser as plain text. + *

    + * The default is false, i.e. to render that caption as plain text. + * + * @param captionAsHtml + * true if the captions are rendered as HTML, false if rendered + * as plain text + */ + public void setEventCaptionAsHtml(boolean eventCaptionAsHtml) { + this.eventCaptionAsHtml = eventCaptionAsHtml; + } + + /** + * Checks whether event captions are rendered as HTML + *

    + * The default is false, i.e. to render that caption as plain text. + * + * @return true if the captions are rendered as HTML, false if rendered as + * plain text + */ + public boolean isEventCaptionAsHtml() { + return eventCaptionAsHtml; + } } diff --git a/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java index 8f5e9d9a59..8c92ef1233 100644 --- a/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java +++ b/client/src/com/vaadin/client/ui/calendar/CalendarConnector.java @@ -345,6 +345,8 @@ public class CalendarConnector extends AbstractComponentConnector implements widget.setEventMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE)); widget.setEventResizeAllowed(hasEventListener(CalendarEventId.EVENTRESIZE)); + widget.setEventCaptionAsHtml(state.eventCaptionAsHtml); + List days = state.days; List events = state.events; diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java index 3b168b636c..8b08e9bc7a 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java @@ -184,14 +184,20 @@ public class DateCellDayEvent extends FocusableHTML implements */ private void updateCaptions(boolean bigMode) { String innerHtml; - String escapedCaption = Util.escapeHTML(calendarEvent.getCaption()); String timeAsText = calendarEvent.getTimeAsText(); + String htmlOrText; + + if (dateCell.weekgrid.getCalendar().isEventCaptionAsHtml()) { + htmlOrText = calendarEvent.getCaption(); + } else { + htmlOrText = Util.escapeHTML(calendarEvent.getCaption()); + } + if (bigMode) { - innerHtml = "" + timeAsText + "
    " - + escapedCaption; + innerHtml = "" + timeAsText + "
    " + htmlOrText; } else { innerHtml = "" + timeAsText + ": " - + escapedCaption; + + htmlOrText; } caption.setInnerHTML(innerHtml); eventContent.setInnerHTML(""); diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java b/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java index 6fc2e430cd..31e600c8f9 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java @@ -20,6 +20,7 @@ import java.util.Date; import com.google.gwt.event.dom.client.ContextMenuEvent; import com.google.gwt.event.dom.client.ContextMenuHandler; import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.Util; import com.vaadin.client.ui.VCalendar; /** @@ -75,7 +76,8 @@ public class MonthEventLabel extends HTML implements HasTooltipKey { * Set the caption of the event label * * @param caption - * The caption string, can be HTML + * The caption string, can be HTML if + * {@link VCalendar#isEventCaptionAsHtml()} is true */ public void setCaption(String caption) { this.caption = caption; @@ -87,13 +89,20 @@ public class MonthEventLabel extends HTML implements HasTooltipKey { */ private void renderCaption() { StringBuilder html = new StringBuilder(); + String textOrHtml; + if (calendar.isEventCaptionAsHtml()) { + textOrHtml = caption; + } else { + textOrHtml = Util.escapeHTML(caption); + } + if (caption != null && time != null) { html.append(""); html.append(calendar.getTimeFormat().format(time)); html.append(" "); - html.append(caption); + html.append(textOrHtml); } else if (caption != null) { - html.append(caption); + html.append(textOrHtml); } else if (time != null) { html.append(""); html.append(calendar.getTimeFormat().format(time)); diff --git a/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java index bd833e06a0..9488c8835a 100644 --- a/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java +++ b/client/src/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java @@ -102,7 +102,11 @@ public class WeeklyLongEvents extends HorizontalPanel implements HasTooltipKey { eventLabel.addStyleDependentName(extraStyle + "-all-day"); } if (!started) { - eventLabel.setText(calendarEvent.getCaption()); + if (calendar.isEventCaptionAsHtml()) { + eventLabel.setHTML(calendarEvent.getCaption()); + } else { + eventLabel.setText(calendarEvent.getCaption()); + } started = true; } } diff --git a/server/src/com/vaadin/ui/Calendar.java b/server/src/com/vaadin/ui/Calendar.java index 5b5c390fa1..206cc01d1a 100644 --- a/server/src/com/vaadin/ui/Calendar.java +++ b/server/src/com/vaadin/ui/Calendar.java @@ -296,6 +296,11 @@ public class Calendar extends AbstractComponent implements return (CalendarState) super.getState(); } + @Override + protected CalendarState getState(boolean markAsDirty) { + return (CalendarState) super.getState(markAsDirty); + } + @Override public void beforeClientResponse(boolean initial) { super.beforeClientResponse(initial); @@ -1667,7 +1672,7 @@ public class Calendar extends AbstractComponent implements * weekly mode */ public boolean isMonthlyMode() { - CalendarState state = (CalendarState) getState(false); + CalendarState state = getState(false); if (state.days != null) { return state.days.size() > 7; } else { @@ -1895,4 +1900,34 @@ public class Calendar extends AbstractComponent implements dropHandler.getAcceptCriterion().paint(target); } } + + /** + * Sets whether the event captions are rendered as HTML. + *

    + * If set to true, the captions are rendered in the browser as HTML and the + * developer is responsible for ensuring no harmful HTML is used. If set to + * false, the caption is rendered in the browser as plain text. + *

    + * The default is false, i.e. to render that caption as plain text. + * + * @param captionAsHtml + * true if the captions are rendered as HTML, false if rendered + * as plain text + */ + public void setEventCaptionAsHtml(boolean eventCaptionAsHtml) { + getState().eventCaptionAsHtml = eventCaptionAsHtml; + } + + /** + * Checks whether event captions are rendered as HTML + *

    + * The default is false, i.e. to render that caption as plain text. + * + * @return true if the captions are rendered as HTML, false if rendered as + * plain text + */ + public boolean isEventCaptionAsHtml() { + return getState(false).eventCaptionAsHtml; + } + } diff --git a/shared/src/com/vaadin/shared/ui/calendar/CalendarState.java b/shared/src/com/vaadin/shared/ui/calendar/CalendarState.java index 93bd05bc1e..c26c4ead16 100644 --- a/shared/src/com/vaadin/shared/ui/calendar/CalendarState.java +++ b/shared/src/com/vaadin/shared/ui/calendar/CalendarState.java @@ -38,6 +38,7 @@ public class CalendarState extends AbstractComponentState { public List days; public List events; public List actions; + public boolean eventCaptionAsHtml; public static class Day implements java.io.Serializable { public String date; diff --git a/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEvents.java b/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEvents.java new file mode 100644 index 0000000000..15cde71838 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEvents.java @@ -0,0 +1,101 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.tests.components.calendar; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.util.MethodProperty; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Calendar; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.NativeSelect; +import com.vaadin.ui.components.calendar.event.BasicEvent; +import com.vaadin.ui.components.calendar.event.CalendarEvent; +import com.vaadin.ui.components.calendar.event.CalendarEventProvider; + +public class CalendarHtmlInEvents extends AbstractTestUIWithLog { + + private Calendar calendar = new Calendar(); + + @Override + protected void setup(VaadinRequest request) { + final NativeSelect ns = new NativeSelect("Period"); + ns.addItems("Day", "Week", "Month"); + ns.addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + if ("Day".equals(ns.getValue())) { + calendar.setStartDate(new Date(2014 - 1900, 1 - 1, 1)); + calendar.setEndDate(new Date(2014 - 1900, 1 - 1, 1)); + } else if ("Week".equals(ns.getValue())) { + calendar.setStartDate(new Date(2014 - 1900, 1 - 1, 1)); + calendar.setEndDate(new Date(2014 - 1900, 1 - 1, 7)); + } else if ("Month".equals(ns.getValue())) { + calendar.setStartDate(new Date(2014 - 1900, 1 - 1, 1)); + calendar.setEndDate(new Date(2014 - 1900, 2 - 1, 1)); + } + } + }); + ns.setValue("Month"); + final CheckBox allowHtml = new CheckBox("Allow HTML in event caption", + new MethodProperty(calendar, "eventCaptionAsHtml")); + allowHtml.addValueChangeListener(new ValueChangeListener() { + @Override + public void valueChange(ValueChangeEvent event) { + log("HTML in event caption: " + allowHtml.getValue()); + } + }); + HorizontalLayout hl = new HorizontalLayout(); + hl.setDefaultComponentAlignment(Alignment.BOTTOM_LEFT); + hl.addComponents(ns, allowHtml); + hl.setSpacing(true); + hl.setMargin(true); + calendar.setEventProvider(new CalendarEventProvider() { + + @Override + public List getEvents(Date startDate, Date endDate) { + Date d = startDate; + ArrayList events = new ArrayList(); + while (d.before(endDate)) { + BasicEvent ce = new BasicEvent(); + ce.setAllDay(false); + ce.setCaption("Hello world!"); + ce.setDescription("Nothing really important"); + Date start = new Date(d.getTime()); + start.setHours(d.getDay()); + Date end = new Date(d.getTime()); + end.setHours(d.getDay() + 3); + ce.setStart(start); + ce.setEnd(end); + events.add(ce); + d.setTime(d.getTime() + 1000 * 60 * 60 * 24); + } + + return events; + } + + }); + addComponent(hl); + addComponent(calendar); + } +} diff --git a/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEventsTest.java b/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEventsTest.java new file mode 100644 index 0000000000..31e3f754e3 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/calendar/CalendarHtmlInEventsTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * 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.vaadin.tests.components.calendar; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.elements.CalendarElement; +import com.vaadin.testbench.elements.CheckBoxElement; +import com.vaadin.testbench.elements.NativeSelectElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class CalendarHtmlInEventsTest extends SingleBrowserTest { + + private NativeSelectElement periodSelect; + private CheckBoxElement htmlAllowed; + private CalendarElement calendar; + + @Override + public void setup() throws Exception { + super.setup(); + openTestURL(); + periodSelect = $(NativeSelectElement.class).first(); + htmlAllowed = $(CheckBoxElement.class).first(); + calendar = $(CalendarElement.class).first(); + } + + @Test + public void monthViewEventCaptions() { + Assert.assertEquals(getMonthEvent(0).getText(), + "12:00 AM Hello world!"); + + // Switch to HTML mode + click(htmlAllowed); + Assert.assertEquals("1. HTML in event caption: true", getLogRow(0)); + + Assert.assertEquals(getMonthEvent(0).getText(), "12:00 AM Hello world!"); + } + + @Test + public void weekViewEventCaptions() { + periodSelect.selectByText("Week"); + Assert.assertEquals("4:00 AM\nHello world!", + getWeekEvent(1).getText()); + + // Switch to HTML mode + click(htmlAllowed); + Assert.assertEquals("1. HTML in event caption: true", getLogRow(0)); + + Assert.assertEquals("4:00 AM\nHello world!", getWeekEvent(1).getText()); + } + + @Test + public void dayViewEventCaptions() { + periodSelect.selectByText("Day"); + Assert.assertEquals("3:00 AM\nHello world!", + getWeekEvent(0).getText()); + + // Switch to HTML mode + click(htmlAllowed); + Assert.assertEquals("1. HTML in event caption: true", getLogRow(0)); + Assert.assertEquals("3:00 AM\nHello world!", getWeekEvent(0).getText()); + } + + private WebElement getMonthEvent(int dayInCalendar) { + return getMonthDay(dayInCalendar).findElement( + By.className("v-calendar-event")); + } + + private WebElement getWeekEvent(int dayInCalendar) { + return getWeekDay(dayInCalendar).findElement( + By.className("v-calendar-event")); + } + + private void click(CheckBoxElement htmlAllowed2) { + htmlAllowed2.findElement(By.xpath("input")).click(); + } + + private WebElement getMonthDay(int i) { + return calendar.findElements(By.className("v-calendar-month-day")).get( + i); + } + + private WebElement getWeekDay(int i) { + return calendar.findElements(By.className("v-calendar-day-times")).get( + i); + } +} -- 2.39.5