diff options
author | Artur Signell <artur@vaadin.com> | 2016-08-18 22:10:47 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-08-20 00:12:18 +0300 |
commit | fe3dca081a64af892a7f4c0416ecc643aec3ec5a (patch) | |
tree | 1901fb377336d3c5a772335322d9c434a4a75e24 /compatibility-client/src | |
parent | 65370e12a0605926cb80e205c2b0e74fefe83e5b (diff) | |
download | vaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.tar.gz vaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.zip |
Move remaining selects and container implementations to compatibility package
Because of dependencies also moves
Calendar, ColorPicker, SQLContainer, container filters
Change-Id: I0594cb24f20486ebbca4be578827fea7cdf92108
Diffstat (limited to 'compatibility-client/src')
43 files changed, 14686 insertions, 0 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java new file mode 100644 index 0000000000..8414d7c99c --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java @@ -0,0 +1,1504 @@ +/* + * Copyright 2000-2016 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.client.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.DockPanel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.calendar.schedule.CalendarDay; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.client.ui.calendar.schedule.DayToolbar; +import com.vaadin.client.ui.calendar.schedule.MonthGrid; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.calendar.schedule.SimpleDayToolbar; +import com.vaadin.client.ui.calendar.schedule.SimpleWeekToolbar; +import com.vaadin.client.ui.calendar.schedule.WeekGrid; +import com.vaadin.client.ui.calendar.schedule.WeeklyLongEvents; +import com.vaadin.client.ui.calendar.schedule.dd.CalendarDropHandler; +import com.vaadin.client.ui.dd.VHasDropHandler; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Client side implementation for Calendar + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class VCalendar extends Composite implements VHasDropHandler { + + public static final String ATTR_FIRSTDAYOFWEEK = "firstDay"; + public static final String ATTR_LASTDAYOFWEEK = "lastDay"; + public static final String ATTR_FIRSTHOUROFDAY = "firstHour"; + public static final String ATTR_LASTHOUROFDAY = "lastHour"; + + // private boolean hideWeekends; + private String[] monthNames; + private String[] dayNames; + private boolean format; + private final DockPanel outer = new DockPanel(); + private int rows; + + private boolean rangeSelectAllowed = true; + private boolean rangeMoveAllowed = true; + private boolean eventResizeAllowed = true; + private boolean eventMoveAllowed = true; + + private final SimpleDayToolbar nameToolbar = new SimpleDayToolbar(); + + private final DayToolbar dayToolbar = new DayToolbar(this); + private final SimpleWeekToolbar weekToolbar; + private WeeklyLongEvents weeklyLongEvents; + private MonthGrid monthGrid; + private WeekGrid weekGrid; + private int intWidth = 0; + private int intHeight = 0; + + protected final DateTimeFormat dateformat_datetime = DateTimeFormat + .getFormat("yyyy-MM-dd HH:mm:ss"); + protected final DateTimeFormat dateformat_date = DateTimeFormat + .getFormat("yyyy-MM-dd"); + protected final DateTimeFormat time12format_date = DateTimeFormat + .getFormat("h:mm a"); + protected final DateTimeFormat time24format_date = DateTimeFormat + .getFormat("HH:mm"); + + private boolean readOnly = false; + private boolean disabled = false; + + private boolean isHeightUndefined = false; + + private boolean isWidthUndefined = false; + private int firstDay; + private int lastDay; + private int firstHour; + private int lastHour; + + private CalendarDropHandler dropHandler; + + /** + * Listener interface for listening to event click events + */ + public interface DateClickListener { + /** + * Triggered when a date was clicked + * + * @param date + * The date and time that was clicked + */ + void dateClick(String date); + } + + /** + * Listener interface for listening to week number click events + */ + public interface WeekClickListener { + /** + * Called when a week number was selected. + * + * @param event + * The format of the vent string is "<year>w<week>" + */ + void weekClick(String event); + } + + /** + * Listener interface for listening to forward events + */ + public interface ForwardListener { + + /** + * Called when the calendar should move one view forward + */ + void forward(); + } + + /** + * Listener interface for listening to backward events + */ + public interface BackwardListener { + + /** + * Called when the calendar should move one view backward + */ + void backward(); + } + + /** + * Listener interface for listening to selection events + */ + public interface RangeSelectListener { + + /** + * Called when a user selected a new event by highlighting an area of + * the calendar. + * + * FIXME Fix the value nonsense. + * + * @param value + * The format of the value string is + * "<year>:<start-minutes>:<end-minutes>" if called from the + * {@link SimpleWeekToolbar} and "<yyyy-MM-dd>TO<yyyy-MM-dd>" + * if called from {@link MonthGrid} + */ + void rangeSelected(String value); + } + + /** + * Listener interface for listening to click events + */ + public interface EventClickListener { + /** + * Called when an event was clicked + * + * @param event + * The event that was clicked + */ + void eventClick(CalendarEvent event); + } + + /** + * Listener interface for listening to event moved events. Occurs when a + * user drags an event to a new position + */ + public interface EventMovedListener { + /** + * Triggered when an event was dragged to a new position and the start + * and end dates was changed + * + * @param event + * The event that was moved + */ + void eventMoved(CalendarEvent event); + } + + /** + * Listener interface for when an event gets resized (its start or end date + * changes) + */ + public interface EventResizeListener { + /** + * Triggers when the time limits for the event was changed. + * + * @param event + * The event that was changed. The new time limits have been + * updated in the event before calling this method + */ + void eventResized(CalendarEvent event); + } + + /** + * Listener interface for listening to scroll events. + */ + public interface ScrollListener { + /** + * Triggered when the calendar is scrolled + * + * @param scrollPosition + * The scroll position in pixels as returned by + * {@link ScrollPanel#getScrollPosition()} + */ + void scroll(int scrollPosition); + } + + /** + * Listener interface for listening to mouse events. + */ + public interface MouseEventListener { + /** + * Triggered when a user wants an context menu + * + * @param event + * The context menu event + * + * @param widget + * The widget that the context menu should be added to + */ + void contextMenu(ContextMenuEvent event, Widget widget); + } + + /** + * Default constructor + */ + public VCalendar() { + weekToolbar = new SimpleWeekToolbar(this); + initWidget(outer); + setStylePrimaryName("v-calendar"); + blockSelect(getElement()); + } + + /** + * Hack for IE to not select text when dragging. + * + * @param e + * The element to apply the hack on + */ + private native void blockSelect(Element e) + /*-{ + e.onselectstart = function() { + return false; + } + + e.ondragstart = function() { + return false; + } + }-*/; + + private void updateEventsToWeekGrid(CalendarEvent[] events) { + List<CalendarEvent> allDayLong = new ArrayList<CalendarEvent>(); + List<CalendarEvent> belowDayLong = new ArrayList<CalendarEvent>(); + + for (CalendarEvent e : events) { + if (e.isAllDay()) { + // Event is set on one "allDay" event or more than one. + allDayLong.add(e); + + } else { + // Event is set only on one day. + belowDayLong.add(e); + } + } + + weeklyLongEvents.addEvents(allDayLong); + + for (CalendarEvent e : belowDayLong) { + weekGrid.addEvent(e); + } + } + + /** + * Adds events to the month grid + * + * @param events + * The events to add + * @param drawImmediately + * Should the grid be rendered immediately. (currently not in + * use) + * + */ + public void updateEventsToMonthGrid(Collection<CalendarEvent> events, + boolean drawImmediately) { + for (CalendarEvent e : sortEventsByDuration(events)) { + // FIXME Why is drawImmediately not used ????? + addEventToMonthGrid(e, false); + } + } + + private void addEventToMonthGrid(CalendarEvent e, + boolean renderImmediately) { + Date when = e.getStart(); + Date to = e.getEnd(); + boolean eventAdded = false; + boolean inProgress = false; // Event adding has started + boolean eventMoving = false; + List<SimpleDayCell> dayCells = new ArrayList<SimpleDayCell>(); + List<SimpleDayCell> timeCells = new ArrayList<SimpleDayCell>(); + for (int row = 0; row < monthGrid.getRowCount(); row++) { + if (eventAdded) { + break; + } + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row, + cell); + if (isEventInDay(when, to, sdc.getDate()) + && isEventInDayWithTime(when, to, sdc.getDate(), + e.getEndTime(), e.isAllDay())) { + if (!eventMoving) { + eventMoving = sdc.getMoveEvent() != null; + } + long d = e.getRangeInMilliseconds(); + if ((d > 0 && d <= DateConstants.DAYINMILLIS) + && !e.isAllDay()) { + timeCells.add(sdc); + } else { + dayCells.add(sdc); + } + inProgress = true; + continue; + } else if (inProgress) { + eventAdded = true; + inProgress = false; + break; + } + } + } + + updateEventSlotIndex(e, dayCells); + updateEventSlotIndex(e, timeCells); + + for (SimpleDayCell sdc : dayCells) { + sdc.addCalendarEvent(e); + } + for (SimpleDayCell sdc : timeCells) { + sdc.addCalendarEvent(e); + } + + if (renderImmediately) { + reDrawAllMonthEvents(!eventMoving); + } + } + + /* + * We must also handle the special case when the event lasts exactly for 24 + * hours, thus spanning two days e.g. from 1.1.2001 00:00 to 2.1.2001 00:00. + * That special case still should span one day when rendered. + */ + @SuppressWarnings("deprecation") + // Date methods are not deprecated in GWT + private boolean isEventInDayWithTime(Date from, Date to, Date date, + Date endTime, boolean isAllDay) { + return (isAllDay || !(to.getDay() == date.getDay() + && from.getDay() != to.getDay() && isMidnight(endTime))); + } + + private void updateEventSlotIndex(CalendarEvent e, + List<SimpleDayCell> cells) { + if (cells.isEmpty()) { + return; + } + + if (e.getSlotIndex() == -1) { + // Update slot index + int newSlot = -1; + for (SimpleDayCell sdc : cells) { + int slot = sdc.getEventCount(); + if (slot > newSlot) { + newSlot = slot; + } + } + newSlot++; + + for (int i = 0; i < newSlot; i++) { + // check for empty slot + if (isSlotEmpty(e, i, cells)) { + newSlot = i; + break; + } + } + e.setSlotIndex(newSlot); + } + } + + private void reDrawAllMonthEvents(boolean clearCells) { + for (int row = 0; row < monthGrid.getRowCount(); row++) { + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row, + cell); + sdc.reDraw(clearCells); + } + } + } + + private boolean isSlotEmpty(CalendarEvent addedEvent, int slotIndex, + List<SimpleDayCell> cells) { + for (SimpleDayCell sdc : cells) { + CalendarEvent e = sdc.getCalendarEvent(slotIndex); + if (e != null && !e.equals(addedEvent)) { + return false; + } + } + return true; + } + + /** + * Remove a month event from the view + * + * @param target + * The event to remove + * + * @param repaintImmediately + * Should we repaint after the event was removed? + */ + public void removeMonthEvent(CalendarEvent target, + boolean repaintImmediately) { + if (target != null && target.getSlotIndex() >= 0) { + // Remove event + for (int row = 0; row < monthGrid.getRowCount(); row++) { + for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row, + cell); + if (sdc == null) { + return; + } + sdc.removeEvent(target, repaintImmediately); + } + } + } + } + + /** + * Updates an event in the month grid + * + * @param changedEvent + * The event that has changed + */ + public void updateEventToMonthGrid(CalendarEvent changedEvent) { + removeMonthEvent(changedEvent, true); + changedEvent.setSlotIndex(-1); + addEventToMonthGrid(changedEvent, true); + } + + /** + * Sort the event by how long they are + * + * @param events + * The events to sort + * @return An array where the events has been sorted + */ + public CalendarEvent[] sortEventsByDuration( + Collection<CalendarEvent> events) { + CalendarEvent[] sorted = events + .toArray(new CalendarEvent[events.size()]); + Arrays.sort(sorted, getEventComparator()); + return sorted; + } + + /* + * Check if the given event occurs at the given date. + */ + private boolean isEventInDay(Date eventWhen, Date eventTo, Date gridDate) { + if (eventWhen.compareTo(gridDate) <= 0 + && eventTo.compareTo(gridDate) >= 0) { + + return true; + } + + return false; + } + + /** + * Re-render the week grid + * + * @param daysCount + * The amount of days to include in the week + * @param days + * The days + * @param today + * Todays date + * @param realDayNames + * The names of the dates + */ + @SuppressWarnings("deprecation") + public void updateWeekGrid(int daysCount, List<CalendarDay> days, + Date today, String[] realDayNames) { + weekGrid.setFirstHour(getFirstHourOfTheDay()); + weekGrid.setLastHour(getLastHourOfTheDay()); + weekGrid.getTimeBar().updateTimeBar(is24HFormat()); + + dayToolbar.clear(); + dayToolbar.addBackButton(); + dayToolbar.setVerticalSized(isHeightUndefined); + dayToolbar.setHorizontalSized(isWidthUndefined); + weekGrid.clearDates(); + weekGrid.setDisabled(isDisabledOrReadOnly()); + + for (CalendarDay day : days) { + String date = day.getDate(); + String localized_date_format = day.getLocalizedDateFormat(); + Date d = dateformat_date.parse(date); + int dayOfWeek = day.getDayOfWeek(); + if (dayOfWeek < getFirstDayNumber() + || dayOfWeek > getLastDayNumber()) { + continue; + } + boolean isToday = false; + int dayOfMonth = d.getDate(); + if (today.getDate() == dayOfMonth && today.getYear() == d.getYear() + && today.getMonth() == d.getMonth()) { + isToday = true; + } + dayToolbar.add(realDayNames[dayOfWeek - 1], date, + localized_date_format, isToday ? "today" : null); + weeklyLongEvents.addDate(d); + weekGrid.addDate(d); + if (isToday) { + weekGrid.setToday(d, today); + } + } + dayToolbar.addNextButton(); + } + + /** + * Updates the events in the Month view + * + * @param daysCount + * How many days there are + * @param daysUidl + * + * @param today + * Todays date + */ + @SuppressWarnings("deprecation") + public void updateMonthGrid(int daysCount, List<CalendarDay> days, + Date today) { + int columns = getLastDayNumber() - getFirstDayNumber() + 1; + rows = (int) Math.ceil(daysCount / (double) 7); + + monthGrid = new MonthGrid(this, rows, columns); + monthGrid.setEnabled(!isDisabledOrReadOnly()); + weekToolbar.removeAllRows(); + int pos = 0; + boolean monthNameDrawn = true; + boolean firstDayFound = false; + boolean lastDayFound = false; + + for (CalendarDay day : days) { + String date = day.getDate(); + Date d = dateformat_date.parse(date); + int dayOfWeek = day.getDayOfWeek(); + int week = day.getWeek(); + + int dayOfMonth = d.getDate(); + + // reset at start of each month + if (dayOfMonth == 1) { + monthNameDrawn = false; + if (firstDayFound) { + lastDayFound = true; + } + firstDayFound = true; + } + + if (dayOfWeek < getFirstDayNumber() + || dayOfWeek > getLastDayNumber()) { + continue; + } + int y = (pos / columns); + int x = pos - (y * columns); + if (x == 0 && daysCount > 7) { + // Add week to weekToolbar for navigation + weekToolbar.addWeek(week, day.getYearOfWeek()); + } + final SimpleDayCell cell = new SimpleDayCell(this, y, x); + cell.setMonthGrid(monthGrid); + cell.setDate(d); + cell.addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + if (mouseEventListener != null) { + event.preventDefault(); + event.stopPropagation(); + mouseEventListener.contextMenu(event, cell); + } + } + }, ContextMenuEvent.getType()); + + if (!firstDayFound) { + cell.addStyleDependentName("prev-month"); + } else if (lastDayFound) { + cell.addStyleDependentName("next-month"); + } + + if (dayOfMonth >= 1 && !monthNameDrawn) { + cell.setMonthNameVisible(true); + monthNameDrawn = true; + } + + if (today.getDate() == dayOfMonth && today.getYear() == d.getYear() + && today.getMonth() == d.getMonth()) { + cell.setToday(true); + + } + monthGrid.setWidget(y, x, cell); + pos++; + } + } + + public void setSizeForChildren(int newWidth, int newHeight) { + intWidth = newWidth; + intHeight = newHeight; + isWidthUndefined = intWidth == -1; + dayToolbar.setVerticalSized(isHeightUndefined); + dayToolbar.setHorizontalSized(isWidthUndefined); + recalculateWidths(); + recalculateHeights(); + } + + /** + * Recalculates the heights of the sub-components in the calendar + */ + protected void recalculateHeights() { + if (monthGrid != null) { + + if (intHeight == -1) { + monthGrid.addStyleDependentName("sizedheight"); + } else { + monthGrid.removeStyleDependentName("sizedheight"); + } + + monthGrid.updateCellSizes(intWidth - weekToolbar.getOffsetWidth(), + intHeight - nameToolbar.getOffsetHeight()); + weekToolbar.setHeightPX((intHeight == -1) ? intHeight + : intHeight - nameToolbar.getOffsetHeight()); + + } else if (weekGrid != null) { + weekGrid.setHeightPX((intHeight == -1) ? intHeight + : intHeight - weeklyLongEvents.getOffsetHeight() + - dayToolbar.getOffsetHeight()); + } + } + + /** + * Recalculates the widths of the sub-components in the calendar + */ + protected void recalculateWidths() { + if (!isWidthUndefined) { + nameToolbar.setWidthPX(intWidth); + dayToolbar.setWidthPX(intWidth); + + if (monthGrid != null) { + monthGrid.updateCellSizes( + intWidth - weekToolbar.getOffsetWidth(), + intHeight - nameToolbar.getOffsetHeight()); + } else if (weekGrid != null) { + weekGrid.setWidthPX(intWidth); + weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth()); + } + } else { + dayToolbar.setWidthPX(intWidth); + nameToolbar.setWidthPX(intWidth); + + if (monthGrid != null) { + if (intWidth == -1) { + monthGrid.addStyleDependentName("sizedwidth"); + + } else { + monthGrid.removeStyleDependentName("sizedwidth"); + } + } else if (weekGrid != null) { + weekGrid.setWidthPX(intWidth); + weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth()); + } + } + } + + /** + * Get the date format used to format dates only (excludes time) + * + * @return + */ + public DateTimeFormat getDateFormat() { + return dateformat_date; + } + + /** + * Get the time format used to format time only (excludes date) + * + * @return + */ + public DateTimeFormat getTimeFormat() { + if (is24HFormat()) { + return time24format_date; + } + return time12format_date; + } + + /** + * Get the date and time format to format the dates (includes both date and + * time) + * + * @return + */ + public DateTimeFormat getDateTimeFormat() { + return dateformat_datetime; + } + + /** + * Is the calendar either disabled or readonly + * + * @return + */ + public boolean isDisabledOrReadOnly() { + return disabled || readOnly; + } + + /** + * Is the component disabled + */ + public boolean isDisabled() { + return disabled; + } + + /** + * Is the component disabled + * + * @param disabled + * True if disabled + */ + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + /** + * Is the component read-only + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Is the component read-only + * + * @param readOnly + * True if component is readonly + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Get the month grid component + * + * @return + */ + public MonthGrid getMonthGrid() { + return monthGrid; + } + + /** + * Get he week grid component + * + * @return + */ + public WeekGrid getWeekGrid() { + return weekGrid; + } + + /** + * Calculates correct size for all cells (size / amount of cells ) and + * distributes any overflow over all the cells. + * + * @param totalSize + * the total amount of size reserved for all cells + * @param numberOfCells + * the number of cells + * @param sizeModifier + * a modifier which is applied to all cells before distributing + * the overflow + * @return an integer array that contains the correct size for each cell + */ + public static int[] distributeSize(int totalSize, int numberOfCells, + int sizeModifier) { + int[] cellSizes = new int[numberOfCells]; + int startingSize = totalSize / numberOfCells; + int cellSizeOverFlow = totalSize % numberOfCells; + + for (int i = 0; i < numberOfCells; i++) { + cellSizes[i] = startingSize + sizeModifier; + } + + // distribute size overflow amongst all slots + int j = 0; + while (cellSizeOverFlow > 0) { + cellSizes[j]++; + cellSizeOverFlow--; + j++; + if (j >= numberOfCells) { + j = 0; + } + } + + // cellSizes[numberOfCells - 1] += cellSizeOverFlow; + + return cellSizes; + } + + /** + * Returns a comparator which can compare calendar events. + * + * @return + */ + public static Comparator<CalendarEvent> getEventComparator() { + return new Comparator<CalendarEvent>() { + + @Override + public int compare(CalendarEvent o1, CalendarEvent o2) { + if (o1.isAllDay() != o2.isAllDay()) { + if (o2.isAllDay()) { + return 1; + } + return -1; + } + + Long d1 = o1.getRangeInMilliseconds(); + Long d2 = o2.getRangeInMilliseconds(); + int r = 0; + if (!d1.equals(0L) && !d2.equals(0L)) { + r = d2.compareTo(d1); + return (r == 0) + ? ((Integer) o2.getIndex()).compareTo(o1.getIndex()) + : r; + } + + if (d2.equals(0L) && d1.equals(0L)) { + return ((Integer) o2.getIndex()).compareTo(o1.getIndex()); + } else if (d2.equals(0L) && d1 >= DateConstants.DAYINMILLIS) { + return -1; + } else if (d2.equals(0L) && d1 < DateConstants.DAYINMILLIS) { + return 1; + } else if (d1.equals(0L) && d2 >= DateConstants.DAYINMILLIS) { + return 1; + } else if (d1.equals(0L) && d2 < DateConstants.DAYINMILLIS) { + return -1; + } + r = d2.compareTo(d1); + return (r == 0) + ? ((Integer) o2.getIndex()).compareTo(o1.getIndex()) + : r; + } + }; + } + + /** + * Is the date at midnight + * + * @param date + * The date to check + * + * @return + */ + @SuppressWarnings("deprecation") + public static boolean isMidnight(Date date) { + return (date.getHours() == 0 && date.getMinutes() == 0 + && date.getSeconds() == 0); + } + + /** + * Are the dates equal (uses second resolution) + * + * @param date1 + * The first the to compare + * @param date2 + * The second date to compare + * @return + */ + @SuppressWarnings("deprecation") + public static boolean areDatesEqualToSecond(Date date1, Date date2) { + return date1.getYear() == date2.getYear() + && date1.getMonth() == date2.getMonth() + && date1.getDay() == date2.getDay() + && date1.getHours() == date2.getHours() + && date1.getSeconds() == date2.getSeconds(); + } + + /** + * Is the calendar event zero seconds long and is occurring at midnight + * + * @param event + * The event to check + * @return + */ + public static boolean isZeroLengthMidnightEvent(CalendarEvent event) { + return areDatesEqualToSecond(event.getStartTime(), event.getEndTime()) + && isMidnight(event.getEndTime()); + } + + /** + * Should the 24h time format be used + * + * @param format + * True if the 24h format should be used else the 12h format is + * used + */ + public void set24HFormat(boolean format) { + this.format = format; + } + + /** + * Is the 24h time format used + */ + public boolean is24HFormat() { + return format; + } + + /** + * Set the names of the week days + * + * @param names + * The names of the days (Monday, Thursday,...) + */ + public void setDayNames(String[] names) { + assert (names.length == 7); + dayNames = names; + } + + /** + * Get the names of the week days + */ + public String[] getDayNames() { + return dayNames; + } + + /** + * Set the names of the months + * + * @param names + * The names of the months (January, February,...) + */ + public void setMonthNames(String[] names) { + assert (names.length == 12); + monthNames = names; + } + + /** + * Get the month names + */ + public String[] getMonthNames() { + return monthNames; + } + + /** + * Set the number when a week starts + * + * @param dayNumber + * The number of the day + */ + public void setFirstDayNumber(int dayNumber) { + assert (dayNumber >= 1 && dayNumber <= 7); + firstDay = dayNumber; + } + + /** + * Get the number when a week starts + */ + public int getFirstDayNumber() { + return firstDay; + } + + /** + * Set the number when a week ends + * + * @param dayNumber + * The number of the day + */ + public void setLastDayNumber(int dayNumber) { + assert (dayNumber >= 1 && dayNumber <= 7); + lastDay = dayNumber; + } + + /** + * Get the number when a week ends + */ + public int getLastDayNumber() { + return lastDay; + } + + /** + * Set the number when a week starts + * + * @param dayNumber + * The number of the day + */ + public void setFirstHourOfTheDay(int hour) { + assert (hour >= 0 && hour <= 23); + firstHour = hour; + } + + /** + * Get the number when a week starts + */ + public int getFirstHourOfTheDay() { + return firstHour; + } + + /** + * Set the number when a week ends + * + * @param dayNumber + * The number of the day + */ + public void setLastHourOfTheDay(int hour) { + assert (hour >= 0 && hour <= 23); + lastHour = hour; + } + + /** + * Get the number when a week ends + */ + public int getLastHourOfTheDay() { + return lastHour; + } + + /** + * Re-renders the whole week view + * + * @param scroll + * The amount of pixels to scroll the week view + * @param today + * Todays date + * @param daysInMonth + * How many days are there in the month + * @param firstDayOfWeek + * The first day of the week + * @param events + * The events to render + */ + public void updateWeekView(int scroll, Date today, int daysInMonth, + int firstDayOfWeek, Collection<CalendarEvent> events, + List<CalendarDay> days) { + + while (outer.getWidgetCount() > 0) { + outer.remove(0); + } + + monthGrid = null; + String[] realDayNames = new String[getDayNames().length]; + int j = 0; + + if (firstDayOfWeek == 2) { + for (int i = 1; i < getDayNames().length; i++) { + realDayNames[j++] = getDayNames()[i]; + } + realDayNames[j] = getDayNames()[0]; + } else { + for (int i = 0; i < getDayNames().length; i++) { + realDayNames[j++] = getDayNames()[i]; + } + + } + + weeklyLongEvents = new WeeklyLongEvents(this); + if (weekGrid == null) { + weekGrid = new WeekGrid(this, is24HFormat()); + } + updateWeekGrid(daysInMonth, days, today, realDayNames); + updateEventsToWeekGrid(sortEventsByDuration(events)); + outer.add(dayToolbar, DockPanel.NORTH); + outer.add(weeklyLongEvents, DockPanel.NORTH); + outer.add(weekGrid, DockPanel.SOUTH); + weekGrid.setVerticalScrollPosition(scroll); + } + + /** + * Re-renders the whole month view + * + * @param firstDayOfWeek + * The first day of the week + * @param today + * Todays date + * @param daysInMonth + * Amount of days in the month + * @param events + * The events to render + * @param days + * The day information + */ + public void updateMonthView(int firstDayOfWeek, Date today, int daysInMonth, + Collection<CalendarEvent> events, List<CalendarDay> days) { + + // Remove all week numbers from bar + while (outer.getWidgetCount() > 0) { + outer.remove(0); + } + + int firstDay = getFirstDayNumber(); + int lastDay = getLastDayNumber(); + int daysPerWeek = lastDay - firstDay + 1; + int j = 0; + + String[] dayNames = getDayNames(); + String[] realDayNames = new String[daysPerWeek]; + + if (firstDayOfWeek == 2) { + for (int i = firstDay; i < lastDay + 1; i++) { + if (i == 7) { + realDayNames[j++] = dayNames[0]; + } else { + realDayNames[j++] = dayNames[i]; + } + } + } else { + for (int i = firstDay - 1; i < lastDay; i++) { + realDayNames[j++] = dayNames[i]; + } + } + + nameToolbar.setDayNames(realDayNames); + + weeklyLongEvents = null; + weekGrid = null; + + updateMonthGrid(daysInMonth, days, today); + + outer.add(nameToolbar, DockPanel.NORTH); + outer.add(weekToolbar, DockPanel.WEST); + weekToolbar.updateCellHeights(); + outer.add(monthGrid, DockPanel.CENTER); + + updateEventsToMonthGrid(events, false); + } + + private DateClickListener dateClickListener; + + /** + * Sets the listener for listening to event clicks + * + * @param listener + * The listener to use + */ + public void setListener(DateClickListener listener) { + dateClickListener = listener; + } + + /** + * Gets the listener for listening to event clicks + * + * @return + */ + public DateClickListener getDateClickListener() { + return dateClickListener; + } + + private ForwardListener forwardListener; + + /** + * Set the listener which listens to forward events from the calendar + * + * @param listener + * The listener to use + */ + public void setListener(ForwardListener listener) { + forwardListener = listener; + } + + /** + * Get the listener which listens to forward events from the calendar + * + * @return + */ + public ForwardListener getForwardListener() { + return forwardListener; + } + + private BackwardListener backwardListener; + + /** + * Set the listener which listens to backward events from the calendar + * + * @param listener + * The listener to use + */ + public void setListener(BackwardListener listener) { + backwardListener = listener; + } + + /** + * Set the listener which listens to backward events from the calendar + * + * @return + */ + public BackwardListener getBackwardListener() { + return backwardListener; + } + + private WeekClickListener weekClickListener; + + /** + * Set the listener that listens to user clicking on the week numbers + * + * @param listener + * The listener to use + */ + public void setListener(WeekClickListener listener) { + weekClickListener = listener; + } + + /** + * Get the listener that listens to user clicking on the week numbers + * + * @return + */ + public WeekClickListener getWeekClickListener() { + return weekClickListener; + } + + private RangeSelectListener rangeSelectListener; + + /** + * Set the listener that listens to the user highlighting a region in the + * calendar + * + * @param listener + * The listener to use + */ + public void setListener(RangeSelectListener listener) { + rangeSelectListener = listener; + } + + /** + * Get the listener that listens to the user highlighting a region in the + * calendar + * + * @return + */ + public RangeSelectListener getRangeSelectListener() { + return rangeSelectListener; + } + + private EventClickListener eventClickListener; + + /** + * Get the listener that listens to the user clicking on the events + */ + public EventClickListener getEventClickListener() { + return eventClickListener; + } + + /** + * Set the listener that listens to the user clicking on the events + * + * @param listener + * The listener to use + */ + public void setListener(EventClickListener listener) { + eventClickListener = listener; + } + + private EventMovedListener eventMovedListener; + + /** + * Get the listener that listens to when event is dragged to a new location + * + * @return + */ + public EventMovedListener getEventMovedListener() { + return eventMovedListener; + } + + /** + * Set the listener that listens to when event is dragged to a new location + * + * @param eventMovedListener + * The listener to use + */ + public void setListener(EventMovedListener eventMovedListener) { + this.eventMovedListener = eventMovedListener; + } + + private ScrollListener scrollListener; + + /** + * Get the listener that listens to when the calendar widget is scrolled + * + * @return + */ + public ScrollListener getScrollListener() { + return scrollListener; + } + + /** + * Set the listener that listens to when the calendar widget is scrolled + * + * @param scrollListener + * The listener to use + */ + public void setListener(ScrollListener scrollListener) { + this.scrollListener = scrollListener; + } + + private EventResizeListener eventResizeListener; + + /** + * Get the listener that listens to when an events time limits are being + * adjusted + * + * @return + */ + public EventResizeListener getEventResizeListener() { + return eventResizeListener; + } + + /** + * Set the listener that listens to when an events time limits are being + * adjusted + * + * @param eventResizeListener + * The listener to use + */ + public void setListener(EventResizeListener eventResizeListener) { + this.eventResizeListener = eventResizeListener; + } + + private MouseEventListener mouseEventListener; + private boolean forwardNavigationEnabled = true; + private boolean backwardNavigationEnabled = true; + private boolean eventCaptionAsHtml = false; + + /** + * Get the listener that listen to mouse events + * + * @return + */ + public MouseEventListener getMouseEventListener() { + return mouseEventListener; + } + + /** + * Set the listener that listen to mouse events + * + * @param mouseEventListener + * The listener to use + */ + public void setListener(MouseEventListener mouseEventListener) { + this.mouseEventListener = mouseEventListener; + } + + /** + * Is selecting a range allowed? + */ + public boolean isRangeSelectAllowed() { + return rangeSelectAllowed; + } + + /** + * Set selecting a range allowed + * + * @param rangeSelectAllowed + * Should selecting a range be allowed + */ + public void setRangeSelectAllowed(boolean rangeSelectAllowed) { + this.rangeSelectAllowed = rangeSelectAllowed; + } + + /** + * Is moving a range allowed + * + * @return + */ + public boolean isRangeMoveAllowed() { + return rangeMoveAllowed; + } + + /** + * Is moving a range allowed + * + * @param rangeMoveAllowed + * Is it allowed + */ + public void setRangeMoveAllowed(boolean rangeMoveAllowed) { + this.rangeMoveAllowed = rangeMoveAllowed; + } + + /** + * Is resizing an event allowed + */ + public boolean isEventResizeAllowed() { + return eventResizeAllowed; + } + + /** + * Is resizing an event allowed + * + * @param eventResizeAllowed + * True if allowed false if not + */ + public void setEventResizeAllowed(boolean eventResizeAllowed) { + this.eventResizeAllowed = eventResizeAllowed; + } + + /** + * Is moving an event allowed + */ + public boolean isEventMoveAllowed() { + return eventMoveAllowed; + } + + /** + * Is moving an event allowed + * + * @param eventMoveAllowed + * True if moving is allowed, false if not + */ + public void setEventMoveAllowed(boolean eventMoveAllowed) { + this.eventMoveAllowed = eventMoveAllowed; + } + + public boolean isBackwardNavigationEnabled() { + return backwardNavigationEnabled; + } + + public void setBackwardNavigationEnabled(boolean enabled) { + backwardNavigationEnabled = enabled; + } + + public boolean isForwardNavigationEnabled() { + return forwardNavigationEnabled; + } + + public void setForwardNavigationEnabled(boolean enabled) { + forwardNavigationEnabled = enabled; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.dd.VHasDropHandler#getDropHandler() + */ + @Override + public CalendarDropHandler getDropHandler() { + return dropHandler; + } + + /** + * Set the drop handler + * + * @param dropHandler + * The drophandler to use + */ + public void setDropHandler(CalendarDropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /** + * Sets whether the event captions are rendered as HTML. + * <p> + * 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. + * <p> + * 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 + * <p> + * 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/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java new file mode 100644 index 0000000000..44cbcf28f6 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java @@ -0,0 +1,2772 @@ +/* + * Copyright 2000-2016 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.client.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import com.google.gwt.aria.client.Roles; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.event.dom.client.LoadEvent; +import com.google.gwt.event.dom.client.LoadHandler; +import com.google.gwt.event.logical.shared.CloseEvent; +import com.google.gwt.event.logical.shared.CloseHandler; +import com.google.gwt.i18n.client.HasDirection.Direction; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.PopupPanel.PositionCallback; +import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; +import com.google.gwt.user.client.ui.TextBox; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ComputedStyle; +import com.vaadin.client.DeferredWorker; +import com.vaadin.client.Focusable; +import com.vaadin.client.VConsole; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaCaption; +import com.vaadin.client.ui.aria.HandlesAriaInvalid; +import com.vaadin.client.ui.aria.HandlesAriaRequired; +import com.vaadin.client.ui.combobox.ComboBoxConnector; +import com.vaadin.client.ui.menubar.MenuBar; +import com.vaadin.client.ui.menubar.MenuItem; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.ui.ComponentStateUtil; +import com.vaadin.shared.ui.combobox.FilteringMode; +import com.vaadin.shared.util.SharedUtil; + +/** + * Client side implementation of the Select component. + * + * TODO needs major refactoring (to be extensible etc) + */ +@SuppressWarnings("deprecation") +public class VFilterSelect extends Composite + implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, + FocusHandler, BlurHandler, Focusable, SubPartAware, HandlesAriaCaption, + HandlesAriaInvalid, HandlesAriaRequired, DeferredWorker { + + /** + * Represents a suggestion in the suggestion popup box + */ + public class FilterSelectSuggestion implements Suggestion, Command { + + private final String key; + private final String caption; + private String untranslatedIconUri; + private String style; + + /** + * Constructor + * + * @param key + * item key, empty string for a special null item not in + * container + * @param caption + * item caption + * @param style + * item style name, can be empty string + * @param untranslatedIconUri + * icon URI or null + */ + public FilterSelectSuggestion(String key, String caption, String style, + String untranslatedIconUri) { + this.key = key; + this.caption = caption; + this.style = style; + this.untranslatedIconUri = untranslatedIconUri; + } + + /** + * Gets the visible row in the popup as a HTML string. The string + * contains an image tag with the rows icon (if an icon has been + * specified) and the caption of the item + */ + + @Override + public String getDisplayString() { + final StringBuffer sb = new StringBuffer(); + ApplicationConnection client = connector.getConnection(); + final Icon icon = client + .getIcon(client.translateVaadinUri(untranslatedIconUri)); + if (icon != null) { + sb.append(icon.getElement().getString()); + } + String content; + if ("".equals(caption)) { + // Ensure that empty options use the same height as other + // options and are not collapsed (#7506) + content = " "; + } else { + content = WidgetUtil.escapeHTML(caption); + } + sb.append("<span>" + content + "</span>"); + return sb.toString(); + } + + /** + * Get a string that represents this item. This is used in the text box. + */ + + @Override + public String getReplacementString() { + return caption; + } + + /** + * Get the option key which represents the item on the server side. + * + * @return The key of the item + */ + public String getOptionKey() { + return key; + } + + /** + * Get the URI of the icon. Used when constructing the displayed option. + * + * @return + */ + public String getIconUri() { + ApplicationConnection client = connector.getConnection(); + return client.translateVaadinUri(untranslatedIconUri); + } + + /** + * Gets the style set for this suggestion item. Styles are typically set + * by a server-side {@link com.vaadin.ui.ComboBox.ItemStyleGenerator}. + * The returned style is prefixed by <code>v-filterselect-item-</code>. + * + * @since 7.5.6 + * @return the style name to use, or <code>null</code> to not apply any + * custom style. + */ + public String getStyle() { + return style; + } + + /** + * Executes a selection of this item. + */ + + @Override + public void execute() { + onSuggestionSelected(this); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FilterSelectSuggestion)) { + return false; + } + FilterSelectSuggestion other = (FilterSelectSuggestion) obj; + if ((key == null && other.key != null) + || (key != null && !key.equals(other.key))) { + return false; + } + if ((caption == null && other.caption != null) + || (caption != null && !caption.equals(other.caption))) { + return false; + } + if (!SharedUtil.equals(untranslatedIconUri, + other.untranslatedIconUri)) { + return false; + } + if (!SharedUtil.equals(style, other.style)) { + return false; + } + return true; + } + } + + /** An inner class that handles all logic related to mouse wheel. */ + private class MouseWheeler extends JsniMousewheelHandler { + + public MouseWheeler() { + super(VFilterSelect.this); + } + + @Override + protected native JavaScriptObject createMousewheelListenerFunction( + Widget widget) + /*-{ + return $entry(function(e) { + var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX; + var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY; + + // IE8 has only delta y + if (isNaN(deltaY)) { + deltaY = -0.5*e.wheelDelta; + } + + @com.vaadin.client.ui.VFilterSelect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode); + }); + }-*/; + + } + + /** + * A utility class that contains utility methods that are usually called + * from JSNI. + * <p> + * The methods are moved in this class to minimize the amount of JSNI code + * as much as feasible. + */ + static class JsniUtil { + private static final int DOM_DELTA_PIXEL = 0; + private static final int DOM_DELTA_LINE = 1; + private static final int DOM_DELTA_PAGE = 2; + + // Rough estimation of item height + private static final int SCROLL_UNIT_PX = 25; + + private static double deltaSum = 0; + + public static void moveScrollFromEvent(final Widget widget, + final double deltaX, final double deltaY, + final NativeEvent event, final int deltaMode) { + + if (!Double.isNaN(deltaY)) { + VFilterSelect filterSelect = (VFilterSelect) widget; + + switch (deltaMode) { + case DOM_DELTA_LINE: + if (deltaY >= 0) { + filterSelect.suggestionPopup.selectNextItem(); + } else { + filterSelect.suggestionPopup.selectPrevItem(); + } + break; + case DOM_DELTA_PAGE: + if (deltaY >= 0) { + filterSelect.selectNextPage(); + } else { + filterSelect.selectPrevPage(); + } + break; + case DOM_DELTA_PIXEL: + default: + // Accumulate dampened deltas + deltaSum += Math.pow(Math.abs(deltaY), 0.7) + * Math.signum(deltaY); + + // "Scroll" if change exceeds item height + while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) { + if (!filterSelect.dataReceivedHandler + .isWaitingForFilteringResponse()) { + // Move selection if page flip is not in progress + if (deltaSum < 0) { + filterSelect.suggestionPopup.selectPrevItem(); + } else { + filterSelect.suggestionPopup.selectNextItem(); + } + } + deltaSum -= SCROLL_UNIT_PX * Math.signum(deltaSum); + } + break; + } + } + } + } + + /** + * Represents the popup box with the selection options. Wraps a suggestion + * menu. + */ + public class SuggestionPopup extends VOverlay + implements PositionCallback, CloseHandler<PopupPanel> { + + private static final int Z_INDEX = 30000; + + /** For internal use only. May be removed or replaced in the future. */ + public final SuggestionMenu menu; + + private final Element up = DOM.createDiv(); + private final Element down = DOM.createDiv(); + private final Element status = DOM.createDiv(); + + private boolean isPagingEnabled = true; + + private long lastAutoClosed; + + private int popupOuterPadding = -1; + + private int topPosition; + + private final MouseWheeler mouseWheeler = new MouseWheeler(); + + /** + * Default constructor + */ + SuggestionPopup() { + super(true, false); + debug("VFS.SP: constructor()"); + setOwner(VFilterSelect.this); + menu = new SuggestionMenu(); + setWidget(menu); + + getElement().getStyle().setZIndex(Z_INDEX); + + final Element root = getContainerElement(); + + up.setInnerHTML("<span>Prev</span>"); + DOM.sinkEvents(up, Event.ONCLICK); + + down.setInnerHTML("<span>Next</span>"); + DOM.sinkEvents(down, Event.ONCLICK); + + root.insertFirst(up); + root.appendChild(down); + root.appendChild(status); + + DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL); + addCloseHandler(this); + + Roles.getListRole().set(getElement()); + + setPreviewingAllNativeEvents(true); + } + + @Override + protected void onLoad() { + super.onLoad(); + + // Register mousewheel listener on paged select + if (pageLength > 0) { + mouseWheeler.attachMousewheelListener(getElement()); + } + } + + @Override + protected void onUnload() { + mouseWheeler.detachMousewheelListener(getElement()); + super.onUnload(); + } + + /** + * Shows the popup where the user can see the filtered options + * + * @param currentSuggestions + * The filtered suggestions + * @param currentPage + * The current page number + * @param totalSuggestions + * The total amount of suggestions + */ + public void showSuggestions( + final Collection<FilterSelectSuggestion> currentSuggestions, + final int currentPage, final int totalSuggestions) { + + debug("VFS.SP: showSuggestions(" + currentSuggestions + ", " + + currentPage + ", " + totalSuggestions + ")"); + + /* + * We need to defer the opening of the popup so that the parent DOM + * has stabilized so we can calculate an absolute top and left + * correctly. This issue manifests when a Combobox is placed in + * another popupView which also needs to calculate the absoluteTop() + * to position itself. #9768 + * + * After deferring the showSuggestions method, a problem with + * navigating in the combo box occurs. Because of that the method + * navigateItemAfterPageChange in ComboBoxConnector class, which + * navigates to the exact item after page was changed also was + * marked as deferred. #11333 + */ + final SuggestionPopup popup = this; + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + // Add TT anchor point + getElement().setId("VAADIN_COMBOBOX_OPTIONLIST"); + + menu.setSuggestions(currentSuggestions); + final int x = VFilterSelect.this.getAbsoluteLeft(); + + topPosition = tb.getAbsoluteTop(); + topPosition += tb.getOffsetHeight(); + + setPopupPosition(x, topPosition); + + int nullOffset = (nullSelectionAllowed + && "".equals(lastFilter) ? 1 : 0); + boolean firstPage = (currentPage == 0); + final int first = currentPage * pageLength + 1 + - (firstPage ? 0 : nullOffset); + final int last = first + currentSuggestions.size() - 1 + - (firstPage && "".equals(lastFilter) ? nullOffset + : 0); + final int matches = totalSuggestions - nullOffset; + if (last > 0) { + // nullsel not counted, as requested by user + status.setInnerText((matches == 0 ? 0 : first) + "-" + + last + "/" + matches); + } else { + status.setInnerText(""); + } + // We don't need to show arrows or statusbar if there is + // only one page + if (totalSuggestions <= pageLength || pageLength == 0) { + setPagingEnabled(false); + } else { + setPagingEnabled(true); + } + setPrevButtonActive(first > 1); + setNextButtonActive(last < matches); + + // clear previously fixed width + menu.setWidth(""); + menu.getElement().getFirstChildElement().getStyle() + .clearWidth(); + + setPopupPositionAndShow(popup); + } + }); + } + + /** + * Should the next page button be visible to the user? + * + * @param active + */ + private void setNextButtonActive(boolean active) { + if (enableDebug) { + debug("VFS.SP: setNextButtonActive(" + active + ")"); + } + if (active) { + DOM.sinkEvents(down, Event.ONCLICK); + down.setClassName( + VFilterSelect.this.getStylePrimaryName() + "-nextpage"); + } else { + DOM.sinkEvents(down, 0); + down.setClassName(VFilterSelect.this.getStylePrimaryName() + + "-nextpage-off"); + } + } + + /** + * Should the previous page button be visible to the user + * + * @param active + */ + private void setPrevButtonActive(boolean active) { + if (enableDebug) { + debug("VFS.SP: setPrevButtonActive(" + active + ")"); + } + + if (active) { + DOM.sinkEvents(up, Event.ONCLICK); + up.setClassName( + VFilterSelect.this.getStylePrimaryName() + "-prevpage"); + } else { + DOM.sinkEvents(up, 0); + up.setClassName(VFilterSelect.this.getStylePrimaryName() + + "-prevpage-off"); + } + + } + + /** + * Selects the next item in the filtered selections + */ + public void selectNextItem() { + debug("VFS.SP: selectNextItem()"); + + final int index = menu.getSelectedIndex() + 1; + if (menu.getItems().size() > index) { + selectItem(menu.getItems().get(index)); + + } else { + selectNextPage(); + } + } + + /** + * Selects the previous item in the filtered selections + */ + public void selectPrevItem() { + debug("VFS.SP: selectPrevItem()"); + + final int index = menu.getSelectedIndex() - 1; + if (index > -1) { + selectItem(menu.getItems().get(index)); + + } else if (index == -1) { + selectPrevPage(); + + } else { + if (!menu.getItems().isEmpty()) { + selectLastItem(); + } + } + } + + /** + * Select the first item of the suggestions list popup. + * + * @since 7.2.6 + */ + public void selectFirstItem() { + debug("VFS.SP: selectFirstItem()"); + selectItem(menu.getFirstItem()); + } + + /** + * Select the last item of the suggestions list popup. + * + * @since 7.2.6 + */ + public void selectLastItem() { + debug("VFS.SP: selectLastItem()"); + selectItem(menu.getLastItem()); + } + + /* + * Sets the selected item in the popup menu. + */ + private void selectItem(final MenuItem newSelectedItem) { + menu.selectItem(newSelectedItem); + + // Set the icon. + FilterSelectSuggestion suggestion = (FilterSelectSuggestion) newSelectedItem + .getCommand(); + setSelectedItemIcon(suggestion.getIconUri()); + + // Set the text. + setText(suggestion.getReplacementString()); + + } + + /* + * Using a timer to scroll up or down the pages so when we receive lots + * of consecutive mouse wheel events the pages does not flicker. + */ + private LazyPageScroller lazyPageScroller = new LazyPageScroller(); + + private class LazyPageScroller extends Timer { + private int pagesToScroll = 0; + + @Override + public void run() { + debug("VFS.SP.LPS: run()"); + if (pagesToScroll != 0) { + if (!dataReceivedHandler.isWaitingForFilteringResponse()) { + /* + * Avoid scrolling while we are waiting for a response + * because otherwise the waiting flag will be reset in + * the first response and the second response will be + * ignored, causing an empty popup... + * + * As long as the scrolling delay is suitable + * double/triple clicks will work by scrolling two or + * three pages at a time and this should not be a + * problem. + */ + filterOptions(currentPage + pagesToScroll, lastFilter); + } + pagesToScroll = 0; + } + } + + public void scrollUp() { + debug("VFS.SP.LPS: scrollUp()"); + if (pageLength > 0 && currentPage + pagesToScroll > 0) { + pagesToScroll--; + cancel(); + schedule(200); + } + } + + public void scrollDown() { + debug("VFS.SP.LPS: scrollDown()"); + if (pageLength > 0 + && totalMatches > (currentPage + pagesToScroll + 1) + * pageLength) { + pagesToScroll++; + cancel(); + schedule(200); + } + } + } + + private void scroll(double deltaY) { + boolean scrollActive = menu.isScrollActive(); + + debug("VFS.SP: scroll() scrollActive: " + scrollActive); + + if (!scrollActive) { + if (deltaY > 0d) { + lazyPageScroller.scrollDown(); + } else { + lazyPageScroller.scrollUp(); + } + } + } + + @Override + public void onBrowserEvent(Event event) { + debug("VFS.SP: onBrowserEvent()"); + + if (event.getTypeInt() == Event.ONCLICK) { + final Element target = DOM.eventGetTarget(event); + if (target == up || target == DOM.getChild(up, 0)) { + lazyPageScroller.scrollUp(); + } else if (target == down || target == DOM.getChild(down, 0)) { + lazyPageScroller.scrollDown(); + } + + } + + /* + * Prevent the keyboard focus from leaving the textfield by + * preventing the default behaviour of the browser. Fixes #4285. + */ + handleMouseDownEvent(event); + } + + /** + * Should paging be enabled. If paging is enabled then only a certain + * amount of items are visible at a time and a scrollbar or buttons are + * visible to change page. If paging is turned of then all options are + * rendered into the popup menu. + * + * @param paging + * Should the paging be turned on? + */ + public void setPagingEnabled(boolean paging) { + debug("VFS.SP: setPagingEnabled(" + paging + ")"); + if (isPagingEnabled == paging) { + return; + } + if (paging) { + down.getStyle().clearDisplay(); + up.getStyle().clearDisplay(); + status.getStyle().clearDisplay(); + } else { + down.getStyle().setDisplay(Display.NONE); + up.getStyle().setDisplay(Display.NONE); + status.getStyle().setDisplay(Display.NONE); + } + isPagingEnabled = paging; + } + + @Override + public void setPosition(int offsetWidth, int offsetHeight) { + debug("VFS.SP: setPosition(" + offsetWidth + ", " + offsetHeight + + ")"); + + int top = topPosition; + int left = getPopupLeft(); + + // reset menu size and retrieve its "natural" size + menu.setHeight(""); + if (currentPage > 0 && !hasNextPage()) { + // fix height to avoid height change when getting to last page + menu.fixHeightTo(pageLength); + } + + final int desiredHeight = offsetHeight = getOffsetHeight(); + final int desiredWidth = getMainWidth(); + + debug("VFS.SP: desired[" + desiredWidth + ", " + desiredHeight + + "]"); + + Element menuFirstChild = menu.getElement().getFirstChildElement(); + int naturalMenuWidth; + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 10) { + // On IE 8 & 9 visibility is set to hidden and measuring + // elements while they are hidden yields incorrect results + String before = menu.getElement().getParentElement().getStyle() + .getVisibility(); + menu.getElement().getParentElement().getStyle() + .setVisibility(Visibility.VISIBLE); + naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild); + menu.getElement().getParentElement().getStyle() + .setProperty("visibility", before); + } else { + naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild); + } + + if (popupOuterPadding == -1) { + popupOuterPadding = WidgetUtil + .measureHorizontalPaddingAndBorder(menu.getElement(), 2) + + WidgetUtil.measureHorizontalPaddingAndBorder( + suggestionPopup.getElement(), 0); + } + + updateMenuWidth(desiredWidth, naturalMenuWidth); + + if (BrowserInfo.get().isIE() + && BrowserInfo.get().getBrowserMajorVersion() < 11) { + // Must take margin,border,padding manually into account for + // menu element as we measure the element child and set width to + // the element parent + + double naturalMenuOuterWidth; + if (BrowserInfo.get().getBrowserMajorVersion() < 10) { + // On IE 8 & 9 visibility is set to hidden and measuring + // elements while they are hidden yields incorrect results + String before = menu.getElement().getParentElement() + .getStyle().getVisibility(); + menu.getElement().getParentElement().getStyle() + .setVisibility(Visibility.VISIBLE); + naturalMenuOuterWidth = WidgetUtil + .getRequiredWidthDouble(menuFirstChild) + + getMarginBorderPaddingWidth(menu.getElement()); + menu.getElement().getParentElement().getStyle() + .setProperty("visibility", before); + } else { + naturalMenuOuterWidth = WidgetUtil + .getRequiredWidthDouble(menuFirstChild) + + getMarginBorderPaddingWidth(menu.getElement()); + } + + /* + * IE requires us to specify the width for the container + * element. Otherwise it will be 100% wide + */ + double rootWidth = Math.max(desiredWidth - popupOuterPadding, + naturalMenuOuterWidth); + getContainerElement().getStyle().setWidth(rootWidth, Unit.PX); + } + + final int textInputHeight = VFilterSelect.this.getOffsetHeight(); + final int textInputTopOnPage = tb.getAbsoluteTop(); + final int viewportOffset = Document.get().getScrollTop(); + final int textInputTopInViewport = textInputTopOnPage + - viewportOffset; + final int textInputBottomInViewport = textInputTopInViewport + + textInputHeight; + + final int spaceAboveInViewport = textInputTopInViewport; + final int spaceBelowInViewport = Window.getClientHeight() + - textInputBottomInViewport; + + if (spaceBelowInViewport < offsetHeight + && spaceBelowInViewport < spaceAboveInViewport) { + // popup on top of input instead + if (offsetHeight > spaceAboveInViewport) { + // Shrink popup height to fit above + offsetHeight = spaceAboveInViewport; + } + top = textInputTopOnPage - offsetHeight; + } else { + // Show below, position calculated in showSuggestions for some + // strange reason + top = topPosition; + offsetHeight = Math.min(offsetHeight, spaceBelowInViewport); + } + + // fetch real width (mac FF bugs here due GWT popups overflow:auto ) + offsetWidth = menuFirstChild.getOffsetWidth(); + + if (offsetHeight < desiredHeight) { + int menuHeight = offsetHeight; + if (isPagingEnabled) { + menuHeight -= up.getOffsetHeight() + down.getOffsetHeight() + + status.getOffsetHeight(); + } else { + final ComputedStyle s = new ComputedStyle( + menu.getElement()); + menuHeight -= s.getIntProperty("marginBottom") + + s.getIntProperty("marginTop"); + } + + // If the available page height is really tiny then this will be + // negative and an exception will be thrown on setHeight. + int menuElementHeight = menu.getItemOffsetHeight(); + if (menuHeight < menuElementHeight) { + menuHeight = menuElementHeight; + } + + menu.setHeight(menuHeight + "px"); + + if (suggestionPopupWidth == null) { + final int naturalMenuWidthPlusScrollBar = naturalMenuWidth + + WidgetUtil.getNativeScrollbarSize(); + if (offsetWidth < naturalMenuWidthPlusScrollBar) { + menu.setWidth(naturalMenuWidthPlusScrollBar + "px"); + } + } + } + + if (offsetWidth + left > Window.getClientWidth()) { + left = VFilterSelect.this.getAbsoluteLeft() + + VFilterSelect.this.getOffsetWidth() - offsetWidth; + if (left < 0) { + left = 0; + menu.setWidth(Window.getClientWidth() + "px"); + + } + } + + setPopupPosition(left, top); + menu.scrollSelectionIntoView(); + } + + /** + * Adds in-line CSS rules to the DOM according to the + * suggestionPopupWidth field + * + * @param desiredWidth + * @param naturalMenuWidth + */ + private void updateMenuWidth(final int desiredWidth, + int naturalMenuWidth) { + /** + * Three different width modes for the suggestion pop-up: + * + * 1. Legacy "null"-mode: width is determined by the longest item + * caption for each page while still maintaining minimum width of + * (desiredWidth - popupOuterPadding) + * + * 2. relative to the component itself + * + * 3. fixed width + */ + String width = "auto"; + if (suggestionPopupWidth == null) { + if (naturalMenuWidth < desiredWidth) { + naturalMenuWidth = desiredWidth - popupOuterPadding; + width = (desiredWidth - popupOuterPadding) + "px"; + } + } else if (isrelativeUnits(suggestionPopupWidth)) { + float mainComponentWidth = desiredWidth - popupOuterPadding; + // convert percentage value to fraction + int widthInPx = Math.round( + mainComponentWidth * asFraction(suggestionPopupWidth)); + width = widthInPx + "px"; + } else { + // use as fixed width CSS definition + width = WidgetUtil.escapeAttribute(suggestionPopupWidth); + } + menu.setWidth(width); + } + + /** + * Returns the percentage value as a fraction, e.g. 42% -> 0.42 + * + * @param percentage + */ + private float asFraction(String percentage) { + String trimmed = percentage.trim(); + String withoutPercentSign = trimmed.substring(0, + trimmed.length() - 1); + float asFraction = Float.parseFloat(withoutPercentSign) / 100; + return asFraction; + } + + /** + * @since 7.7 + * @param suggestionPopupWidth + * @return + */ + private boolean isrelativeUnits(String suggestionPopupWidth) { + return suggestionPopupWidth.trim().endsWith("%"); + } + + /** + * Was the popup just closed? + * + * @return true if popup was just closed + */ + public boolean isJustClosed() { + debug("VFS.SP: justClosed()"); + final long now = (new Date()).getTime(); + return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google + * .gwt.event.logical.shared.CloseEvent) + */ + + @Override + public void onClose(CloseEvent<PopupPanel> event) { + if (enableDebug) { + debug("VFS.SP: onClose(" + event.isAutoClosed() + ")"); + } + if (event.isAutoClosed()) { + lastAutoClosed = (new Date()).getTime(); + } + } + + /** + * Updates style names in suggestion popup to help theme building. + * + * @param componentState + * shared state of the combo box + */ + public void updateStyleNames(AbstractComponentState componentState) { + debug("VFS.SP: updateStyleNames()"); + setStyleName( + VFilterSelect.this.getStylePrimaryName() + "-suggestpopup"); + menu.setStyleName( + VFilterSelect.this.getStylePrimaryName() + "-suggestmenu"); + status.setClassName( + VFilterSelect.this.getStylePrimaryName() + "-status"); + if (ComponentStateUtil.hasStyles(componentState)) { + for (String style : componentState.styles) { + if (!"".equals(style)) { + addStyleDependentName(style); + } + } + } + } + + } + + /** + * The menu where the suggestions are rendered + */ + public class SuggestionMenu extends MenuBar + implements SubPartAware, LoadHandler { + + private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor( + 100, new ScheduledCommand() { + + @Override + public void execute() { + debug("VFS.SM: delayedImageLoadExecutioner()"); + if (suggestionPopup.isVisible() + && suggestionPopup.isAttached()) { + setWidth(""); + getElement().getFirstChildElement().getStyle() + .clearWidth(); + suggestionPopup + .setPopupPositionAndShow(suggestionPopup); + } + + } + }); + + /** + * Default constructor + */ + SuggestionMenu() { + super(true); + debug("VFS.SM: constructor()"); + addDomHandler(this, LoadEvent.getType()); + + setScrollEnabled(true); + } + + /** + * Fixes menus height to use same space as full page would use. Needed + * to avoid height changes when quickly "scrolling" to last page. + */ + public void fixHeightTo(int pageItemsCount) { + setHeight(getPreferredHeight(pageItemsCount)); + } + + /* + * Gets the preferred height of the menu including pageItemsCount items. + */ + String getPreferredHeight(int pageItemsCount) { + if (currentSuggestions.size() > 0) { + final int pixels = (getPreferredHeight() + / currentSuggestions.size()) * pageItemsCount; + return pixels + "px"; + } else { + return ""; + } + } + + /** + * Sets the suggestions rendered in the menu + * + * @param suggestions + * The suggestions to be rendered in the menu + */ + public void setSuggestions( + Collection<FilterSelectSuggestion> suggestions) { + if (enableDebug) { + debug("VFS.SM: setSuggestions(" + suggestions + ")"); + } + + clearItems(); + final Iterator<FilterSelectSuggestion> it = suggestions.iterator(); + boolean isFirstIteration = true; + while (it.hasNext()) { + final FilterSelectSuggestion s = it.next(); + final MenuItem mi = new MenuItem(s.getDisplayString(), true, s); + String style = s.getStyle(); + if (style != null) { + mi.addStyleName("v-filterselect-item-" + style); + } + Roles.getListitemRole().set(mi.getElement()); + + WidgetUtil.sinkOnloadForImages(mi.getElement()); + + this.addItem(mi); + + // By default, first item on the list is always highlighted, + // unless adding new items is allowed. + if (isFirstIteration && !allowNewItem) { + selectItem(mi); + } + + // If the filter matches the current selection, highlight that + // instead of the first item. + if (tb.getText().equals(s.getReplacementString()) + && s == currentSuggestion) { + selectItem(mi); + } + + isFirstIteration = false; + } + } + + /** + * Send the current selection to the server. Triggered when a selection + * is made with the ENTER key. + */ + public void doSelectedItemAction() { + debug("VFS.SM: doSelectedItemAction()"); + // do not send a value change event if null was and stays selected + final String enteredItemValue = tb.getText(); + if (nullSelectionAllowed && "".equals(enteredItemValue) + && selectedOptionKey != null + && !"".equals(selectedOptionKey)) { + if (nullSelectItem) { + reset(); + return; + } + // null is not visible on pages != 0, and not visible when + // filtering: handle separately + connector.requestFirstPage(); + + suggestionPopup.hide(); + return; + } + + dataReceivedHandler.doPostFilterWhenReady(); + } + + /** + * Triggered after a selection has been made. + */ + public void doPostFilterSelectedItemAction() { + debug("VFS.SM: doPostFilterSelectedItemAction()"); + final MenuItem item = getSelectedItem(); + final String enteredItemValue = tb.getText(); + + // check for exact match in menu + int p = getItems().size(); + if (p > 0) { + for (int i = 0; i < p; i++) { + final MenuItem potentialExactMatch = getItems().get(i); + if (potentialExactMatch.getText() + .equals(enteredItemValue)) { + selectItem(potentialExactMatch); + // do not send a value change event if null was and + // stays selected + if (!"".equals(enteredItemValue) + || (selectedOptionKey != null + && !"".equals(selectedOptionKey))) { + doItemAction(potentialExactMatch, true); + } + suggestionPopup.hide(); + return; + } + } + } + if (allowNewItem) { + + if (!prompting && !enteredItemValue.equals(lastNewItemString)) { + /* + * Store last sent new item string to avoid double sends + */ + lastNewItemString = enteredItemValue; + connector.sendNewItem(enteredItemValue); + } + } else if (item != null && !"".equals(lastFilter) + && (filteringmode == FilteringMode.CONTAINS + ? item.getText().toLowerCase() + .contains(lastFilter.toLowerCase()) + : item.getText().toLowerCase() + .startsWith(lastFilter.toLowerCase()))) { + doItemAction(item, true); + } else { + // currentSuggestion has key="" for nullselection + if (currentSuggestion != null + && !currentSuggestion.key.equals("")) { + // An item (not null) selected + String text = currentSuggestion.getReplacementString(); + setText(text); + selectedOptionKey = currentSuggestion.key; + } else { + // Null selected + setText(""); + selectedOptionKey = null; + } + } + suggestionPopup.hide(); + } + + private static final String SUBPART_PREFIX = "item"; + + @Override + public com.google.gwt.user.client.Element getSubPartElement( + String subPart) { + int index = Integer + .parseInt(subPart.substring(SUBPART_PREFIX.length())); + + MenuItem item = getItems().get(index); + + return item.getElement(); + } + + @Override + public String getSubPartName( + com.google.gwt.user.client.Element subElement) { + if (!getElement().isOrHasChild(subElement)) { + return null; + } + + Element menuItemRoot = subElement; + while (menuItemRoot != null + && !menuItemRoot.getTagName().equalsIgnoreCase("td")) { + menuItemRoot = menuItemRoot.getParentElement().cast(); + } + // "menuItemRoot" is now the root of the menu item + + final int itemCount = getItems().size(); + for (int i = 0; i < itemCount; i++) { + if (getItems().get(i).getElement() == menuItemRoot) { + String name = SUBPART_PREFIX + i; + return name; + } + } + return null; + } + + @Override + public void onLoad(LoadEvent event) { + debug("VFS.SM: onLoad()"); + // Handle icon onload events to ensure shadow is resized + // correctly + delayedImageLoadExecutioner.trigger(); + + } + + /** + * @deprecated use {@link SuggestionPopup#selectFirstItem()} instead. + */ + @Deprecated + public void selectFirstItem() { + debug("VFS.SM: selectFirstItem()"); + MenuItem firstItem = getItems().get(0); + selectItem(firstItem); + } + + /** + * @deprecated use {@link SuggestionPopup#selectLastItem()} instead. + */ + @Deprecated + public void selectLastItem() { + debug("VFS.SM: selectLastItem()"); + List<MenuItem> items = getItems(); + MenuItem lastItem = items.get(items.size() - 1); + selectItem(lastItem); + } + + /* + * Gets the height of one menu item. + */ + int getItemOffsetHeight() { + List<MenuItem> items = getItems(); + return items != null && items.size() > 0 + ? items.get(0).getOffsetHeight() : 0; + } + + /* + * Gets the width of one menu item. + */ + int getItemOffsetWidth() { + List<MenuItem> items = getItems(); + return items != null && items.size() > 0 + ? items.get(0).getOffsetWidth() : 0; + } + + /** + * Returns true if the scroll is active on the menu element or if the + * menu currently displays the last page with less items then the + * maximum visibility (in which case the scroll is not active, but the + * scroll is active for any other page in general). + * + * @since 7.2.6 + */ + @Override + public boolean isScrollActive() { + String height = getElement().getStyle().getHeight(); + String preferredHeight = getPreferredHeight(pageLength); + + return !(height == null || height.length() == 0 + || height.equals(preferredHeight)); + } + + } + + /** + * TextBox variant used as input element for filter selects, which prevents + * selecting text when disabled. + * + * @since 7.1.5 + */ + public class FilterSelectTextBox extends TextBox { + + /** + * Creates a new filter select text box. + * + * @since 7.6.4 + */ + public FilterSelectTextBox() { + /*- + * Stop the browser from showing its own suggestion popup. + * + * Using an invalid value instead of "off" as suggested by + * https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion + * + * Leaving the non-standard Safari options autocapitalize and + * autocorrect untouched since those do not interfere in the same + * way, and they might be useful in a combo box where new items are + * allowed. + */ + getElement().setAttribute("autocomplete", "nope"); + } + + /** + * Overridden to avoid selecting text when text input is disabled + */ + @Override + public void setSelectionRange(int pos, int length) { + if (textInputEnabled) { + /* + * set selection range with a backwards direction: anchor at the + * back, focus at the front. This means that items that are too + * long to display will display from the start and not the end + * even on Firefox. + * + * We need the JSNI function to set selection range so that we + * can use the optional direction attribute to set the anchor to + * the end and the focus to the start. This makes Firefox work + * the same way as other browsers (#13477) + */ + WidgetUtil.setSelectionRange(getElement(), pos, length, + "backward"); + + } else { + /* + * Setting the selectionrange for an uneditable textbox leads to + * unwanted behaviour when the width of the textbox is narrower + * than the width of the entry: the end of the entry is shown + * instead of the beginning. (see #13477) + * + * To avoid this, we set the caret to the beginning of the line. + */ + + super.setSelectionRange(0, 0); + } + } + + } + + /** + * Handler receiving notifications from the connector and updating the + * widget state accordingly. + * + * This class is still subject to change and should not be considered as + * public stable API. + * + * @since + */ + public class DataReceivedHandler { + + private Runnable navigationCallback = null; + /** + * Set true when popupopened has been clicked. Cleared on each + * UIDL-update. This handles the special case where are not filtering + * yet and the selected value has changed on the server-side. See #2119 + * <p> + * For internal use only. May be removed or replaced in the future. + */ + private boolean popupOpenerClicked = false; + private boolean performPostFilteringOnDataReceived = false; + /** For internal use only. May be removed or replaced in the future. */ + private boolean waitingForFilteringResponse = false; + + /** + * Called by the connector when new data for the last requested filter + * is received from the server. + */ + public void dataReceived() { + suggestionPopup.showSuggestions(currentSuggestions, currentPage, + totalMatches); + + waitingForFilteringResponse = false; + + if (!popupOpenerClicked) { + navigateItemAfterPageChange(); + } + + if (performPostFilteringOnDataReceived) { + performPostFilteringOnDataReceived = false; + suggestionPopup.menu.doPostFilterSelectedItemAction(); + } + + popupOpenerClicked = false; + } + + /* + * This method navigates to the proper item in the combobox page. This + * should be executed after setSuggestions() method which is called from + * vFilterSelect.showSuggestions(). ShowSuggestions() method builds the + * page content. As far as setSuggestions() method is called as + * deferred, navigateItemAfterPageChange method should be also be called + * as deferred. #11333 + */ + private void navigateItemAfterPageChange() { + if (navigationCallback != null) { + // pageChangeCallback is not reset here but after any server + // request in case you are in between two requests both changing + // the page back and forth + + // we're paging w/ arrows + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + if (navigationCallback != null) { + navigationCallback.run(); + } + } + }); + } + } + + /** + * Called by the connector when any request has been sent to the server, + * before waiting for a reply. + */ + public void anyRequestSentToServer() { + navigationCallback = null; + } + + /** + * Set a callback that is invoked when a page change occurs if there + * have not been intervening requests to the server. The callback is + * reset when any additional request is made to the server. + * + * @param callback + */ + public void setNavigationCallback(Runnable callback) { + navigationCallback = callback; + } + + /** + * Record that the popup opener has been clicked and the popup should be + * opened on the next request. + * + * This handles the special case where are not filtering yet and the + * selected value has changed on the server-side. See #2119. The flag is + * cleared on each UIDL reply. + */ + public void popupOpenerClicked() { + popupOpenerClicked = true; + } + + /** + * Cancel a pending request to perform post-filtering actions. + */ + private void cancelPendingPostFiltering() { + performPostFilteringOnDataReceived = false; + } + + /** + * Called by the connector when it has finished handling any reply from + * the server, regardless of what was updated. + */ + public void serverReplyHandled() { + popupOpenerClicked = false; + } + + /** + * For internal use only - this method will be removed in the future. + * + * @return true if the combo box is waiting for a reply from the server + * with a new page of data, false otherwise + */ + public boolean isWaitingForFilteringResponse() { + return waitingForFilteringResponse; + } + + /** + * Set a flag that filtering of options is pending a response from the + * server. + */ + private void startWaitingForFilteringResponse() { + waitingForFilteringResponse = true; + } + + /** + * Perform the post-filter action either now (if not waiting for a + * server response) or when a response is received. + */ + private void doPostFilterWhenReady() { + if (isWaitingForFilteringResponse()) { + performPostFilteringOnDataReceived = true; + } else { + performPostFilteringOnDataReceived = false; + suggestionPopup.menu.doPostFilterSelectedItemAction(); + } + } + + /** + * Perform selection (if appropriate) based on a reply from the server. + * When this method is called, the suggestions have been reset if new + * ones (different from the previous list) were received from the + * server. + * + * @param selectedKey + * new selected key or null if none given by the server + * @param selectedCaption + * new selected item caption if sent by the server or null - + * this is used when the selected item is not on the current + * page + * @param oldSuggestionTextMatchTheOldSelection + * true if the old filtering text box contents matched + * exactly the old selection + */ + public void updateSelectionFromServer(String selectedKey, + String selectedCaption, + boolean oldSuggestionTextMatchTheOldSelection) { + // when filtering with empty filter, server sets the selected key + // to "", which we don't select here. Otherwise we won't be able to + // reset back to the item that was selected before filtering + // started. + if (selectedKey != null && !selectedKey.equals("")) { + performSelection(selectedKey, + oldSuggestionTextMatchTheOldSelection, + !isWaitingForFilteringResponse() || popupOpenerClicked); + setSelectedCaption(null); + } else if (!isWaitingForFilteringResponse() + && selectedCaption != null) { + // scrolling to correct page is disabled, caption is passed as a + // special parameter + setSelectedCaption(selectedCaption); + } else { + if (!isWaitingForFilteringResponse() || popupOpenerClicked) { + resetSelection(popupOpenerClicked); + } + } + } + + } + + @Deprecated + public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF; + @Deprecated + public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH; + @Deprecated + public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS; + + public static final String CLASSNAME = "v-filterselect"; + private static final String STYLE_NO_INPUT = "no-input"; + + /** For internal use only. May be removed or replaced in the future. */ + public int pageLength = 10; + + private boolean enableDebug = false; + + private final FlowPanel panel = new FlowPanel(); + + /** + * The text box where the filter is written + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public final TextBox tb; + + /** For internal use only. May be removed or replaced in the future. */ + public final SuggestionPopup suggestionPopup; + + /** + * Used when measuring the width of the popup + */ + private final HTML popupOpener = new HTML("") { + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + /* + * Prevent the keyboard focus from leaving the textfield by + * preventing the default behaviour of the browser. Fixes #4285. + */ + handleMouseDownEvent(event); + } + }; + + private class IconWidget extends Widget { + IconWidget(Icon icon) { + setElement(icon.getElement()); + addDomHandler(VFilterSelect.this, ClickEvent.getType()); + } + } + + private IconWidget selectedItemIcon; + + /** For internal use only. May be removed or replaced in the future. */ + public ComboBoxConnector connector; + + /** For internal use only. May be removed or replaced in the future. */ + public int currentPage; + + /** + * A collection of available suggestions (options) as received from the + * server. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>(); + + /** For internal use only. May be removed or replaced in the future. */ + public String selectedOptionKey; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean initDone = false; + + /** For internal use only. May be removed or replaced in the future. */ + public String lastFilter = ""; + + /** + * The current suggestion selected from the dropdown. This is one of the + * values in currentSuggestions except when filtering, in this case + * currentSuggestion might not be in currentSuggestions. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public FilterSelectSuggestion currentSuggestion; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean allowNewItem; + + /** For internal use only. May be removed or replaced in the future. */ + public int totalMatches; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean nullSelectionAllowed; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean nullSelectItem; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean enabled; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean readonly; + + /** For internal use only. May be removed or replaced in the future. */ + public FilteringMode filteringmode = FilteringMode.OFF; + + // shown in unfocused empty field, disappears on focus (e.g "Search here") + private static final String CLASSNAME_PROMPT = "prompt"; + + /** For internal use only. May be removed or replaced in the future. */ + public String inputPrompt = ""; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean prompting = false; + + /** For internal use only. May be removed or replaced in the future. */ + public int suggestionPopupMinWidth = 0; + + public String suggestionPopupWidth = null; + + private int popupWidth = -1; + /** + * Stores the last new item string to avoid double submissions. Cleared on + * uidl updates. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public String lastNewItemString; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean focused = false; + + /** + * If set to false, the component should not allow entering text to the + * field even for filtering. + */ + private boolean textInputEnabled = true; + + private final DataReceivedHandler dataReceivedHandler = new DataReceivedHandler(); + + /** + * Default constructor. + */ + public VFilterSelect() { + tb = createTextBox(); + suggestionPopup = createSuggestionPopup(); + + popupOpener.sinkEvents(Event.ONMOUSEDOWN); + Roles.getButtonRole().setAriaHiddenState(popupOpener.getElement(), + true); + Roles.getButtonRole().set(popupOpener.getElement()); + + panel.add(tb); + panel.add(popupOpener); + initWidget(panel); + Roles.getComboboxRole().set(panel.getElement()); + + tb.addKeyDownHandler(this); + tb.addKeyUpHandler(this); + + tb.addFocusHandler(this); + tb.addBlurHandler(this); + tb.addClickHandler(this); + + popupOpener.addClickHandler(this); + + setStyleName(CLASSNAME); + + sinkEvents(Event.ONPASTE); + } + + private static double getMarginBorderPaddingWidth(Element element) { + final ComputedStyle s = new ComputedStyle(element); + return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth(); + + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + + if (event.getTypeInt() == Event.ONPASTE) { + if (textInputEnabled) { + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + + @Override + public void execute() { + filterOptions(currentPage); + } + }); + } + } + } + + /** + * This method will create the TextBox used by the VFilterSelect instance. + * It is invoked during the Constructor and should only be overridden if a + * custom TextBox shall be used. The overriding method cannot use any + * instance variables. + * + * @since 7.1.5 + * @return TextBox instance used by this VFilterSelect + */ + protected TextBox createTextBox() { + return new FilterSelectTextBox(); + } + + /** + * This method will create the SuggestionPopup used by the VFilterSelect + * instance. It is invoked during the Constructor and should only be + * overridden if a custom SuggestionPopup shall be used. The overriding + * method cannot use any instance variables. + * + * @since 7.1.5 + * @return SuggestionPopup instance used by this VFilterSelect + */ + protected SuggestionPopup createSuggestionPopup() { + return new SuggestionPopup(); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style); + updateStyleNames(); + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + updateStyleNames(); + } + + protected void updateStyleNames() { + tb.setStyleName(getStylePrimaryName() + "-input"); + popupOpener.setStyleName(getStylePrimaryName() + "-button"); + suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup"); + } + + /** + * Does the Select have more pages? + * + * @return true if a next page exists, else false if the current page is the + * last page + */ + public boolean hasNextPage() { + if (pageLength > 0 && totalMatches > (currentPage + 1) * pageLength) { + return true; + } else { + return false; + } + } + + /** + * Filters the options at a certain page. Uses the text box input as a + * filter + * + * @param page + * The page which items are to be filtered + */ + public void filterOptions(int page) { + filterOptions(page, tb.getText()); + } + + /** + * Filters the options at certain page using the given filter + * + * @param page + * The page to filter + * @param filter + * The filter to apply to the components + */ + public void filterOptions(int page, String filter) { + debug("VFS: filterOptions(" + page + ", " + filter + ")"); + + if (filter.equals(lastFilter) && currentPage == page) { + if (!suggestionPopup.isAttached()) { + suggestionPopup.showSuggestions(currentSuggestions, currentPage, + totalMatches); + } + return; + } + if (!filter.equals(lastFilter)) { + // when filtering, let the server decide the page unless we've + // set the filter to empty and explicitly said that we want to see + // the results starting from page 0. + if ("".equals(filter) && page != 0) { + // let server decide + page = -1; + } else { + page = 0; + } + } + + dataReceivedHandler.startWaitingForFilteringResponse(); + connector.requestPage(filter, page); + + lastFilter = filter; + currentPage = page; + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateReadOnly() { + debug("VFS: updateReadOnly()"); + tb.setReadOnly(readonly || !textInputEnabled); + } + + public void setTextInputEnabled(boolean textInputEnabled) { + debug("VFS: setTextInputEnabled()"); + // Always update styles as they might have been overwritten + if (textInputEnabled) { + removeStyleDependentName(STYLE_NO_INPUT); + Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement()); + } else { + addStyleDependentName(STYLE_NO_INPUT); + Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(), + true); + } + + if (this.textInputEnabled == textInputEnabled) { + return; + } + + this.textInputEnabled = textInputEnabled; + updateReadOnly(); + } + + /** + * Sets the text in the text box. + * + * @param text + * the text to set in the text box + */ + public void setTextboxText(final String text) { + if (enableDebug) { + debug("VFS: setTextboxText(" + text + ")"); + } + setText(text); + } + + private void setText(final String text) { + /** + * To leave caret in the beginning of the line. SetSelectionRange + * wouldn't work on IE (see #13477) + */ + Direction previousDirection = tb.getDirection(); + tb.setDirection(Direction.RTL); + tb.setText(text); + tb.setDirection(previousDirection); + } + + /** + * Turns prompting on. When prompting is turned on a command prompt is shown + * in the text box if nothing has been entered. + */ + public void setPromptingOn() { + debug("VFS: setPromptingOn()"); + if (!prompting) { + prompting = true; + addStyleDependentName(CLASSNAME_PROMPT); + } + setTextboxText(inputPrompt); + } + + /** + * Turns prompting off. When prompting is turned on a command prompt is + * shown in the text box if nothing has been entered. + * <p> + * For internal use only. May be removed or replaced in the future. + * + * @param text + * The text the text box should contain. + */ + public void setPromptingOff(String text) { + debug("VFS: setPromptingOff()"); + setTextboxText(text); + if (prompting) { + prompting = false; + removeStyleDependentName(CLASSNAME_PROMPT); + } + } + + /** + * Triggered when a suggestion is selected + * + * @param suggestion + * The suggestion that just got selected. + */ + public void onSuggestionSelected(FilterSelectSuggestion suggestion) { + if (enableDebug) { + debug("VFS: onSuggestionSelected(" + suggestion.caption + ": " + + suggestion.key + ")"); + } + dataReceivedHandler.cancelPendingPostFiltering(); + + currentSuggestion = suggestion; + String newKey; + if (suggestion.key.equals("")) { + // "nullselection" + newKey = ""; + } else { + // normal selection + newKey = suggestion.getOptionKey(); + } + + String text = suggestion.getReplacementString(); + if ("".equals(newKey) && !focused) { + setPromptingOn(); + } else { + setPromptingOff(text); + } + setSelectedItemIcon(suggestion.getIconUri()); + + if (!(newKey.equals(selectedOptionKey) + || ("".equals(newKey) && selectedOptionKey == null))) { + selectedOptionKey = newKey; + connector.sendSelection(selectedOptionKey); + + // currentPage = -1; // forget the page + } + + if (getSelectedCaption() != null && newKey.equals("")) { + // In scrollToPage(false) mode selecting null seems to be broken + // if current selection is not on first page. The above clause is so + // hard to interpret that new clause added here :-( + selectedOptionKey = newKey; + explicitSelectedCaption = null; + connector.sendSelection(selectedOptionKey); + } + + suggestionPopup.hide(); + } + + /** + * Sets the icon URI of the selected item. The icon is shown on the left + * side of the item caption text. Set the URI to null to remove the icon. + * + * @param iconUri + * The URI of the icon + */ + public void setSelectedItemIcon(String iconUri) { + + if (iconUri == null || iconUri.length() == 0) { + if (selectedItemIcon != null) { + panel.remove(selectedItemIcon); + selectedItemIcon = null; + afterSelectedItemIconChange(); + } + } else { + if (selectedItemIcon != null) { + panel.remove(selectedItemIcon); + } + selectedItemIcon = new IconWidget( + connector.getConnection().getIcon(iconUri)); + // Older IE versions don't scale icon correctly if DOM + // contains height and width attributes. + selectedItemIcon.getElement().removeAttribute("height"); + selectedItemIcon.getElement().removeAttribute("width"); + selectedItemIcon.addDomHandler(new LoadHandler() { + @Override + public void onLoad(LoadEvent event) { + afterSelectedItemIconChange(); + } + }, LoadEvent.getType()); + panel.insert(selectedItemIcon, 0); + afterSelectedItemIconChange(); + } + } + + private void afterSelectedItemIconChange() { + if (BrowserInfo.get().isWebkit()) { + // Some browsers need a nudge to reposition the text field + forceReflow(); + } + updateRootWidth(); + if (selectedItemIcon != null) { + updateSelectedIconPosition(); + } + } + + /** + * Perform selection based on a message from the server. + * + * This method is called when the server gave a non-empty selected item key, + * whereas null selection is handled by {@link #resetSelection()} and the + * special case where the selected item is not on the current page is + * handled separately by the caller. + * + * @param selectedKey + * non-empty selected item key + * @param oldSuggestionTextMatchTheOldSelection + * true if the suggestion box text matched the previous selection + * before the message from the server updating the selection + * @param updatePromptAndSelectionIfMatchFound + */ + private void performSelection(String selectedKey, + boolean oldSuggestionTextMatchTheOldSelection, + boolean updatePromptAndSelectionIfMatchFound) { + // some item selected + for (FilterSelectSuggestion suggestion : currentSuggestions) { + String suggestionKey = suggestion.getOptionKey(); + if (!suggestionKey.equals(selectedKey)) { + continue; + } + // at this point, suggestion key matches the new selection key + if (updatePromptAndSelectionIfMatchFound) { + if (!suggestionKey.equals(selectedOptionKey) + || suggestion.getReplacementString() + .equals(tb.getText()) + || oldSuggestionTextMatchTheOldSelection) { + // Update text field if we've got a new + // selection + // Also update if we've got the same text to + // retain old text selection behavior + // OR if selected item caption is changed. + setPromptingOff(suggestion.getReplacementString()); + selectedOptionKey = suggestionKey; + } + } + currentSuggestion = suggestion; + setSelectedItemIcon(suggestion.getIconUri()); + // only a single item can be selected + break; + } + } + + /** + * Reset the selection of the combo box to an empty string if focused, the + * input prompt if not. + * + * This method also clears the current suggestion and the selected option + * key. + */ + private void resetSelection(boolean useSelectedCaption) { + // select nulled + if (!focused) { + // TODO it is unclear whether this is really needed anymore - + // client.updateComponent used to overwrite all styles so we had to + // set them again + setPromptingOff(""); + if (enabled && !readonly) { + setPromptingOn(); + } + } else { + // we have focus in field, prompting can't be set on, + // instead just clear the input if the value has changed from + // something else to null + if (selectedOptionKey != null + || (allowNewItem && !tb.getValue().isEmpty())) { + if (useSelectedCaption && getSelectedCaption() != null) { + tb.setValue(getSelectedCaption()); + tb.selectAll(); + } else { + tb.setValue(""); + } + } + } + currentSuggestion = null; // #13217 + setSelectedItemIcon(null); + selectedOptionKey = null; + } + + private void forceReflow() { + WidgetUtil.setStyleTemporarily(tb.getElement(), "zoom", "1"); + } + + /** + * Positions the icon vertically in the middle. Should be called after the + * icon has loaded + */ + private void updateSelectedIconPosition() { + // Position icon vertically to middle + int availableHeight = 0; + availableHeight = getOffsetHeight(); + + int iconHeight = WidgetUtil.getRequiredHeight(selectedItemIcon); + int marginTop = (availableHeight - iconHeight) / 2; + selectedItemIcon.getElement().getStyle().setMarginTop(marginTop, + Unit.PX); + } + + private static Set<Integer> navigationKeyCodes = new HashSet<Integer>(); + static { + navigationKeyCodes.add(KeyCodes.KEY_DOWN); + navigationKeyCodes.add(KeyCodes.KEY_UP); + navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN); + navigationKeyCodes.add(KeyCodes.KEY_PAGEUP); + navigationKeyCodes.add(KeyCodes.KEY_ENTER); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + + @Override + public void onKeyDown(KeyDownEvent event) { + if (enabled && !readonly) { + int keyCode = event.getNativeKeyCode(); + + if (enableDebug) { + debug("VFS: key down: " + keyCode); + } + if (dataReceivedHandler.isWaitingForFilteringResponse() + && navigationKeyCodes.contains(keyCode)) { + /* + * Keyboard navigation events should not be handled while we are + * waiting for a response. This avoids flickering, disappearing + * items, wrongly interpreted responses and more. + */ + if (enableDebug) { + debug("Ignoring " + keyCode + + " because we are waiting for a filtering response"); + } + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + return; + } + + if (suggestionPopup.isAttached()) { + if (enableDebug) { + debug("Keycode " + keyCode + " target is popup"); + } + popupKeyDown(event); + } else { + if (enableDebug) { + debug("Keycode " + keyCode + " target is text field"); + } + inputFieldKeyDown(event); + } + } + } + + private void debug(String string) { + if (enableDebug) { + VConsole.error(string); + } + } + + /** + * Triggered when a key is pressed in the text box + * + * @param event + * The KeyDownEvent + */ + private void inputFieldKeyDown(KeyDownEvent event) { + if (enableDebug) { + debug("VFS: inputFieldKeyDown(" + event.getNativeKeyCode() + ")"); + } + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_DOWN: + case KeyCodes.KEY_UP: + case KeyCodes.KEY_PAGEDOWN: + case KeyCodes.KEY_PAGEUP: + // open popup as from gadget + filterOptions(-1, ""); + lastFilter = ""; + tb.selectAll(); + break; + case KeyCodes.KEY_ENTER: + /* + * This only handles the case when new items is allowed, a text is + * entered, the popup opener button is clicked to close the popup + * and enter is then pressed (see #7560). + */ + if (!allowNewItem) { + return; + } + + if (currentSuggestion != null && tb.getText() + .equals(currentSuggestion.getReplacementString())) { + // Retain behavior from #6686 by returning without stopping + // propagation if there's nothing to do + return; + } + suggestionPopup.menu.doSelectedItemAction(); + + event.stopPropagation(); + break; + } + + } + + /** + * Triggered when a key was pressed in the suggestion popup. + * + * @param event + * The KeyDownEvent of the key + */ + private void popupKeyDown(KeyDownEvent event) { + if (enableDebug) { + debug("VFS: popupKeyDown(" + event.getNativeKeyCode() + ")"); + } + // Propagation of handled events is stopped so other handlers such as + // shortcut key handlers do not also handle the same events. + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_DOWN: + suggestionPopup.selectNextItem(); + + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + break; + case KeyCodes.KEY_UP: + suggestionPopup.selectPrevItem(); + + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + break; + case KeyCodes.KEY_PAGEDOWN: + selectNextPage(); + event.stopPropagation(); + break; + case KeyCodes.KEY_PAGEUP: + selectPrevPage(); + event.stopPropagation(); + break; + case KeyCodes.KEY_ESCAPE: + reset(); + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + event.stopPropagation(); + break; + case KeyCodes.KEY_TAB: + case KeyCodes.KEY_ENTER: + + if (!allowNewItem) { + int selected = suggestionPopup.menu.getSelectedIndex(); + if (selected != -1) { + onSuggestionSelected(currentSuggestions.get(selected)); + } else { + // The way VFilterSelect is done, it handles enter and tab + // in exactly the same way so we close the popup in both + // cases even though we could leave it open when pressing + // enter + suggestionPopup.hide(); + } + } else { + // Handle addition of new items. + suggestionPopup.menu.doSelectedItemAction(); + } + + event.stopPropagation(); + break; + } + + } + + /* + * Show the prev page. + */ + private void selectPrevPage() { + if (currentPage > 0) { + filterOptions(currentPage - 1, lastFilter); + dataReceivedHandler.setNavigationCallback(new Runnable() { + @Override + public void run() { + suggestionPopup.selectLastItem(); + } + }); + } + } + + /* + * Show the next page. + */ + private void selectNextPage() { + if (hasNextPage()) { + filterOptions(currentPage + 1, lastFilter); + dataReceivedHandler.setNavigationCallback(new Runnable() { + @Override + public void run() { + suggestionPopup.selectFirstItem(); + } + }); + } + } + + /** + * Triggered when a key was depressed + * + * @param event + * The KeyUpEvent of the key depressed + */ + + @Override + public void onKeyUp(KeyUpEvent event) { + if (enableDebug) { + debug("VFS: onKeyUp(" + event.getNativeKeyCode() + ")"); + } + if (enabled && !readonly) { + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_ENTER: + case KeyCodes.KEY_TAB: + case KeyCodes.KEY_SHIFT: + case KeyCodes.KEY_CTRL: + case KeyCodes.KEY_ALT: + case KeyCodes.KEY_DOWN: + case KeyCodes.KEY_UP: + case KeyCodes.KEY_PAGEDOWN: + case KeyCodes.KEY_PAGEUP: + case KeyCodes.KEY_ESCAPE: + // NOP + break; + default: + if (textInputEnabled) { + // when filtering, we always want to see the results on the + // first page first. + filterOptions(0); + } + break; + } + } + } + + /** + * Resets the Select to its initial state + */ + private void reset() { + debug("VFS: reset()"); + if (currentSuggestion != null) { + String text = currentSuggestion.getReplacementString(); + setPromptingOff(text); + setSelectedItemIcon(currentSuggestion.getIconUri()); + + selectedOptionKey = currentSuggestion.key; + + } else { + if (focused || readonly || !enabled) { + setPromptingOff(""); + } else { + setPromptingOn(); + } + setSelectedItemIcon(null); + + selectedOptionKey = null; + } + + lastFilter = ""; + suggestionPopup.hide(); + } + + /** + * Listener for popupopener + */ + + @Override + public void onClick(ClickEvent event) { + debug("VFS: onClick()"); + if (textInputEnabled && event.getNativeEvent().getEventTarget() + .cast() == tb.getElement()) { + // Don't process clicks on the text field if text input is enabled + return; + } + if (enabled && !readonly) { + // ask suggestionPopup if it was just closed, we are using GWT + // Popup's auto close feature + if (!suggestionPopup.isJustClosed()) { + filterOptions(-1, ""); + dataReceivedHandler.popupOpenerClicked(); + lastFilter = ""; + } + DOM.eventPreventDefault(DOM.eventGetCurrentEvent()); + focus(); + tb.selectAll(); + } + } + + /** + * Update minimum width for FilterSelect textarea based on input prompt and + * suggestions. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public void updateSuggestionPopupMinWidth() { + // used only to calculate minimum width + String captions = WidgetUtil.escapeHTML(inputPrompt); + + for (FilterSelectSuggestion suggestion : currentSuggestions) { + // Collect captions so we can calculate minimum width for + // textarea + if (captions.length() > 0) { + captions += "|"; + } + captions += WidgetUtil + .escapeHTML(suggestion.getReplacementString()); + } + + // Calculate minimum textarea width + suggestionPopupMinWidth = minWidth(captions); + } + + /** + * Calculate minimum width for FilterSelect textarea. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public native int minWidth(String captions) + /*-{ + if(!captions || captions.length <= 0) + return 0; + captions = captions.split("|"); + var d = $wnd.document.createElement("div"); + var html = ""; + for(var i=0; i < captions.length; i++) { + html += "<div>" + captions[i] + "</div>"; + // TODO apply same CSS classname as in suggestionmenu + } + d.style.position = "absolute"; + d.style.top = "0"; + d.style.left = "0"; + d.style.visibility = "hidden"; + d.innerHTML = html; + $wnd.document.body.appendChild(d); + var w = d.offsetWidth; + $wnd.document.body.removeChild(d); + return w; + }-*/; + + /** + * A flag which prevents a focus event from taking place + */ + boolean iePreventNextFocus = false; + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + + @Override + public void onFocus(FocusEvent event) { + debug("VFS: onFocus()"); + + /* + * When we disable a blur event in ie we need to refocus the textfield. + * This will cause a focus event we do not want to process, so in that + * case we just ignore it. + */ + if (BrowserInfo.get().isIE() && iePreventNextFocus) { + iePreventNextFocus = false; + return; + } + + focused = true; + if (prompting && !readonly) { + setPromptingOff(""); + } + addStyleDependentName("focus"); + + connector.sendFocusEvent(); + + connector.getConnection().getVTooltip() + .showAssistive(connector.getTooltipInfo(getElement())); + } + + /** + * A flag which cancels the blur event and sets the focus back to the + * textfield if the Browser is IE + */ + boolean preventNextBlurEventInIE = false; + + private String explicitSelectedCaption; + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + + @Override + public void onBlur(BlurEvent event) { + debug("VFS: onBlur()"); + + if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) { + /* + * Clicking in the suggestion popup or on the popup button in IE + * causes a blur event to be sent for the field. In other browsers + * this is prevented by canceling/preventing default behavior for + * the focus event, in IE we handle it here by refocusing the text + * field and ignoring the resulting focus event for the textfield + * (in onFocus). + */ + preventNextBlurEventInIE = false; + + Element focusedElement = WidgetUtil.getFocusedElement(); + if (getElement().isOrHasChild(focusedElement) || suggestionPopup + .getElement().isOrHasChild(focusedElement)) { + + // IF the suggestion popup or another part of the VFilterSelect + // was focused, move the focus back to the textfield and prevent + // the triggered focus event (in onFocus). + iePreventNextFocus = true; + tb.setFocus(true); + return; + } + } + + focused = false; + if (!readonly) { + if (selectedOptionKey == null) { + if (explicitSelectedCaption != null) { + setPromptingOff(explicitSelectedCaption); + } else { + setPromptingOn(); + } + } else if (currentSuggestion != null) { + setPromptingOff(currentSuggestion.caption); + } + } + removeStyleDependentName("focus"); + + connector.sendBlurEvent(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.Focusable#focus() + */ + + @Override + public void focus() { + debug("VFS: focus()"); + focused = true; + if (prompting && !readonly) { + setPromptingOff(""); + } + tb.setFocus(true); + } + + /** + * Calculates the width of the select if the select has undefined width. + * Should be called when the width changes or when the icon changes. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + public void updateRootWidth() { + if (connector.isUndefinedWidth()) { + + /* + * When the select has a undefined with we need to check that we are + * only setting the text box width relative to the first page width + * of the items. If this is not done the text box width will change + * when the popup is used to view longer items than the text box is + * wide. + */ + int w = WidgetUtil.getRequiredWidth(this); + + if ((!initDone || currentPage + 1 < 0) + && suggestionPopupMinWidth > w) { + /* + * We want to compensate for the paddings just to preserve the + * exact size as in Vaadin 6.x, but we get here before + * MeasuredSize has been initialized. + * Util.measureHorizontalPaddingAndBorder does not work with + * border-box, so we must do this the hard way. + */ + Style style = getElement().getStyle(); + String originalPadding = style.getPadding(); + String originalBorder = style.getBorderWidth(); + style.setPaddingLeft(0, Unit.PX); + style.setBorderWidth(0, Unit.PX); + style.setProperty("padding", originalPadding); + style.setProperty("borderWidth", originalBorder); + + // Use util.getRequiredWidth instead of getOffsetWidth here + + int iconWidth = selectedItemIcon == null ? 0 + : WidgetUtil.getRequiredWidth(selectedItemIcon); + int buttonWidth = popupOpener == null ? 0 + : WidgetUtil.getRequiredWidth(popupOpener); + + /* + * Instead of setting the width of the wrapper, set the width of + * the combobox. Subtract the width of the icon and the + * popupopener + */ + + tb.setWidth((suggestionPopupMinWidth - iconWidth - buttonWidth) + + "px"); + + } + + /* + * Lock the textbox width to its current value if it's not already + * locked + */ + if (!tb.getElement().getStyle().getWidth().endsWith("px")) { + int iconWidth = selectedItemIcon == null ? 0 + : selectedItemIcon.getOffsetWidth(); + tb.setWidth((tb.getOffsetWidth() - iconWidth) + "px"); + } + } + } + + /** + * Get the width of the select in pixels where the text area and icon has + * been included. + * + * @return The width in pixels + */ + private int getMainWidth() { + return getOffsetWidth(); + } + + @Override + public void setWidth(String width) { + super.setWidth(width); + if (width.length() != 0) { + tb.setWidth("100%"); + } + } + + /** + * Handles special behavior of the mouse down event + * + * @param event + */ + private void handleMouseDownEvent(Event event) { + /* + * Prevent the keyboard focus from leaving the textfield by preventing + * the default behaviour of the browser. Fixes #4285. + */ + if (event.getTypeInt() == Event.ONMOUSEDOWN) { + event.preventDefault(); + event.stopPropagation(); + + /* + * In IE the above wont work, the blur event will still trigger. So, + * we set a flag here to prevent the next blur event from happening. + * This is not needed if do not already have focus, in that case + * there will not be any blur event and we should not cancel the + * next blur. + */ + if (BrowserInfo.get().isIE() && focused) { + preventNextBlurEventInIE = true; + debug("VFS: Going to prevent next blur event on IE"); + } + } + } + + @Override + protected void onDetach() { + super.onDetach(); + suggestionPopup.hide(); + } + + @Override + public com.google.gwt.user.client.Element getSubPartElement( + String subPart) { + String[] parts = subPart.split("/"); + if ("textbox".equals(parts[0])) { + return tb.getElement(); + } else if ("button".equals(parts[0])) { + return popupOpener.getElement(); + } else if ("popup".equals(parts[0]) && suggestionPopup.isAttached()) { + if (parts.length == 2) { + return suggestionPopup.menu.getSubPartElement(parts[1]); + } + return suggestionPopup.getElement(); + } + return null; + } + + @Override + public String getSubPartName( + com.google.gwt.user.client.Element subElement) { + if (tb.getElement().isOrHasChild(subElement)) { + return "textbox"; + } else if (popupOpener.getElement().isOrHasChild(subElement)) { + return "button"; + } else if (suggestionPopup.getElement().isOrHasChild(subElement)) { + return "popup"; + } + return null; + } + + @Override + public void setAriaRequired(boolean required) { + AriaHelper.handleInputRequired(tb, required); + } + + @Override + public void setAriaInvalid(boolean invalid) { + AriaHelper.handleInputInvalid(tb, invalid); + } + + @Override + public void bindAriaCaption( + com.google.gwt.user.client.Element captionElement) { + AriaHelper.bindCaption(tb, captionElement); + } + + @Override + public boolean isWorkPending() { + return dataReceivedHandler.isWaitingForFilteringResponse() + || suggestionPopup.lazyPageScroller.isRunning(); + } + + /** + * Sets the caption of selected item, if "scroll to page" is disabled. This + * method is meant for internal use and may change in future versions. + * + * @since 7.7 + * @param selectedCaption + * the caption of selected item + */ + public void setSelectedCaption(String selectedCaption) { + explicitSelectedCaption = selectedCaption; + if (selectedCaption != null) { + setPromptingOff(selectedCaption); + } + } + + /** + * This method is meant for internal use and may change in future versions. + * + * @since 7.7 + * @return the caption of selected item, if "scroll to page" is disabled + */ + public String getSelectedCaption() { + return explicitSelectedCaption; + } + + /** + * Returns a handler receiving notifications from the connector about + * communications. + * + * @return the dataReceivedHandler + */ + public DataReceivedHandler getDataReceivedHandler() { + return dataReceivedHandler; + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java new file mode 100644 index 0000000000..f9101a5e30 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java @@ -0,0 +1,2267 @@ +/* + * Copyright 2000-2016 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.client.ui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.gwt.aria.client.ExpandedValue; +import com.google.gwt.aria.client.Id; +import com.google.gwt.aria.client.Roles; +import com.google.gwt.aria.client.SelectedValue; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.MouseEventDetailsBuilder; +import com.vaadin.client.UIDL; +import com.vaadin.client.Util; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.aria.AriaHelper; +import com.vaadin.client.ui.aria.HandlesAriaCaption; +import com.vaadin.client.ui.dd.DDUtil; +import com.vaadin.client.ui.dd.VAbstractDropHandler; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragAndDropManager; +import com.vaadin.client.ui.dd.VDragEvent; +import com.vaadin.client.ui.dd.VDropHandler; +import com.vaadin.client.ui.dd.VHasDropHandler; +import com.vaadin.client.ui.dd.VTransferable; +import com.vaadin.client.ui.tree.TreeConnector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.MouseEventDetails.MouseButton; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.shared.ui.tree.TreeConstants; + +/** + * + */ +public class VTree extends FocusElementPanel + implements VHasDropHandler, FocusHandler, BlurHandler, KeyPressHandler, + KeyDownHandler, SubPartAware, ActionOwner, HandlesAriaCaption { + private String lastNodeKey = ""; + + public static final String CLASSNAME = "v-tree"; + + /** + * @deprecated As of 7.0, use {@link MultiSelectMode#DEFAULT} instead. + */ + @Deprecated + public static final MultiSelectMode MULTISELECT_MODE_DEFAULT = MultiSelectMode.DEFAULT; + + /** + * @deprecated As of 7.0, use {@link MultiSelectMode#SIMPLE} instead. + */ + @Deprecated + public static final MultiSelectMode MULTISELECT_MODE_SIMPLE = MultiSelectMode.SIMPLE; + + private static final int CHARCODE_SPACE = 32; + + /** For internal use only. May be removed or replaced in the future. */ + public final FlowPanel body = new FlowPanel(); + + /** For internal use only. May be removed or replaced in the future. */ + public Set<String> selectedIds = new HashSet<String>(); + + /** For internal use only. May be removed or replaced in the future. */ + public ApplicationConnection client; + + /** For internal use only. May be removed or replaced in the future. */ + public String paintableId; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean selectable; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean isMultiselect; + + private String currentMouseOverKey; + + /** For internal use only. May be removed or replaced in the future. */ + public TreeNode lastSelection; + + /** For internal use only. May be removed or replaced in the future. */ + public TreeNode focusedNode; + + /** For internal use only. May be removed or replaced in the future. */ + public MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>(); + + /** + * This map contains captions and icon urls for actions like: * "33_c" -> + * "Edit" * "33_i" -> "http://dom.com/edit.png" + */ + private final HashMap<String, String> actionMap = new HashMap<String, String>(); + + /** For internal use only. May be removed or replaced in the future. */ + public boolean immediate; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean isNullSelectionAllowed = true; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean isHtmlContentAllowed = false; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean disabled = false; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean readonly; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean rendering; + + private VAbstractDropHandler dropHandler; + + /** For internal use only. May be removed or replaced in the future. */ + public int dragMode; + + private boolean selectionHasChanged = false; + + /* + * to fix #14388. The cause of defect #14388: event 'clickEvent' is sent to + * server before updating of "selected" variable, but should be sent after + * it + */ + private boolean clickEventPending = false; + + /** For internal use only. May be removed or replaced in the future. */ + public String[] bodyActionKeys; + + /** For internal use only. May be removed or replaced in the future. */ + public TreeConnector connector; + + public VLazyExecutor iconLoaded = new VLazyExecutor(50, + new ScheduledCommand() { + + @Override + public void execute() { + doLayout(); + } + + }); + + public VTree() { + super(); + setStyleName(CLASSNAME); + + Roles.getTreeRole().set(body.getElement()); + add(body); + + addFocusHandler(this); + addBlurHandler(this); + + /* + * Listen to context menu events on the empty space in the tree + */ + sinkEvents(Event.ONCONTEXTMENU); + addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + handleBodyContextMenu(event); + } + }, ContextMenuEvent.getType()); + + /* + * Firefox auto-repeat works correctly only if we use a key press + * handler, other browsers handle it correctly when using a key down + * handler + */ + if (BrowserInfo.get().isGecko()) { + addKeyPressHandler(this); + } else { + addKeyDownHandler(this); + } + + /* + * We need to use the sinkEvents method to catch the keyUp events so we + * can cache a single shift. KeyUpHandler cannot do this. At the same + * time we catch the mouse down and up events so we can apply the text + * selection patch in IE + */ + sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP); + + /* + * Re-set the tab index to make sure that the FocusElementPanel's + * (super) focus element gets the tab index and not the element + * containing the tree. + */ + setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user + * .client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONMOUSEDOWN) { + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()).setPropertyJSO( + "onselectstart", applyDisableTextSelectionIEHack()); + } + } else if (event.getTypeInt() == Event.ONMOUSEUP) { + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO("onselectstart", null); + } + } else if (event.getTypeInt() == Event.ONKEYUP) { + if (selectionHasChanged) { + if (event.getKeyCode() == getNavigationDownKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationUpKey() + && !event.getShiftKey()) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) { + sendSelectionToServer(); + event.preventDefault(); + } else if (event.getKeyCode() == getNavigationSelectKey()) { + sendSelectionToServer(); + event.preventDefault(); + } + } + } + } + + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + /** + * Returns the first root node of the tree or null if there are no root + * nodes. + * + * @return The first root {@link TreeNode} + */ + protected TreeNode getFirstRootNode() { + if (body.getWidgetCount() == 0) { + return null; + } + return (TreeNode) body.getWidget(0); + } + + /** + * Returns the last root node of the tree or null if there are no root + * nodes. + * + * @return The last root {@link TreeNode} + */ + protected TreeNode getLastRootNode() { + if (body.getWidgetCount() == 0) { + return null; + } + return (TreeNode) body.getWidget(body.getWidgetCount() - 1); + } + + /** + * Returns a list of all root nodes in the Tree in the order they appear in + * the tree. + * + * @return A list of all root {@link TreeNode}s. + */ + protected List<TreeNode> getRootNodes() { + ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>(); + for (int i = 0; i < body.getWidgetCount(); i++) { + rootNodes.add((TreeNode) body.getWidget(i)); + } + return rootNodes; + } + + private void updateTreeRelatedDragData(VDragEvent drag) { + + currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver()); + + drag.getDropDetails().put("itemIdOver", currentMouseOverKey); + if (currentMouseOverKey != null) { + TreeNode treeNode = getNodeByKey(currentMouseOverKey); + VerticalDropLocation detail = treeNode + .getDropDetail(drag.getCurrentGwtEvent()); + Boolean overTreeNode = null; + if (treeNode != null && !treeNode.isLeaf() + && detail == VerticalDropLocation.MIDDLE) { + overTreeNode = true; + } + drag.getDropDetails().put("itemIdOverIsNode", overTreeNode); + drag.getDropDetails().put("detail", detail); + } else { + drag.getDropDetails().put("itemIdOverIsNode", null); + drag.getDropDetails().put("detail", null); + } + + } + + private String findCurrentMouseOverKey(Element elementOver) { + TreeNode treeNode = WidgetUtil.findWidget(elementOver, TreeNode.class); + return treeNode == null ? null : treeNode.key; + } + + /** For internal use only. May be removed or replaced in the future. */ + public void updateDropHandler(UIDL childUidl) { + if (dropHandler == null) { + dropHandler = new VAbstractDropHandler() { + + @Override + public void dragEnter(VDragEvent drag) { + } + + @Override + protected void dragAccepted(final VDragEvent drag) { + + } + + @Override + public void dragOver(final VDragEvent currentDrag) { + final Object oldIdOver = currentDrag.getDropDetails() + .get("itemIdOver"); + final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag + .getDropDetails().get("detail"); + + updateTreeRelatedDragData(currentDrag); + final VerticalDropLocation detail = (VerticalDropLocation) currentDrag + .getDropDetails().get("detail"); + boolean nodeHasChanged = (currentMouseOverKey != null + && currentMouseOverKey != oldIdOver) + || (currentMouseOverKey == null + && oldIdOver != null); + boolean detailHasChanded = (detail != null + && detail != oldDetail) + || (detail == null && oldDetail != null); + + if (nodeHasChanged || detailHasChanded) { + final String newKey = currentMouseOverKey; + TreeNode treeNode = keyToNode.get(oldIdOver); + if (treeNode != null) { + // clear old styles + treeNode.emphasis(null); + } + if (newKey != null) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + VerticalDropLocation curDetail = (VerticalDropLocation) event + .getDropDetails().get("detail"); + if (curDetail == detail && newKey + .equals(currentMouseOverKey)) { + getNodeByKey(newKey).emphasis(detail); + } + /* + * Else drag is already on a different + * node-detail pair, new criteria check is + * going on + */ + } + }, currentDrag); + + } + } + + } + + @Override + public void dragLeave(VDragEvent drag) { + cleanUp(); + } + + private void cleanUp() { + if (currentMouseOverKey != null) { + getNodeByKey(currentMouseOverKey).emphasis(null); + currentMouseOverKey = null; + } + } + + @Override + public boolean drop(VDragEvent drag) { + cleanUp(); + return super.drop(drag); + } + + @Override + public ComponentConnector getConnector() { + return ConnectorMap.get(client).getConnector(VTree.this); + } + + @Override + public ApplicationConnection getApplicationConnection() { + return client; + } + + }; + } + dropHandler.updateAcceptRules(childUidl); + } + + public void setSelected(TreeNode treeNode, boolean selected) { + if (selected) { + if (!isMultiselect) { + while (selectedIds.size() > 0) { + final String id = selectedIds.iterator().next(); + final TreeNode oldSelection = getNodeByKey(id); + if (oldSelection != null) { + // can be null if the node is not visible (parent + // collapsed) + oldSelection.setSelected(false); + } + selectedIds.remove(id); + } + } + treeNode.setSelected(true); + selectedIds.add(treeNode.key); + } else { + if (!isNullSelectionAllowed) { + if (!isMultiselect || selectedIds.size() == 1) { + return; + } + } + selectedIds.remove(treeNode.key); + treeNode.setSelected(false); + } + + sendSelectionToServer(); + } + + /** + * Sends the selection to the server + */ + private void sendSelectionToServer() { + Command command = new Command() { + @Override + public void execute() { + /* + * we should send selection to server immediately in 2 cases: 1) + * 'immediate' property of Tree is true 2) clickEventPending is + * true + */ + client.updateVariable(paintableId, "selected", + selectedIds.toArray(new String[selectedIds.size()]), + clickEventPending || immediate); + clickEventPending = false; + selectionHasChanged = false; + } + }; + + /* + * Delaying the sending of the selection in webkit to ensure the + * selection is always sent when the tree has focus and after click + * events have been processed. This is due to the focusing + * implementation in FocusImplSafari which uses timeouts when focusing + * and blurring. + */ + if (BrowserInfo.get().isWebkit()) { + Scheduler.get().scheduleDeferred(command); + } else { + command.execute(); + } + } + + /** + * Is a node selected in the tree + * + * @param treeNode + * The node to check + * @return + */ + public boolean isSelected(TreeNode treeNode) { + return selectedIds.contains(treeNode.key); + } + + public class TreeNode extends SimplePanel implements ActionOwner { + + public static final String CLASSNAME = "v-tree-node"; + public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused"; + + public String key; + + /** For internal use only. May be removed or replaced in the future. */ + public String[] actionKeys = null; + + /** For internal use only. May be removed or replaced in the future. */ + public boolean childrenLoaded; + + Element nodeCaptionDiv; + + protected Element nodeCaptionSpan; + + /** For internal use only. May be removed or replaced in the future. */ + public FlowPanel childNodeContainer; + + private boolean open; + + private Icon icon; + + private Event mouseDownEvent; + + private int cachedHeight = -1; + + private boolean focused = false; + + public TreeNode() { + constructDom(); + sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS + | Event.TOUCHEVENTS | Event.ONCONTEXTMENU); + } + + public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) { + if (cachedHeight < 0) { + /* + * Height is cached to avoid flickering (drop hints may change + * the reported offsetheight -> would change the drop detail) + */ + cachedHeight = nodeCaptionDiv.getOffsetHeight(); + } + VerticalDropLocation verticalDropLocation = DDUtil + .getVerticalDropLocation(nodeCaptionDiv, cachedHeight, + currentGwtEvent, 0.15); + return verticalDropLocation; + } + + protected void emphasis(VerticalDropLocation detail) { + String base = "v-tree-node-drag-"; + UIObject.setStyleName(getElement(), base + "top", + VerticalDropLocation.TOP == detail); + UIObject.setStyleName(getElement(), base + "bottom", + VerticalDropLocation.BOTTOM == detail); + UIObject.setStyleName(getElement(), base + "center", + VerticalDropLocation.MIDDLE == detail); + base = "v-tree-node-caption-drag-"; + UIObject.setStyleName(nodeCaptionDiv, base + "top", + VerticalDropLocation.TOP == detail); + UIObject.setStyleName(nodeCaptionDiv, base + "bottom", + VerticalDropLocation.BOTTOM == detail); + UIObject.setStyleName(nodeCaptionDiv, base + "center", + VerticalDropLocation.MIDDLE == detail); + + // also add classname to "folder node" into which the drag is + // targeted + + TreeNode folder = null; + /* Possible parent of this TreeNode will be stored here */ + TreeNode parentFolder = getParentNode(); + + // TODO fix my bugs + if (isLeaf()) { + folder = parentFolder; + // note, parent folder may be null if this is root node => no + // folder target exists + } else { + if (detail == VerticalDropLocation.TOP) { + folder = parentFolder; + } else { + folder = this; + } + // ensure we remove the dragfolder classname from the previous + // folder node + setDragFolderStyleName(this, false); + setDragFolderStyleName(parentFolder, false); + } + if (folder != null) { + setDragFolderStyleName(folder, detail != null); + } + + } + + private TreeNode getParentNode() { + Widget parent2 = getParent().getParent(); + if (parent2 instanceof TreeNode) { + return (TreeNode) parent2; + } + return null; + } + + private void setDragFolderStyleName(TreeNode folder, boolean add) { + if (folder != null) { + UIObject.setStyleName(folder.getElement(), + "v-tree-node-dragfolder", add); + UIObject.setStyleName(folder.nodeCaptionDiv, + "v-tree-node-caption-dragfolder", add); + } + } + + /** + * Handles mouse selection + * + * @param ctrl + * Was the ctrl-key pressed + * @param shift + * Was the shift-key pressed + * @return Returns true if event was handled, else false + */ + private boolean handleClickSelection(final boolean ctrl, + final boolean shift) { + + // always when clicking an item, focus it + setFocusedNode(this, false); + + if (!BrowserInfo.get().isOpera()) { + /* + * Ensure that the tree's focus element also gains focus + * (TreeNodes focus is faked using FocusElementPanel in browsers + * other than Opera). + */ + focus(); + } + + executeEventCommand(new ScheduledCommand() { + + @Override + public void execute() { + + if (multiSelectMode == MultiSelectMode.SIMPLE + || !isMultiselect) { + toggleSelection(); + lastSelection = TreeNode.this; + } else if (multiSelectMode == MultiSelectMode.DEFAULT) { + // Handle ctrl+click + if (isMultiselect && ctrl && !shift) { + toggleSelection(); + lastSelection = TreeNode.this; + + // Handle shift+click + } else if (isMultiselect && !ctrl && shift) { + deselectAll(); + selectNodeRange(lastSelection.key, key); + sendSelectionToServer(); + + // Handle ctrl+shift click + } else if (isMultiselect && ctrl && shift) { + selectNodeRange(lastSelection.key, key); + + // Handle click + } else { + // TODO should happen only if this alone not yet + // selected, + // now sending excess server calls + deselectAll(); + toggleSelection(); + lastSelection = TreeNode.this; + } + } + } + }); + + return true; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt + * .user.client.Event) + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + final int type = DOM.eventGetType(event); + final Element target = DOM.eventGetTarget(event); + + if (type == Event.ONLOAD && icon != null + && target == icon.getElement()) { + iconLoaded.trigger(); + } + + if (disabled) { + return; + } + + final boolean inCaption = isCaptionElement(target); + if (inCaption && client.hasEventListeners(VTree.this, + TreeConstants.ITEM_CLICK_EVENT_ID) + + && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) { + fireClick(event); + } + if (type == Event.ONCLICK) { + if (getElement() == target) { + // state change + toggleState(); + } else if (!readonly && inCaption) { + if (selectable) { + // caption click = selection change && possible click + // event + if (handleClickSelection( + event.getCtrlKey() || event.getMetaKey(), + event.getShiftKey())) { + event.preventDefault(); + } + } else { + // Not selectable, only focus the node. + setFocusedNode(this); + } + } + event.stopPropagation(); + } else if (type == Event.ONCONTEXTMENU) { + showContextMenu(event); + } + + if (dragMode != 0 || dropHandler != null) { + if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) { + if (nodeCaptionDiv.isOrHasChild( + (Node) event.getEventTarget().cast())) { + if (dragMode > 0 && (type == Event.ONTOUCHSTART || event + .getButton() == NativeEvent.BUTTON_LEFT)) { + mouseDownEvent = event; // save event for possible + // dd operation + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); // prevent text + // selection + } else { + /* + * FIXME We prevent touch start event to be used + * as a scroll start event. Note that we cannot + * easily distinguish whether the user wants to + * drag or scroll. The same issue is in table + * that has scrollable area and has drag and + * drop enable. Some kind of timer might be used + * to resolve the issue. + */ + event.stopPropagation(); + } + } + } + } else if (type == Event.ONMOUSEMOVE || type == Event.ONMOUSEOUT + || type == Event.ONTOUCHMOVE) { + + if (mouseDownEvent != null) { + // start actual drag on slight move when mouse is down + VTransferable t = new VTransferable(); + t.setDragSource(ConnectorMap.get(client) + .getConnector(VTree.this)); + t.setData("itemId", key); + VDragEvent drag = VDragAndDropManager.get().startDrag(t, + mouseDownEvent, true); + + drag.createDragImage(nodeCaptionDiv, true); + event.stopPropagation(); + + mouseDownEvent = null; + } + } else if (type == Event.ONMOUSEUP) { + mouseDownEvent = null; + } + if (type == Event.ONMOUSEOVER) { + mouseDownEvent = null; + currentMouseOverKey = key; + event.stopPropagation(); + } + + } else if (type == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT) { + event.preventDefault(); // text selection + } + } + + /** + * Checks if the given element is the caption or the icon. + * + * @param target + * The element to check + * @return true if the element is the caption or the icon + */ + public boolean isCaptionElement( + com.google.gwt.dom.client.Element target) { + return (target == nodeCaptionSpan + || (icon != null && target == icon.getElement())); + } + + private void fireClick(final Event evt) { + /* + * Ensure we have focus in tree before sending variables. Otherwise + * previously modified field may contain dirty variables. + */ + if (!treeHasFocus) { + if (BrowserInfo.get().isOpera()) { + if (focusedNode == null) { + getNodeByKey(key).setFocused(true); + } else { + focusedNode.setFocused(true); + } + } else { + focus(); + } + } + + final MouseEventDetails details = MouseEventDetailsBuilder + .buildMouseEventDetails(evt); + + executeEventCommand(new ScheduledCommand() { + + @Override + public void execute() { + // Determine if we should send the event immediately to the + // server. We do not want to send the event if there is a + // selection event happening after this. In all other cases + // we want to send it immediately. + clickEventPending = false; + if ((details.getButton() == MouseButton.LEFT + || details.getButton() == MouseButton.MIDDLE) + && !details.isDoubleClick() && selectable) { + // Probably a selection that will cause a value change + // event to be sent + clickEventPending = true; + + // The exception is that user clicked on the + // currently selected row and null selection is not + // allowed == no selection event + if (isSelected() && selectedIds.size() == 1 + && !isNullSelectionAllowed) { + clickEventPending = false; + } + } + client.updateVariable(paintableId, "clickedKey", key, + false); + client.updateVariable(paintableId, "clickEvent", + details.toString(), !clickEventPending); + } + }); + } + + /* + * Must wait for Safari to focus before sending click and value change + * events (see #6373, #6374) + */ + private void executeEventCommand(ScheduledCommand command) { + if (BrowserInfo.get().isWebkit() && !treeHasFocus) { + Scheduler.get().scheduleDeferred(command); + } else { + command.execute(); + } + } + + private void toggleSelection() { + if (selectable) { + VTree.this.setSelected(this, !isSelected()); + } + } + + private void toggleState() { + setState(!getState(), true); + } + + protected void constructDom() { + String labelId = DOM.createUniqueId(); + + addStyleName(CLASSNAME); + String treeItemId = DOM.createUniqueId(); + getElement().setId(treeItemId); + Roles.getTreeitemRole().set(getElement()); + Roles.getTreeitemRole().setAriaSelectedState(getElement(), + SelectedValue.FALSE); + Roles.getTreeitemRole().setAriaLabelledbyProperty(getElement(), + Id.of(labelId)); + + nodeCaptionDiv = DOM.createDiv(); + DOM.setElementProperty(nodeCaptionDiv, "className", + CLASSNAME + "-caption"); + Element wrapper = DOM.createDiv(); + wrapper.setId(labelId); + wrapper.setAttribute("for", treeItemId); + + nodeCaptionSpan = DOM.createSpan(); + DOM.appendChild(getElement(), nodeCaptionDiv); + DOM.appendChild(nodeCaptionDiv, wrapper); + DOM.appendChild(wrapper, nodeCaptionSpan); + + if (BrowserInfo.get().isOpera()) { + /* + * Focus the caption div of the node to get keyboard navigation + * to work without scrolling up or down when focusing a node. + */ + nodeCaptionDiv.setTabIndex(-1); + } + + childNodeContainer = new FlowPanel(); + childNodeContainer.setStyleName(CLASSNAME + "-children"); + Roles.getGroupRole().set(childNodeContainer.getElement()); + setWidget(childNodeContainer); + } + + public boolean isLeaf() { + String[] styleNames = getStyleName().split(" "); + for (String styleName : styleNames) { + if (styleName.equals(CLASSNAME + "-leaf")) { + return true; + } + } + return false; + } + + /** For internal use only. May be removed or replaced in the future. */ + public void setState(boolean state, boolean notifyServer) { + if (open == state) { + return; + } + if (state) { + if (!childrenLoaded && notifyServer) { + client.updateVariable(paintableId, "requestChildTree", true, + false); + } + if (notifyServer) { + client.updateVariable(paintableId, "expand", + new String[] { key }, true); + } + addStyleName(CLASSNAME + "-expanded"); + Roles.getTreeitemRole().setAriaExpandedState(getElement(), + ExpandedValue.TRUE); + childNodeContainer.setVisible(true); + } else { + removeStyleName(CLASSNAME + "-expanded"); + Roles.getTreeitemRole().setAriaExpandedState(getElement(), + ExpandedValue.FALSE); + childNodeContainer.setVisible(false); + if (notifyServer) { + client.updateVariable(paintableId, "collapse", + new String[] { key }, true); + } + } + open = state; + + if (!rendering) { + doLayout(); + } + } + + /** For internal use only. May be removed or replaced in the future. */ + public boolean getState() { + return open; + } + + /** For internal use only. May be removed or replaced in the future. */ + public void setText(String text) { + DOM.setInnerText(nodeCaptionSpan, text); + } + + /** For internal use only. May be removed or replaced in the future. */ + public void setHtml(String html) { + nodeCaptionSpan.setInnerHTML(html); + } + + public boolean isChildrenLoaded() { + return childrenLoaded; + } + + /** + * Returns the children of the node + * + * @return A set of tree nodes + */ + public List<TreeNode> getChildren() { + List<TreeNode> nodes = new LinkedList<TreeNode>(); + + if (!isLeaf() && isChildrenLoaded()) { + Iterator<Widget> iter = childNodeContainer.iterator(); + while (iter.hasNext()) { + TreeNode node = (TreeNode) iter.next(); + nodes.add(node); + } + } + return nodes; + } + + @Override + public Action[] getActions() { + if (actionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[actionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = actionKeys[i]; + final TreeAction a = new TreeAction(this, String.valueOf(key), + actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + /** + * Adds/removes Vaadin specific style name. + * <p> + * For internal use only. May be removed or replaced in the future. + * + * @param selected + */ + public void setSelected(boolean selected) { + // add style name to caption dom structure only, not to subtree + setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected); + } + + protected boolean isSelected() { + return VTree.this.isSelected(this); + } + + /** + * Travels up the hierarchy looking for this node + * + * @param child + * The child which grandparent this is or is not + * @return True if this is a grandparent of the child node + */ + public boolean isGrandParentOf(TreeNode child) { + TreeNode currentNode = child; + boolean isGrandParent = false; + while (currentNode != null) { + currentNode = currentNode.getParentNode(); + if (currentNode == this) { + isGrandParent = true; + break; + } + } + return isGrandParent; + } + + public boolean isSibling(TreeNode node) { + return node.getParentNode() == getParentNode(); + } + + public void showContextMenu(Event event) { + if (!readonly && !disabled) { + if (actionKeys != null) { + int left = event.getClientX(); + int top = event.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + + event.stopPropagation(); + event.preventDefault(); + } + } + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.Widget#onDetach() + */ + @Override + protected void onDetach() { + super.onDetach(); + client.getContextMenu().ensureHidden(this); + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.UIObject#toString() + */ + @Override + public String toString() { + return nodeCaptionSpan.getInnerText(); + } + + /** + * Is the node focused? + * + * @param focused + * True if focused, false if not + */ + public void setFocused(boolean focused) { + if (!this.focused && focused) { + nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED); + + this.focused = focused; + if (BrowserInfo.get().isOpera()) { + nodeCaptionDiv.focus(); + } + treeHasFocus = true; + } else if (this.focused && !focused) { + nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED); + this.focused = focused; + treeHasFocus = false; + } + } + + /** + * Scrolls the caption into view + */ + public void scrollIntoView() { + WidgetUtil.scrollIntoViewVertically(nodeCaptionDiv); + } + + public void setIcon(String iconUrl, String altText) { + if (icon != null) { + DOM.getFirstChild(nodeCaptionDiv) + .removeChild(icon.getElement()); + } + icon = client.getIcon(iconUrl); + if (icon != null) { + DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv), + icon.getElement(), nodeCaptionSpan); + icon.setAlternateText(altText); + } + } + + public void setNodeStyleName(String styleName) { + addStyleName(TreeNode.CLASSNAME + "-" + styleName); + setStyleName(nodeCaptionDiv, + TreeNode.CLASSNAME + "-caption-" + styleName, true); + childNodeContainer.addStyleName( + TreeNode.CLASSNAME + "-children-" + styleName); + + } + + } + + @Override + public VDropHandler getDropHandler() { + return dropHandler; + } + + public TreeNode getNodeByKey(String key) { + return keyToNode.get(key); + } + + /** + * Deselects all items in the tree + */ + public void deselectAll() { + for (String key : selectedIds) { + TreeNode node = keyToNode.get(key); + if (node != null) { + node.setSelected(false); + } + } + selectedIds.clear(); + selectionHasChanged = true; + } + + /** + * Selects a range of nodes + * + * @param startNodeKey + * The start node key + * @param endNodeKey + * The end node key + */ + private void selectNodeRange(String startNodeKey, String endNodeKey) { + + TreeNode startNode = keyToNode.get(startNodeKey); + TreeNode endNode = keyToNode.get(endNodeKey); + + // The nodes have the same parent + if (startNode.getParent() == endNode.getParent()) { + doSiblingSelection(startNode, endNode); + + // The start node is a grandparent of the end node + } else if (startNode.isGrandParentOf(endNode)) { + doRelationSelection(startNode, endNode); + + // The end node is a grandparent of the start node + } else if (endNode.isGrandParentOf(startNode)) { + doRelationSelection(endNode, startNode); + + } else { + doNoRelationSelection(startNode, endNode); + } + } + + /** + * Selects a node and deselect all other nodes + * + * @param node + * The node to select + */ + private void selectNode(TreeNode node, boolean deselectPrevious) { + if (deselectPrevious) { + deselectAll(); + } + + if (node != null) { + node.setSelected(true); + selectedIds.add(node.key); + lastSelection = node; + } + selectionHasChanged = true; + } + + /** + * Deselects a node + * + * @param node + * The node to deselect + */ + private void deselectNode(TreeNode node) { + node.setSelected(false); + selectedIds.remove(node.key); + selectionHasChanged = true; + } + + /** + * Selects all the open children to a node + * + * @param node + * The parent node + */ + private void selectAllChildren(TreeNode node, boolean includeRootNode) { + if (includeRootNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + for (TreeNode child : node.getChildren()) { + if (!child.isLeaf() && child.getState()) { + selectAllChildren(child, true); + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + selectionHasChanged = true; + } + + /** + * Selects all children until a stop child is reached + * + * @param root + * The root not to start from + * @param stopNode + * The node to finish with + * @param includeRootNode + * Should the root node be selected + * @param includeStopNode + * Should the stop node be selected + * + * @return Returns false if the stop child was found, else true if all + * children was selected + */ + private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode, + boolean includeRootNode, boolean includeStopNode) { + if (includeRootNode) { + root.setSelected(true); + selectedIds.add(root.key); + } + if (root.getState() && root != stopNode) { + for (TreeNode child : root.getChildren()) { + if (!child.isLeaf() && child.getState() && child != stopNode) { + if (!selectAllChildrenUntil(child, stopNode, true, + includeStopNode)) { + return false; + } + } else if (child == stopNode) { + if (includeStopNode) { + child.setSelected(true); + selectedIds.add(child.key); + } + return false; + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + } + selectionHasChanged = true; + + return true; + } + + /** + * Select a range between two nodes which have no relation to each other + * + * @param startNode + * The start node to start the selection from + * @param endNode + * The end node to end the selection to + */ + private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) { + + TreeNode commonParent = getCommonGrandParent(startNode, endNode); + TreeNode startBranch = null, endBranch = null; + + // Find the children of the common parent + List<TreeNode> children; + if (commonParent != null) { + children = commonParent.getChildren(); + } else { + children = getRootNodes(); + } + + // Find the start and end branches + for (TreeNode node : children) { + if (nodeIsInBranch(startNode, node)) { + startBranch = node; + } + if (nodeIsInBranch(endNode, node)) { + endBranch = node; + } + } + + // Swap nodes if necessary + if (children.indexOf(startBranch) > children.indexOf(endBranch)) { + TreeNode temp = startBranch; + startBranch = endBranch; + endBranch = temp; + + temp = startNode; + startNode = endNode; + endNode = temp; + } + + // Select all children under the start node + selectAllChildren(startNode, true); + TreeNode startParent = startNode.getParentNode(); + TreeNode currentNode = startNode; + while (startParent != null && startParent != commonParent) { + List<TreeNode> startChildren = startParent.getChildren(); + for (int i = startChildren.indexOf(currentNode) + + 1; i < startChildren.size(); i++) { + selectAllChildren(startChildren.get(i), true); + } + + currentNode = startParent; + startParent = startParent.getParentNode(); + } + + // Select nodes until the end node is reached + for (int i = children.indexOf(startBranch) + 1; i <= children + .indexOf(endBranch); i++) { + selectAllChildrenUntil(children.get(i), endNode, true, true); + } + + // Ensure end node was selected + endNode.setSelected(true); + selectedIds.add(endNode.key); + selectionHasChanged = true; + } + + /** + * Examines the children of the branch node and returns true if a node is in + * that branch + * + * @param node + * The node to search for + * @param branch + * The branch to search in + * @return True if found, false if not found + */ + private boolean nodeIsInBranch(TreeNode node, TreeNode branch) { + if (node == branch) { + return true; + } + for (TreeNode child : branch.getChildren()) { + if (child == node) { + return true; + } + if (!child.isLeaf() && child.getState()) { + if (nodeIsInBranch(node, child)) { + return true; + } + } + } + return false; + } + + /** + * Selects a range of items which are in direct relation with each + * other.<br/> + * NOTE: The start node <b>MUST</b> be before the end node! + * + * @param startNode + * + * @param endNode + */ + private void doRelationSelection(TreeNode startNode, TreeNode endNode) { + TreeNode currentNode = endNode; + while (currentNode != startNode) { + currentNode.setSelected(true); + selectedIds.add(currentNode.key); + + // Traverse children above the selection + List<TreeNode> subChildren = currentNode.getParentNode() + .getChildren(); + if (subChildren.size() > 1) { + selectNodeRange(subChildren.iterator().next().key, + currentNode.key); + } else if (subChildren.size() == 1) { + TreeNode n = subChildren.get(0); + n.setSelected(true); + selectedIds.add(n.key); + } + + currentNode = currentNode.getParentNode(); + } + startNode.setSelected(true); + selectedIds.add(startNode.key); + selectionHasChanged = true; + } + + /** + * Selects a range of items which have the same parent. + * + * @param startNode + * The start node + * @param endNode + * The end node + */ + private void doSiblingSelection(TreeNode startNode, TreeNode endNode) { + TreeNode parent = startNode.getParentNode(); + + List<TreeNode> children; + if (parent == null) { + // Topmost parent + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + // Swap start and end point if needed + if (children.indexOf(startNode) > children.indexOf(endNode)) { + TreeNode temp = startNode; + startNode = endNode; + endNode = temp; + } + + Iterator<TreeNode> childIter = children.iterator(); + boolean startFound = false; + while (childIter.hasNext()) { + TreeNode node = childIter.next(); + if (node == startNode) { + startFound = true; + } + + if (startFound && node != endNode && node.getState()) { + selectAllChildren(node, true); + } else if (startFound && node != endNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + if (node == endNode) { + node.setSelected(true); + selectedIds.add(node.key); + break; + } + } + selectionHasChanged = true; + } + + /** + * Returns the first common parent of two nodes + * + * @param node1 + * The first node + * @param node2 + * The second node + * @return The common parent or null + */ + public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) { + // If either one does not have a grand parent then return null + if (node1.getParentNode() == null || node2.getParentNode() == null) { + return null; + } + + // If the nodes are parents of each other then return null + if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) { + return null; + } + + // Get parents of node1 + List<TreeNode> parents1 = new ArrayList<TreeNode>(); + TreeNode parent1 = node1.getParentNode(); + while (parent1 != null) { + parents1.add(parent1); + parent1 = parent1.getParentNode(); + } + + // Get parents of node2 + List<TreeNode> parents2 = new ArrayList<TreeNode>(); + TreeNode parent2 = node2.getParentNode(); + while (parent2 != null) { + parents2.add(parent2); + parent2 = parent2.getParentNode(); + } + + // Search the parents for the first common parent + for (int i = 0; i < parents1.size(); i++) { + parent1 = parents1.get(i); + for (int j = 0; j < parents2.size(); j++) { + parent2 = parents2.get(j); + if (parent1 == parent2) { + return parent1; + } + } + } + + return null; + } + + /** + * Sets the node currently in focus + * + * @param node + * The node to focus or null to remove the focus completely + * @param scrollIntoView + * Scroll the node into view + */ + public void setFocusedNode(TreeNode node, boolean scrollIntoView) { + // Unfocus previously focused node + if (focusedNode != null) { + focusedNode.setFocused(false); + + Roles.getTreeRole().removeAriaActivedescendantProperty( + focusedNode.getElement()); + } + + if (node != null) { + node.setFocused(true); + Roles.getTreeitemRole().setAriaSelectedState(node.getElement(), + SelectedValue.TRUE); + + /* + * FIXME: This code needs to be changed when the keyboard navigation + * doesn't immediately trigger a selection change anymore. + * + * Right now this function is called before and after the Tree is + * rebuilt when up/down arrow keys are pressed. This leads to the + * problem, that the newly selected item is announced too often with + * a screen reader. + * + * Behaviour is different when using the Tree with and without + * screen reader. + */ + if (node.key.equals(lastNodeKey)) { + Roles.getTreeRole().setAriaActivedescendantProperty( + getFocusElement(), Id.of(node.getElement())); + } else { + lastNodeKey = node.key; + } + } + + focusedNode = node; + + if (node != null && scrollIntoView) { + /* + * Delay scrolling the focused node into view if we are still + * rendering. #5396 + */ + if (!rendering) { + node.scrollIntoView(); + } else { + Scheduler.get().scheduleDeferred(new Command() { + @Override + public void execute() { + focusedNode.scrollIntoView(); + } + }); + } + } + } + + /** + * Focuses a node and scrolls it into view + * + * @param node + * The node to focus + */ + public void setFocusedNode(TreeNode node) { + setFocusedNode(node, true); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event + * .dom.client.FocusEvent) + */ + @Override + public void onFocus(FocusEvent event) { + treeHasFocus = true; + // If no node has focus, focus the first item in the tree + if (focusedNode == null && lastSelection == null && selectable) { + setFocusedNode(getFirstRootNode(), false); + } else if (focusedNode != null && selectable) { + setFocusedNode(focusedNode, false); + } else if (lastSelection != null && selectable) { + setFocusedNode(lastSelection, false); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event + * .dom.client.BlurEvent) + */ + @Override + public void onBlur(BlurEvent event) { + treeHasFocus = false; + if (focusedNode != null) { + focusedNode.setFocused(false); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google + * .gwt.event.dom.client.KeyPressEvent) + */ + @Override + public void onKeyPress(KeyPressEvent event) { + NativeEvent nativeEvent = event.getNativeEvent(); + int keyCode = nativeEvent.getKeyCode(); + if (keyCode == 0 && nativeEvent.getCharCode() == ' ') { + // Provide a keyCode for space to be compatible with FireFox + // keypress event + keyCode = CHARCODE_SPACE; + } + if (handleKeyNavigation(keyCode, + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt + * .event.dom.client.KeyDownEvent) + */ + @Override + public void onKeyDown(KeyDownEvent event) { + if (handleKeyNavigation(event.getNativeEvent().getKeyCode(), + event.isControlKeyDown() || event.isMetaKeyDown(), + event.isShiftKeyDown())) { + event.preventDefault(); + event.stopPropagation(); + } + } + + /** + * Handles the keyboard navigation + * + * @param keycode + * The keycode of the pressed key + * @param ctrl + * Was ctrl pressed + * @param shift + * Was shift pressed + * @return Returns true if the key was handled, else false + */ + protected boolean handleKeyNavigation(int keycode, boolean ctrl, + boolean shift) { + // Navigate down + if (keycode == getNavigationDownKey()) { + TreeNode node = null; + // If node is open and has children then move in to the children + if (!focusedNode.isLeaf() && focusedNode.getState() + && focusedNode.getChildren().size() > 0) { + node = focusedNode.getChildren().get(0); + } + + // Else move down to the next sibling + else { + node = getNextSibling(focusedNode); + if (node == null) { + // Else jump to the parent and try to select the next + // sibling there + TreeNode current = focusedNode; + while (node == null && current.getParentNode() != null) { + node = getNextSibling(current.getParentNode()); + current = current.getParentNode(); + } + } + } + + if (node != null) { + setFocusedNode(node); + if (selectable) { + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + showTooltipForKeyboardNavigation(node); + } + return true; + } + + // Navigate up + if (keycode == getNavigationUpKey()) { + TreeNode prev = getPreviousSibling(focusedNode); + TreeNode node = null; + if (prev != null) { + node = getLastVisibleChildInTree(prev); + } else if (focusedNode.getParentNode() != null) { + node = focusedNode.getParentNode(); + } + if (node != null) { + setFocusedNode(node); + if (selectable) { + if (!ctrl && !shift) { + selectNode(node, true); + } else if (shift && isMultiselect) { + deselectAll(); + selectNodeRange(lastSelection.key, node.key); + } else if (shift) { + selectNode(node, true); + } + } + showTooltipForKeyboardNavigation(node); + } + return true; + } + + // Navigate left (close branch) + if (keycode == getNavigationLeftKey()) { + if (!focusedNode.isLeaf() && focusedNode.getState()) { + focusedNode.setState(false, true); + } else if (focusedNode.getParentNode() != null + && (focusedNode.isLeaf() || !focusedNode.getState())) { + + if (ctrl || !selectable) { + setFocusedNode(focusedNode.getParentNode()); + } else if (shift) { + doRelationSelection(focusedNode.getParentNode(), + focusedNode); + setFocusedNode(focusedNode.getParentNode()); + } else { + focusAndSelectNode(focusedNode.getParentNode()); + } + } + showTooltipForKeyboardNavigation(focusedNode); + return true; + } + + // Navigate right (open branch) + if (keycode == getNavigationRightKey()) { + if (!focusedNode.isLeaf() && !focusedNode.getState()) { + focusedNode.setState(true, true); + } else if (!focusedNode.isLeaf()) { + if (ctrl || !selectable) { + setFocusedNode(focusedNode.getChildren().get(0)); + } else if (shift) { + setSelected(focusedNode, true); + setFocusedNode(focusedNode.getChildren().get(0)); + setSelected(focusedNode, true); + } else { + focusAndSelectNode(focusedNode.getChildren().get(0)); + } + } + showTooltipForKeyboardNavigation(focusedNode); + return true; + } + + // Selection + if (keycode == getNavigationSelectKey()) { + if (!focusedNode.isSelected()) { + selectNode(focusedNode, + (!isMultiselect + || multiSelectMode == MULTISELECT_MODE_SIMPLE) + && selectable); + } else { + deselectNode(focusedNode); + } + return true; + } + + // Home selection + if (keycode == getNavigationStartKey()) { + TreeNode node = getFirstRootNode(); + if (ctrl || !selectable) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } else { + selectNode(node, true); + } + sendSelectionToServer(); + showTooltipForKeyboardNavigation(node); + return true; + } + + // End selection + if (keycode == getNavigationEndKey()) { + TreeNode lastNode = getLastRootNode(); + TreeNode node = getLastVisibleChildInTree(lastNode); + if (ctrl || !selectable) { + setFocusedNode(node); + } else if (shift) { + deselectAll(); + selectNodeRange(focusedNode.key, node.key); + } else { + selectNode(node, true); + } + sendSelectionToServer(); + showTooltipForKeyboardNavigation(node); + return true; + } + + return false; + } + + private void showTooltipForKeyboardNavigation(TreeNode node) { + if (connector != null) { + getClient().getVTooltip().showAssistive( + connector.getTooltipInfo(node.nodeCaptionSpan)); + } + } + + private void focusAndSelectNode(TreeNode node) { + /* + * Keyboard navigation doesn't work reliably if the tree is in + * multiselect mode as well as isNullSelectionAllowed = false. It first + * tries to deselect the old focused node, which fails since there must + * be at least one selection. After this the newly focused node is + * selected and we've ended up with two selected nodes even though we + * only navigated with the arrow keys. + * + * Because of this, we first select the next node and later de-select + * the old one. + */ + TreeNode oldFocusedNode = focusedNode; + setFocusedNode(node); + setSelected(focusedNode, true); + setSelected(oldFocusedNode, false); + } + + /** + * Traverses the tree to the bottom most child + * + * @param root + * The root of the tree + * @return The bottom most child + */ + private TreeNode getLastVisibleChildInTree(TreeNode root) { + if (root.isLeaf() || !root.getState() + || root.getChildren().size() == 0) { + return root; + } + List<TreeNode> children = root.getChildren(); + return getLastVisibleChildInTree(children.get(children.size() - 1)); + } + + /** + * Gets the next sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the last sibling + */ + private TreeNode getNextSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx < children.size() - 1) { + return children.get(idx + 1); + } + + return null; + } + + /** + * Returns the previous sibling in the tree + * + * @param node + * The node to get the sibling for + * @return The sibling node or null if the node is the first sibling + */ + private TreeNode getPreviousSibling(TreeNode node) { + TreeNode parent = node.getParentNode(); + List<TreeNode> children; + if (parent == null) { + children = getRootNodes(); + } else { + children = parent.getChildren(); + } + + int idx = children.indexOf(node); + if (idx > 0) { + return children.get(idx - 1); + } + + return null; + } + + /** + * Add this to the element mouse down event by using element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again + * when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private native JavaScriptObject applyDisableTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + + /** + * Get the key that moves the selection head upwards. By default it is the + * up arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationUpKey() { + return KeyCodes.KEY_UP; + } + + /** + * Get the key that moves the selection head downwards. By default it is the + * down arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationDownKey() { + return KeyCodes.KEY_DOWN; + } + + /** + * Get the key that scrolls to the left in the table. By default it is the + * left arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationLeftKey() { + return KeyCodes.KEY_LEFT; + } + + /** + * Get the key that scroll to the right on the table. By default it is the + * right arrow key but by overriding this you can change the key to whatever + * you want. + * + * @return The keycode of the key + */ + protected int getNavigationRightKey() { + return KeyCodes.KEY_RIGHT; + } + + /** + * Get the key that selects an item in the table. By default it is the space + * bar key but by overriding this you can change the key to whatever you + * want. + * + * @return + */ + protected int getNavigationSelectKey() { + return CHARCODE_SPACE; + } + + /** + * Get the key the moves the selection one page up in the table. By default + * this is the Page Up key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationPageUpKey() { + return KeyCodes.KEY_PAGEUP; + } + + /** + * Get the key the moves the selection one page down in the table. By + * default this is the Page Down key but by overriding this you can change + * the key to whatever you want. + * + * @return + */ + protected int getNavigationPageDownKey() { + return KeyCodes.KEY_PAGEDOWN; + } + + /** + * Get the key the moves the selection to the beginning of the table. By + * default this is the Home key but by overriding this you can change the + * key to whatever you want. + * + * @return + */ + protected int getNavigationStartKey() { + return KeyCodes.KEY_HOME; + } + + /** + * Get the key the moves the selection to the end of the table. By default + * this is the End key but by overriding this you can change the key to + * whatever you want. + * + * @return + */ + protected int getNavigationEndKey() { + return KeyCodes.KEY_END; + } + + private final String SUBPART_NODE_PREFIX = "n"; + private final String EXPAND_IDENTIFIER = "expand"; + + /* + * In webkit, focus may have been requested for this component but not yet + * gained. Use this to trac if tree has gained the focus on webkit. See + * FocusImplSafari and #6373 + */ + private boolean treeHasFocus; + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.SubPartAware#getSubPartElement(java + * .lang.String) + */ + @Override + public com.google.gwt.user.client.Element getSubPartElement( + String subPart) { + if ("fe".equals(subPart)) { + if (BrowserInfo.get().isOpera() && focusedNode != null) { + return focusedNode.getElement(); + } + return getFocusElement(); + } + + if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) { + boolean expandCollapse = false; + + // Node + String[] nodes = subPart.split("/"); + TreeNode treeNode = null; + try { + for (String node : nodes) { + if (node.startsWith(SUBPART_NODE_PREFIX)) { + + // skip SUBPART_NODE_PREFIX"[" + node = node.substring(SUBPART_NODE_PREFIX.length() + 1); + // skip "]" + node = node.substring(0, node.length() - 1); + int position = Integer.parseInt(node); + if (treeNode == null) { + treeNode = getRootNodes().get(position); + } else { + treeNode = treeNode.getChildren().get(position); + } + } else if (node.startsWith(EXPAND_IDENTIFIER)) { + expandCollapse = true; + } + } + + if (expandCollapse) { + return treeNode.getElement(); + } else { + return DOM.asOld(treeNode.nodeCaptionSpan); + } + } catch (Exception e) { + // Invalid locator string or node could not be found + return null; + } + } + return null; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.SubPartAware#getSubPartName(com.google + * .gwt.user.client.Element) + */ + @Override + public String getSubPartName( + com.google.gwt.user.client.Element subElement) { + // Supported identifiers: + // + // n[index]/n[index]/n[index]{/expand} + // + // Ends with "/expand" if the target is expand/collapse indicator, + // otherwise ends with the node + + boolean isExpandCollapse = false; + + if (!getElement().isOrHasChild(subElement)) { + return null; + } + + if (subElement == getFocusElement()) { + return "fe"; + } + + TreeNode treeNode = WidgetUtil.findWidget(subElement, TreeNode.class); + if (treeNode == null) { + // Did not click on a node, let somebody else take care of the + // locator string + return null; + } + + if (subElement == treeNode.getElement()) { + // Targets expand/collapse arrow + isExpandCollapse = true; + } + + ArrayList<Integer> positions = new ArrayList<Integer>(); + while (treeNode.getParentNode() != null) { + positions.add(0, + treeNode.getParentNode().getChildren().indexOf(treeNode)); + treeNode = treeNode.getParentNode(); + } + positions.add(0, getRootNodes().indexOf(treeNode)); + + String locator = ""; + for (Integer i : positions) { + locator += SUBPART_NODE_PREFIX + "[" + i + "]/"; + } + + locator = locator.substring(0, locator.length() - 1); + if (isExpandCollapse) { + locator += "/" + EXPAND_IDENTIFIER; + } + return locator; + } + + @Override + public Action[] getActions() { + if (bodyActionKeys == null) { + return new Action[] {}; + } + final Action[] actions = new Action[bodyActionKeys.length]; + for (int i = 0; i < actions.length; i++) { + final String actionKey = bodyActionKeys[i]; + final TreeAction a = new TreeAction(this, null, actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + actions[i] = a; + } + return actions; + } + + @Override + public ApplicationConnection getClient() { + return client; + } + + @Override + public String getPaintableId() { + return paintableId; + } + + private void handleBodyContextMenu(ContextMenuEvent event) { + if (!readonly && !disabled) { + if (bodyActionKeys != null) { + int left = event.getNativeEvent().getClientX(); + int top = event.getNativeEvent().getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + client.getContextMenu().showAt(this, left, top); + } + event.stopPropagation(); + event.preventDefault(); + } + } + + public void registerAction(String key, String caption, String iconUrl) { + actionMap.put(key + "_c", caption); + if (iconUrl != null) { + actionMap.put(key + "_i", iconUrl); + } else { + actionMap.remove(key + "_i"); + } + + } + + public void registerNode(TreeNode treeNode) { + keyToNode.put(treeNode.key, treeNode); + } + + public void clearNodeToKeyMap() { + keyToNode.clear(); + } + + @Override + public void bindAriaCaption( + com.google.gwt.user.client.Element captionElement) { + AriaHelper.bindCaption(body, captionElement); + } + + /** + * Tell LayoutManager that a layout is needed later for this VTree + */ + private void doLayout() { + // This calls LayoutManager setNeedsMeasure and layoutNow + Util.notifyParentOfSizeChange(this, false); + } +} + diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java new file mode 100644 index 0000000000..f97ba9d14a --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java @@ -0,0 +1,713 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.Paintable; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.VConsole; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.Action; +import com.vaadin.client.ui.ActionOwner; +import com.vaadin.client.ui.SimpleManagedLayout; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.client.ui.VCalendar.BackwardListener; +import com.vaadin.client.ui.VCalendar.DateClickListener; +import com.vaadin.client.ui.VCalendar.EventClickListener; +import com.vaadin.client.ui.VCalendar.EventMovedListener; +import com.vaadin.client.ui.VCalendar.EventResizeListener; +import com.vaadin.client.ui.VCalendar.ForwardListener; +import com.vaadin.client.ui.VCalendar.MouseEventListener; +import com.vaadin.client.ui.VCalendar.RangeSelectListener; +import com.vaadin.client.ui.VCalendar.WeekClickListener; +import com.vaadin.client.ui.calendar.schedule.CalendarDay; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.client.ui.calendar.schedule.DateCell; +import com.vaadin.client.ui.calendar.schedule.DateCell.DateCellSlot; +import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent; +import com.vaadin.client.ui.calendar.schedule.DateUtil; +import com.vaadin.client.ui.calendar.schedule.HasTooltipKey; +import com.vaadin.client.ui.calendar.schedule.MonthEventLabel; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.calendar.schedule.dd.CalendarDropHandler; +import com.vaadin.client.ui.calendar.schedule.dd.CalendarMonthDropHandler; +import com.vaadin.client.ui.calendar.schedule.dd.CalendarWeekDropHandler; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.calendar.CalendarClientRpc; +import com.vaadin.shared.ui.calendar.CalendarEventId; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.CalendarState; +import com.vaadin.shared.ui.calendar.DateConstants; +import com.vaadin.ui.Calendar; + +/** + * Handles communication between Calendar on the server side and + * {@link VCalendar} on the client side. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +@Connect(value = Calendar.class, loadStyle = LoadStyle.LAZY) +public class CalendarConnector extends AbstractComponentConnector + implements ActionOwner, SimpleManagedLayout, Paintable { + + private CalendarServerRpc rpc = RpcProxy.create(CalendarServerRpc.class, + this); + + private final HashMap<String, String> actionMap = new HashMap<String, String>(); + private HashMap<Object, String> tooltips = new HashMap<Object, String>(); + + private static final String DROPHANDLER_ACCEPT_CRITERIA_PAINT_TAG = "-ac"; + + /** + * + */ + public CalendarConnector() { + + // Listen to events + registerListeners(); + } + + @Override + protected void init() { + super.init(); + registerRpc(CalendarClientRpc.class, new CalendarClientRpc() { + @Override + public void scroll(int scrollPosition) { + // TODO widget scroll + } + }); + getLayoutManager().registerDependency(this, getWidget().getElement()); + } + + @Override + public void onUnregister() { + super.onUnregister(); + getLayoutManager().unregisterDependency(this, getWidget().getElement()); + } + + @Override + public VCalendar getWidget() { + return (VCalendar) super.getWidget(); + } + + @Override + public CalendarState getState() { + return (CalendarState) super.getState(); + } + + /** + * Registers listeners on the calendar so server can be notified of the + * events + */ + protected void registerListeners() { + getWidget().setListener(new DateClickListener() { + @Override + public void dateClick(String date) { + if (!getWidget().isDisabled() + && hasEventListener(CalendarEventId.DATECLICK)) { + rpc.dateClick(date); + } + } + }); + getWidget().setListener(new ForwardListener() { + @Override + public void forward() { + if (hasEventListener(CalendarEventId.FORWARD)) { + rpc.forward(); + } + } + }); + getWidget().setListener(new BackwardListener() { + @Override + public void backward() { + if (hasEventListener(CalendarEventId.BACKWARD)) { + rpc.backward(); + } + } + }); + getWidget().setListener(new RangeSelectListener() { + @Override + public void rangeSelected(String value) { + if (hasEventListener(CalendarEventId.RANGESELECT)) { + rpc.rangeSelect(value); + } + } + }); + getWidget().setListener(new WeekClickListener() { + @Override + public void weekClick(String event) { + if (!getWidget().isDisabled() + && hasEventListener(CalendarEventId.WEEKCLICK)) { + rpc.weekClick(event); + } + } + }); + getWidget().setListener(new EventMovedListener() { + @Override + public void eventMoved(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTMOVE)) { + StringBuilder sb = new StringBuilder(); + sb.append(DateUtil.formatClientSideDate(event.getStart())); + sb.append("-"); + sb.append(DateUtil + .formatClientSideTime(event.getStartTime())); + rpc.eventMove(event.getIndex(), sb.toString()); + } + } + }); + getWidget().setListener(new EventResizeListener() { + @Override + public void eventResized(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTRESIZE)) { + StringBuilder buffer = new StringBuilder(); + + buffer.append( + DateUtil.formatClientSideDate(event.getStart())); + buffer.append("-"); + buffer.append(DateUtil + .formatClientSideTime(event.getStartTime())); + + String newStartDate = buffer.toString(); + + buffer = new StringBuilder(); + buffer.append( + DateUtil.formatClientSideDate(event.getEnd())); + buffer.append("-"); + buffer.append( + DateUtil.formatClientSideTime(event.getEndTime())); + + String newEndDate = buffer.toString(); + + rpc.eventResize(event.getIndex(), newStartDate, newEndDate); + } + } + }); + getWidget().setListener(new VCalendar.ScrollListener() { + @Override + public void scroll(int scrollPosition) { + // This call is @Delayed (== non-immediate) + rpc.scroll(scrollPosition); + } + }); + getWidget().setListener(new EventClickListener() { + @Override + public void eventClick(CalendarEvent event) { + if (hasEventListener(CalendarEventId.EVENTCLICK)) { + rpc.eventClick(event.getIndex()); + } + } + }); + getWidget().setListener(new MouseEventListener() { + @Override + public void contextMenu(ContextMenuEvent event, + final Widget widget) { + final NativeEvent ne = event.getNativeEvent(); + int left = ne.getClientX(); + int top = ne.getClientY(); + top += Window.getScrollTop(); + left += Window.getScrollLeft(); + getClient().getContextMenu().showAt(new ActionOwner() { + @Override + public String getPaintableId() { + return CalendarConnector.this.getPaintableId(); + } + + @Override + public ApplicationConnection getClient() { + return CalendarConnector.this.getClient(); + } + + @Override + @SuppressWarnings("deprecation") + public Action[] getActions() { + if (widget instanceof SimpleDayCell) { + /* + * Month view + */ + SimpleDayCell cell = (SimpleDayCell) widget; + Date start = new Date(cell.getDate().getYear(), + cell.getDate().getMonth(), + cell.getDate().getDate(), 0, 0, 0); + + Date end = new Date(cell.getDate().getYear(), + cell.getDate().getMonth(), + cell.getDate().getDate(), 23, 59, 59); + + return CalendarConnector.this + .getActionsBetween(start, end); + + } else if (widget instanceof MonthEventLabel) { + MonthEventLabel mel = (MonthEventLabel) widget; + CalendarEvent event = mel.getCalendarEvent(); + Action[] actions = CalendarConnector.this + .getActionsBetween(event.getStartTime(), + event.getEndTime()); + for (Action action : actions) { + ((VCalendarAction) action).setEvent(event); + } + return actions; + + } else if (widget instanceof DateCell) { + /* + * Week and Day view + */ + DateCell cell = (DateCell) widget; + int slotIndex = DOM.getChildIndex(cell.getElement(), + (Element) ne.getEventTarget().cast()); + DateCellSlot slot = cell.getSlot(slotIndex); + return CalendarConnector.this.getActionsBetween( + slot.getFrom(), slot.getTo()); + } else if (widget instanceof DateCellDayEvent) { + /* + * Context menu on event + */ + DateCellDayEvent dayEvent = (DateCellDayEvent) widget; + CalendarEvent event = dayEvent.getCalendarEvent(); + + Action[] actions = CalendarConnector.this + .getActionsBetween(event.getStartTime(), + event.getEndTime()); + + for (Action action : actions) { + ((VCalendarAction) action).setEvent(event); + } + + return actions; + } + return null; + } + }, left, top); + } + }); + } + + private boolean showingMonthView() { + return getState().days.size() > 7; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + CalendarState state = getState(); + VCalendar widget = getWidget(); + + // Enable or disable the forward and backward navigation buttons + widget.setForwardNavigationEnabled( + hasEventListener(CalendarEventId.FORWARD)); + widget.setBackwardNavigationEnabled( + hasEventListener(CalendarEventId.BACKWARD)); + + widget.set24HFormat(state.format24H); + widget.setDayNames(state.dayNames); + widget.setMonthNames(state.monthNames); + widget.setFirstDayNumber(state.firstVisibleDayOfWeek); + widget.setLastDayNumber(state.lastVisibleDayOfWeek); + widget.setFirstHourOfTheDay(state.firstHourOfDay); + widget.setLastHourOfTheDay(state.lastHourOfDay); + widget.setReadOnly(state.readOnly); + widget.setDisabled(!state.enabled); + + widget.setRangeSelectAllowed( + hasEventListener(CalendarEventId.RANGESELECT)); + widget.setRangeMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE)); + widget.setEventMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE)); + widget.setEventResizeAllowed( + hasEventListener(CalendarEventId.EVENTRESIZE)); + + widget.setEventCaptionAsHtml(state.eventCaptionAsHtml); + + List<CalendarState.Day> days = state.days; + List<CalendarState.Event> events = state.events; + + CalendarDropHandler dropHandler = getWidget().getDropHandler(); + if (showingMonthView()) { + updateMonthView(days, events); + if (dropHandler != null + && !(dropHandler instanceof CalendarMonthDropHandler)) { + getWidget().setDropHandler(new CalendarMonthDropHandler(this)); + } + } else { + updateWeekView(days, events); + if (dropHandler != null + && !(dropHandler instanceof CalendarWeekDropHandler)) { + getWidget().setDropHandler(new CalendarWeekDropHandler(this)); + } + } + + updateSizes(); + + registerEventToolTips(state.events); + updateActionMap(state.actions); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin. + * terminal .gwt.client.UIDL, + * com.vaadin.terminal.gwt.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + Iterator<Object> childIterator = uidl.getChildIterator(); + while (childIterator.hasNext()) { + UIDL child = (UIDL) childIterator.next(); + if (DROPHANDLER_ACCEPT_CRITERIA_PAINT_TAG.equals(child.getTag())) { + if (getWidget().getDropHandler() == null) { + getWidget().setDropHandler(showingMonthView() + ? new CalendarMonthDropHandler(this) + : new CalendarWeekDropHandler(this)); + } + getWidget().getDropHandler().updateAcceptRules(child); + } else { + getWidget().setDropHandler(null); + } + } + } + + /** + * Returns the ApplicationConnection used to connect to the server side + */ + @Override + public ApplicationConnection getClient() { + return getConnection(); + } + + /** + * Register the description of the events as tooltips. This way, any event + * displaying widget can use the event index as a key to display the + * tooltip. + */ + private void registerEventToolTips(List<CalendarState.Event> events) { + for (CalendarState.Event e : events) { + if (e.description != null && !"".equals(e.description)) { + tooltips.put(e.index, e.description); + } else { + tooltips.remove(e.index); + } + } + } + + @Override + public TooltipInfo getTooltipInfo( + com.google.gwt.dom.client.Element element) { + TooltipInfo tooltipInfo = null; + Widget w = WidgetUtil.findWidget(element, null); + if (w instanceof HasTooltipKey) { + tooltipInfo = GWT.create(TooltipInfo.class); + String title = tooltips.get(((HasTooltipKey) w).getTooltipKey()); + tooltipInfo.setTitle(title != null ? title : ""); + } + if (tooltipInfo == null) { + tooltipInfo = super.getTooltipInfo(element); + } + return tooltipInfo; + } + + @Override + public boolean hasTooltip() { + /* + * Tooltips are not processed until updateFromUIDL, so we can't be sure + * that there are no tooltips during onStateChange when this is used. + */ + return true; + } + + private void updateMonthView(List<CalendarState.Day> days, + List<CalendarState.Event> events) { + CalendarState state = getState(); + getWidget().updateMonthView(state.firstDayOfWeek, + getWidget().getDateTimeFormat().parse(state.now), days.size(), + calendarEventListOf(events, state.format24H), + calendarDayListOf(days)); + } + + private void updateWeekView(List<CalendarState.Day> days, + List<CalendarState.Event> events) { + CalendarState state = getState(); + getWidget().updateWeekView(state.scroll, + getWidget().getDateTimeFormat().parse(state.now), days.size(), + state.firstDayOfWeek, + calendarEventListOf(events, state.format24H), + calendarDayListOf(days)); + } + + private Action[] getActionsBetween(Date start, Date end) { + List<Action> actions = new ArrayList<Action>(); + List<String> ids = new ArrayList<String>(); + + for (int i = 0; i < actionKeys.size(); i++) { + String actionKey = actionKeys.get(i); + String id = getActionID(actionKey); + if (!ids.contains(id)) { + + Date actionStartDate; + Date actionEndDate; + try { + actionStartDate = getActionStartDate(actionKey); + actionEndDate = getActionEndDate(actionKey); + } catch (ParseException pe) { + VConsole.error("Failed to parse action date"); + continue; + } + + // Case 0: action inside event timeframe + // Action should start AFTER or AT THE SAME TIME as the event, + // and + // Action should end BEFORE or AT THE SAME TIME as the event + boolean test0 = actionStartDate.compareTo(start) >= 0 + && actionEndDate.compareTo(end) <= 0; + + // Case 1: action intersects start of timeframe + // Action end time must be between start and end of event + boolean test1 = actionEndDate.compareTo(start) > 0 + && actionEndDate.compareTo(end) <= 0; + + // Case 2: action intersects end of timeframe + // Action start time must be between start and end of event + boolean test2 = actionStartDate.compareTo(start) >= 0 + && actionStartDate.compareTo(end) < 0; + + // Case 3: event inside action timeframe + // Action should start AND END before the event is complete + boolean test3 = start.compareTo(actionStartDate) >= 0 + && end.compareTo(actionEndDate) <= 0; + + if (test0 || test1 || test2 || test3) { + VCalendarAction a = new VCalendarAction(this, rpc, + actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + a.setActionStartDate(start); + a.setActionEndDate(end); + actions.add(a); + ids.add(id); + } + } + } + + return actions.toArray(new Action[actions.size()]); + } + + private List<String> actionKeys = new ArrayList<String>(); + + private void updateActionMap(List<CalendarState.Action> actions) { + actionMap.clear(); + actionKeys.clear(); + + if (actions == null) { + return; + } + + for (CalendarState.Action action : actions) { + String id = action.actionKey + "-" + action.startDate + "-" + + action.endDate; + actionMap.put(id + "_k", action.actionKey); + actionMap.put(id + "_c", action.caption); + actionMap.put(id + "_s", action.startDate); + actionMap.put(id + "_e", action.endDate); + actionKeys.add(id); + if (action.iconKey != null) { + actionMap.put(id + "_i", getResourceUrl(action.iconKey)); + + } else { + actionMap.remove(id + "_i"); + } + } + + Collections.sort(actionKeys); + } + + /** + * Get the original action ID that was passed in from the shared state + * + * @since 7.1.2 + * @param actionKey + * the unique action key + * @return + */ + public String getActionID(String actionKey) { + return actionMap.get(actionKey + "_k"); + } + + /** + * Get the text that is displayed for a context menu item + * + * @param actionKey + * The unique action key + * @return + */ + public String getActionCaption(String actionKey) { + return actionMap.get(actionKey + "_c"); + } + + /** + * Get the icon url for a context menu item + * + * @param actionKey + * The unique action key + * @return + */ + public String getActionIcon(String actionKey) { + return actionMap.get(actionKey + "_i"); + } + + /** + * Get the start date for an action item + * + * @param actionKey + * The unique action key + * @return + * @throws ParseException + */ + public Date getActionStartDate(String actionKey) throws ParseException { + String dateStr = actionMap.get(actionKey + "_s"); + DateTimeFormat formatter = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + return formatter.parse(dateStr); + } + + /** + * Get the end date for an action item + * + * @param actionKey + * The unique action key + * @return + * @throws ParseException + */ + public Date getActionEndDate(String actionKey) throws ParseException { + String dateStr = actionMap.get(actionKey + "_e"); + DateTimeFormat formatter = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + return formatter.parse(dateStr); + } + + /** + * Returns ALL currently registered events. Use {@link #getActions(Date)} to + * get the actions for a specific date + */ + @Override + public Action[] getActions() { + List<Action> actions = new ArrayList<Action>(); + for (int i = 0; i < actionKeys.size(); i++) { + final String actionKey = actionKeys.get(i); + final VCalendarAction a = new VCalendarAction(this, rpc, actionKey); + a.setCaption(getActionCaption(actionKey)); + a.setIconUrl(getActionIcon(actionKey)); + + try { + a.setActionStartDate(getActionStartDate(actionKey)); + a.setActionEndDate(getActionEndDate(actionKey)); + } catch (ParseException pe) { + VConsole.error(pe); + } + + actions.add(a); + } + return actions.toArray(new Action[actions.size()]); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.ActionOwner#getPaintableId() + */ + @Override + public String getPaintableId() { + return getConnectorId(); + } + + private List<CalendarEvent> calendarEventListOf( + List<CalendarState.Event> events, boolean format24h) { + List<CalendarEvent> list = new ArrayList<CalendarEvent>(events.size()); + for (CalendarState.Event event : events) { + final String dateFrom = event.dateFrom; + final String dateTo = event.dateTo; + final String timeFrom = event.timeFrom; + final String timeTo = event.timeTo; + CalendarEvent calendarEvent = new CalendarEvent(); + calendarEvent.setAllDay(event.allDay); + calendarEvent.setCaption(event.caption); + calendarEvent.setDescription(event.description); + calendarEvent.setStart(getWidget().getDateFormat().parse(dateFrom)); + calendarEvent.setEnd(getWidget().getDateFormat().parse(dateTo)); + calendarEvent.setFormat24h(format24h); + calendarEvent.setStartTime(getWidget().getDateTimeFormat() + .parse(dateFrom + " " + timeFrom)); + calendarEvent.setEndTime(getWidget().getDateTimeFormat() + .parse(dateTo + " " + timeTo)); + calendarEvent.setStyleName(event.styleName); + calendarEvent.setIndex(event.index); + list.add(calendarEvent); + } + return list; + } + + private List<CalendarDay> calendarDayListOf(List<CalendarState.Day> days) { + List<CalendarDay> list = new ArrayList<CalendarDay>(days.size()); + for (CalendarState.Day day : days) { + CalendarDay d = new CalendarDay(day.date, day.localizedDateFormat, + day.dayOfWeek, day.week, day.yearOfWeek); + + list.add(d); + } + return list; + } + + @Override + public void layout() { + updateSizes(); + } + + private void updateSizes() { + int height = getLayoutManager() + .getOuterHeight(getWidget().getElement()); + int width = getLayoutManager().getOuterWidth(getWidget().getElement()); + + if (isUndefinedWidth()) { + width = -1; + } + if (isUndefinedHeight()) { + height = -1; + } + + getWidget().setSizeForChildren(width, height); + + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java new file mode 100644 index 0000000000..f16a43fbb2 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.client.ui.Action; +import com.vaadin.client.ui.calendar.schedule.CalendarEvent; +import com.vaadin.shared.ui.calendar.CalendarServerRpc; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Action performed by the calendar + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class VCalendarAction extends Action { + + private CalendarServerRpc rpc; + + private String actionKey = ""; + + private Date actionStartDate; + + private Date actionEndDate; + + private CalendarEvent event; + + private final DateTimeFormat dateformat_datetime = DateTimeFormat + .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN); + + /** + * + * @param owner + */ + public VCalendarAction(CalendarConnector owner) { + super(owner); + } + + /** + * Constructor + * + * @param owner + * The owner who trigger this kinds of events + * @param rpc + * The CalendarRpc which is used for executing actions + * @param key + * The unique action key which identifies this particular action + */ + public VCalendarAction(CalendarConnector owner, CalendarServerRpc rpc, + String key) { + this(owner); + this.rpc = rpc; + actionKey = key; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.Action#execute() + */ + @Override + public void execute() { + String startDate = dateformat_datetime.format(actionStartDate); + String endDate = dateformat_datetime.format(actionEndDate); + + if (event == null) { + rpc.actionOnEmptyCell(actionKey.split("-")[0], startDate, endDate); + } else { + rpc.actionOnEvent(actionKey.split("-")[0], startDate, endDate, + event.getIndex()); + } + + owner.getClient().getContextMenu().hide(); + } + + /** + * Get the date and time when the action starts + * + * @return + */ + public Date getActionStartDate() { + return actionStartDate; + } + + /** + * Set the date when the actions start + * + * @param actionStartDate + * The date and time when the action starts + */ + public void setActionStartDate(Date actionStartDate) { + this.actionStartDate = actionStartDate; + } + + /** + * Get the date and time when the action ends + * + * @return + */ + public Date getActionEndDate() { + return actionEndDate; + } + + /** + * Set the date and time when the action ends + * + * @param actionEndDate + * The date and time when the action ends + */ + public void setActionEndDate(Date actionEndDate) { + this.actionEndDate = actionEndDate; + } + + public CalendarEvent getEvent() { + return event; + } + + public void setEvent(CalendarEvent event) { + this.event = event; + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java new file mode 100644 index 0000000000..c2ade39a6d --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +/** + * Utility class used to represent a day when updating views. Only used + * internally. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarDay { + private String date; + private String localizedDateFormat; + private int dayOfWeek; + private int week; + private int yearOfWeek; + + public CalendarDay(String date, String localizedDateFormat, int dayOfWeek, + int week, int yearOfWeek) { + super(); + this.date = date; + this.localizedDateFormat = localizedDateFormat; + this.dayOfWeek = dayOfWeek; + this.week = week; + this.yearOfWeek = yearOfWeek; + } + + public String getDate() { + return date; + } + + public String getLocalizedDateFormat() { + return localizedDateFormat; + } + + public int getDayOfWeek() { + return dayOfWeek; + } + + public int getWeek() { + return week; + } + + public int getYearOfWeek() { + return yearOfWeek; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java new file mode 100644 index 0000000000..937b7c0ccb --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java @@ -0,0 +1,320 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * A client side implementation of a calendar event + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarEvent { + private int index; + private String caption; + private Date start, end; + private String styleName; + private Date startTime, endTime; + private String description; + private int slotIndex = -1; + private boolean format24h; + + DateTimeFormat dateformat_date = DateTimeFormat.getFormat("h:mm a"); + DateTimeFormat dateformat_date24 = DateTimeFormat.getFormat("H:mm"); + private boolean allDay; + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + */ + public String getStyleName() { + return styleName; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + */ + public Date getStart() { + return start; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName() + * @param style + */ + public void setStyleName(String style) { + styleName = style; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart() + * @param start + */ + public void setStart(Date start) { + this.start = start; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + * @return + */ + public Date getEnd() { + return end; + } + + /** + * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd() + * @param end + */ + public void setEnd(Date end) { + this.end = end; + } + + /** + * Returns the start time of the event + * + * @return Time embedded in the {@link Date} object + */ + public Date getStartTime() { + return startTime; + } + + /** + * Set the start time of the event + * + * @param startTime + * The time of the event. Use the time fields in the {@link Date} + * object + */ + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + /** + * Get the end time of the event + * + * @return Time embedded in the {@link Date} object + */ + public Date getEndTime() { + return endTime; + } + + /** + * Set the end time of the event + * + * @param endTime + * Time embedded in the {@link Date} object + */ + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + + /** + * Get the (server side) index of the event + * + * @return + */ + public int getIndex() { + return index; + } + + /** + * Get the index of the slot where the event in rendered + * + * @return + */ + public int getSlotIndex() { + return slotIndex; + } + + /** + * Set the index of the slot where the event in rendered + * + * @param index + * The index of the slot + */ + public void setSlotIndex(int index) { + slotIndex = index; + } + + /** + * Set the (server side) index of the event + * + * @param index + * The index + */ + public void setIndex(int index) { + this.index = index; + } + + /** + * Get the caption of the event. The caption is the text displayed in the + * calendar on the event. + * + * @return + */ + public String getCaption() { + return caption; + } + + /** + * Set the caption of the event. The caption is the text displayed in the + * calendar on the event. + * + * @param caption + * The visible caption of the event + */ + public void setCaption(String caption) { + this.caption = caption; + } + + /** + * Get the description of the event. The description is the text displayed + * when hoovering over the event with the mouse + * + * @return + */ + public String getDescription() { + return description; + } + + /** + * Set the description of the event. The description is the text displayed + * when hoovering over the event with the mouse + * + * @param description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Does the event use the 24h time format + * + * @param format24h + * True if it uses the 24h format, false if it uses the 12h time + * format + */ + public void setFormat24h(boolean format24h) { + this.format24h = format24h; + } + + /** + * Is the event an all day event. + * + * @param allDay + * True if the event should be rendered all day + */ + public void setAllDay(boolean allDay) { + this.allDay = allDay; + } + + /** + * Is the event an all day event. + * + * @return + */ + public boolean isAllDay() { + return allDay; + } + + /** + * Get the time as a formatted string + * + * @return + */ + public String getTimeAsText() { + if (format24h) { + return dateformat_date24.format(startTime); + } else { + return dateformat_date.format(startTime); + } + } + + /** + * Get the amount of milliseconds between the start and end of the event + * + * @return + */ + public long getRangeInMilliseconds() { + return getEndTime().getTime() - getStartTime().getTime(); + } + + /** + * Get the amount of minutes between the start and end of the event + * + * @return + */ + public long getRangeInMinutes() { + return (getRangeInMilliseconds() / DateConstants.MINUTEINMILLIS); + } + + /** + * Get the amount of minutes for the event on a specific day. This is useful + * if the event spans several days. + * + * @param targetDay + * The date to check + * @return + */ + public long getRangeInMinutesForDay(Date targetDay) { + long rangeInMinutesForDay = 0; + // we must take into account that here can be not only 1 and 2 days, but + // 1, 2, 3, 4... days first and last days - special cases all another + // days between first and last - have range "ALL DAY" + if (isTimeOnDifferentDays()) { + if (targetDay.compareTo(getStart()) == 0) { // for first day + rangeInMinutesForDay = DateConstants.DAYINMINUTES + - (getStartTime().getTime() - getStart().getTime()) + / DateConstants.MINUTEINMILLIS; + } else if (targetDay.compareTo(getEnd()) == 0) { // for last day + rangeInMinutesForDay = (getEndTime().getTime() + - getEnd().getTime()) / DateConstants.MINUTEINMILLIS; + } else { // for in-between days + rangeInMinutesForDay = DateConstants.DAYINMINUTES; + } + } else { // simple case - period is in one day + rangeInMinutesForDay = getRangeInMinutes(); + } + return rangeInMinutesForDay; + } + + /** + * Does the event span several days + * + * @return + */ + @SuppressWarnings("deprecation") + public boolean isTimeOnDifferentDays() { + boolean isSeveralDays = false; + + // if difference between start and end times is more than day - of + // course it is not one day, but several days + if (getEndTime().getTime() + - getStartTime().getTime() > DateConstants.DAYINMILLIS) { + isSeveralDays = true; + } else { // if difference <= day -> there can be different cases + if (getStart().compareTo(getEnd()) != 0 + && getEndTime().compareTo(getEnd()) != 0) { + isSeveralDays = true; + } + } + return isSeveralDays; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java new file mode 100644 index 0000000000..2590c4ed03 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java @@ -0,0 +1,850 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.WidgetUtil; + +public class DateCell extends FocusableComplexPanel implements MouseDownHandler, + MouseMoveHandler, MouseUpHandler, KeyDownHandler, ContextMenuHandler { + private static final String DRAGEMPHASISSTYLE = " dragemphasis"; + private Date date; + private int width; + private int eventRangeStart = -1; + private int eventRangeStop = -1; + final WeekGrid weekgrid; + private boolean disabled = false; + private int height; + private final Element[] slotElements; + private final List<DateCellSlot> slots = new ArrayList<DateCell.DateCellSlot>(); + private int[] slotElementHeights; + private int startingSlotHeight; + private Date today; + private Element todaybar; + private final List<HandlerRegistration> handlers; + private final int numberOfSlots; + private final int firstHour; + private final int lastHour; + + public class DateCellSlot extends Widget { + + private final DateCell cell; + + private final Date from; + + private final Date to; + + public DateCellSlot(DateCell cell, Date from, Date to) { + setElement(DOM.createDiv()); + getElement().setInnerHTML(" "); + this.cell = cell; + this.from = from; + this.to = to; + } + + public Date getFrom() { + return from; + } + + public Date getTo() { + return to; + } + + public DateCell getParentCell() { + return cell; + } + } + + public DateCell(WeekGrid parent, Date date) { + weekgrid = parent; + Element mainElement = DOM.createDiv(); + setElement(mainElement); + makeFocusable(); + setDate(date); + + addStyleName("v-calendar-day-times"); + + handlers = new LinkedList<HandlerRegistration>(); + + // 2 slots / hour + firstHour = weekgrid.getFirstHour(); + lastHour = weekgrid.getLastHour(); + numberOfSlots = (lastHour - firstHour + 1) * 2; + long slotTime = Math.round( + ((lastHour - firstHour + 1) * 3600000.0) / numberOfSlots); + + slotElements = new Element[numberOfSlots]; + slotElementHeights = new int[numberOfSlots]; + + slots.clear(); + long start = getDate().getTime() + firstHour * 3600000; + long end = start + slotTime; + for (int i = 0; i < numberOfSlots; i++) { + DateCellSlot slot = new DateCellSlot(this, new Date(start), + new Date(end)); + if (i % 2 == 0) { + slot.setStyleName("v-datecellslot-even"); + } else { + slot.setStyleName("v-datecellslot"); + } + Event.sinkEvents(slot.getElement(), Event.MOUSEEVENTS); + mainElement.appendChild(slot.getElement()); + slotElements[i] = slot.getElement(); + slots.add(slot); + start = end; + end = start + slotTime; + } + + // Sink events for tooltip handling + Event.sinkEvents(mainElement, Event.MOUSEEVENTS); + } + + public int getFirstHour() { + return firstHour; + } + + public int getLastHour() { + return lastHour; + } + + @Override + protected void onAttach() { + super.onAttach(); + + handlers.add(addHandler(this, MouseDownEvent.getType())); + handlers.add(addHandler(this, MouseUpEvent.getType())); + handlers.add(addHandler(this, MouseMoveEvent.getType())); + handlers.add(addDomHandler(this, ContextMenuEvent.getType())); + handlers.add(addKeyDownHandler(this)); + } + + @Override + protected void onDetach() { + for (HandlerRegistration handler : handlers) { + handler.removeHandler(); + } + handlers.clear(); + + super.onDetach(); + } + + public int getSlotIndex(Element slotElement) { + for (int i = 0; i < slotElements.length; i++) { + if (slotElement == slotElements[i]) { + return i; + } + } + + throw new IllegalArgumentException( + "Element not found in this DateCell"); + } + + public DateCellSlot getSlot(int index) { + return slots.get(index); + } + + public int getNumberOfSlots() { + return numberOfSlots; + } + + public void setTimeBarWidth(int timebarWidth) { + todaybar.getStyle().setWidth(timebarWidth, Unit.PX); + } + + /** + * @param isHorizontalSized + * if true, this DateCell is sized with CSS and not via + * {@link #setWidthPX(int)} + */ + public void setHorizontalSized(boolean isHorizontalSized) { + if (isHorizontalSized) { + addStyleDependentName("Hsized"); + + width = getOffsetWidth() + - WidgetUtil.measureHorizontalBorder(getElement()); + // Update moveWidth for any DateCellDayEvent child + updateEventCellsWidth(); + recalculateEventWidths(); + } else { + removeStyleDependentName("Hsized"); + } + } + + /** + * @param isVerticalSized + * if true, this DateCell is sized with CSS and not via + * {@link #setHeightPX(int)} + */ + public void setVerticalSized(boolean isVerticalSized) { + if (isVerticalSized) { + addStyleDependentName("Vsized"); + + // recalc heights&size for events. all other height sizes come + // from css + startingSlotHeight = slotElements[0].getOffsetHeight(); + // Update slotHeight for each DateCellDayEvent child + updateEventCellsHeight(); + recalculateEventPositions(); + + if (isToday()) { + recalculateTimeBarPosition(); + } + + } else { + removeStyleDependentName("Vsized"); + } + } + + public void setDate(Date date) { + this.date = date; + } + + public void setWidthPX(int cellWidth) { + width = cellWidth; + setWidth(cellWidth + "px"); + recalculateEventWidths(); + } + + public void setHeightPX(int height, int[] cellHeights) { + this.height = height; + slotElementHeights = cellHeights; + setHeight(height + "px"); + recalculateCellHeights(); + recalculateEventPositions(); + if (today != null) { + recalculateTimeBarPosition(); + } + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private void recalculateTimeBarPosition() { + int h = today.getHours(); + int m = today.getMinutes(); + if (h >= firstHour && h <= lastHour) { + int pixelTop = weekgrid.getPixelTopFor(m + 60 * h); + todaybar.getStyle().clearDisplay(); + todaybar.getStyle().setTop(pixelTop, Unit.PX); + } else { + todaybar.getStyle().setDisplay(Display.NONE); + } + } + + private void recalculateEventPositions() { + for (int i = 0; i < getWidgetCount(); i++) { + DateCellDayEvent dayEvent = (DateCellDayEvent) getWidget(i); + updatePositionFor(dayEvent, getDate(), dayEvent.getCalendarEvent()); + } + } + + public void recalculateEventWidths() { + List<DateCellGroup> groups = new ArrayList<DateCellGroup>(); + + int count = getWidgetCount(); + + List<Integer> handled = new ArrayList<Integer>(); + + // Iterate through all events and group them. Events that overlaps + // with each other, are added to the same group. + for (int i = 0; i < count; i++) { + if (handled.contains(i)) { + continue; + } + + DateCellGroup curGroup = getOverlappingEvents(i); + handled.addAll(curGroup.getItems()); + + boolean newGroup = true; + // No need to check other groups, if size equals the count + if (curGroup.getItems().size() != count) { + // Check other groups. When the whole group overlaps with + // other group, the group is merged to the other. + for (DateCellGroup g : groups) { + + if (WeekGridMinuteTimeRange.doesOverlap( + curGroup.getDateRange(), g.getDateRange())) { + newGroup = false; + updateGroup(g, curGroup); + } + } + } else { + if (newGroup) { + groups.add(curGroup); + } + break; + } + + if (newGroup) { + groups.add(curGroup); + } + } + + drawDayEvents(groups); + } + + private void recalculateCellHeights() { + startingSlotHeight = height / numberOfSlots; + + for (int i = 0; i < slotElements.length; i++) { + slotElements[i].getStyle().setHeight(slotElementHeights[i], + Unit.PX); + } + + updateEventCellsHeight(); + } + + public int getSlotHeight() { + return startingSlotHeight; + } + + public int getSlotBorder() { + return WidgetUtil.measureVerticalBorder(slotElements[0]); + } + + private void drawDayEvents(List<DateCellGroup> groups) { + for (DateCellGroup g : groups) { + int col = 0; + int colCount = 0; + List<Integer> order = new ArrayList<Integer>(); + Map<Integer, Integer> columns = new HashMap<Integer, Integer>(); + for (Integer eventIndex : g.getItems()) { + DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex); + d.setMoveWidth(width); + + int freeSpaceCol = findFreeColumnSpaceOnLeft( + new WeekGridMinuteTimeRange( + d.getCalendarEvent().getStartTime(), + d.getCalendarEvent().getEndTime()), + order, columns); + if (freeSpaceCol >= 0) { + col = freeSpaceCol; + columns.put(eventIndex, col); + int newOrderindex = 0; + for (Integer i : order) { + if (columns.get(i) >= col) { + newOrderindex = order.indexOf(i); + break; + } + } + order.add(newOrderindex, eventIndex); + } else { + // New column + col = colCount++; + columns.put(eventIndex, col); + order.add(eventIndex); + } + } + + // Update widths and left position + int eventWidth = (width / colCount); + for (Integer index : g.getItems()) { + DateCellDayEvent d = (DateCellDayEvent) getWidget(index); + d.getElement().getStyle().setMarginLeft( + (eventWidth * columns.get(index)), Unit.PX); + d.setWidth(eventWidth + "px"); + d.setSlotHeightInPX(getSlotHeight()); + } + } + } + + private int findFreeColumnSpaceOnLeft(WeekGridMinuteTimeRange dateRange, + List<Integer> order, Map<Integer, Integer> columns) { + int freeSpot = -1; + int skipIndex = -1; + for (Integer eventIndex : order) { + int col = columns.get(eventIndex); + if (col == skipIndex) { + continue; + } + + if (freeSpot != -1 && freeSpot != col) { + // Free spot found + return freeSpot; + } + + DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex); + WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange( + d.getCalendarEvent().getStartTime(), + d.getCalendarEvent().getEndTime()); + + if (WeekGridMinuteTimeRange.doesOverlap(dateRange, nextRange)) { + skipIndex = col; + freeSpot = -1; + } else { + freeSpot = col; + } + } + + return freeSpot; + } + + /* Update top and bottom date range values. Add new index to the group. */ + private void updateGroup(DateCellGroup targetGroup, DateCellGroup byGroup) { + Date newStart = targetGroup.getStart(); + Date newEnd = targetGroup.getEnd(); + if (byGroup.getStart().before(targetGroup.getStart())) { + newStart = byGroup.getEnd(); + } + if (byGroup.getStart().after(targetGroup.getEnd())) { + newStart = byGroup.getStart(); + } + + targetGroup.setDateRange(new WeekGridMinuteTimeRange(newStart, newEnd)); + + for (Integer index : byGroup.getItems()) { + if (!targetGroup.getItems().contains(index)) { + targetGroup.add(index); + } + } + } + + /** + * Returns all overlapping DayEvent indexes in the Group. Including the + * target. + * + * @param targetIndex + * Index of DayEvent in the current DateCell widget. + * @return Group that contains all Overlapping DayEvent indexes + */ + public DateCellGroup getOverlappingEvents(int targetIndex) { + DateCellGroup g = new DateCellGroup(targetIndex); + + int count = getWidgetCount(); + DateCellDayEvent target = (DateCellDayEvent) getWidget(targetIndex); + WeekGridMinuteTimeRange targetRange = new WeekGridMinuteTimeRange( + target.getCalendarEvent().getStartTime(), + target.getCalendarEvent().getEndTime()); + Date groupStart = targetRange.getStart(); + Date groupEnd = targetRange.getEnd(); + + for (int i = 0; i < count; i++) { + if (targetIndex == i) { + continue; + } + + DateCellDayEvent d = (DateCellDayEvent) getWidget(i); + WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange( + d.getCalendarEvent().getStartTime(), + d.getCalendarEvent().getEndTime()); + if (WeekGridMinuteTimeRange.doesOverlap(targetRange, nextRange)) { + g.add(i); + + // Update top & bottom values to the greatest + if (nextRange.getStart().before(targetRange.getStart())) { + groupStart = targetRange.getStart(); + } + if (nextRange.getEnd().after(targetRange.getEnd())) { + groupEnd = targetRange.getEnd(); + } + } + } + + g.setDateRange(new WeekGridMinuteTimeRange(groupStart, groupEnd)); + return g; + } + + public Date getDate() { + return date; + } + + public void addEvent(Date targetDay, CalendarEvent calendarEvent) { + Element main = getElement(); + DateCellDayEvent dayEvent = new DateCellDayEvent(this, weekgrid, + calendarEvent); + dayEvent.setSlotHeightInPX(getSlotHeight()); + dayEvent.setDisabled(isDisabled()); + + if (startingSlotHeight > 0) { + updatePositionFor(dayEvent, targetDay, calendarEvent); + } + + add(dayEvent, main); + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private void updatePositionFor(DateCellDayEvent dayEvent, Date targetDay, + CalendarEvent calendarEvent) { + + if (shouldDisplay(calendarEvent)) { + dayEvent.getElement().getStyle().clearDisplay(); + + Date fromDt = calendarEvent.getStartTime(); + int h = fromDt.getHours(); + int m = fromDt.getMinutes(); + long range = calendarEvent.getRangeInMinutesForDay(targetDay); + + boolean onDifferentDays = calendarEvent.isTimeOnDifferentDays(); + if (onDifferentDays) { + if (calendarEvent.getStart().compareTo(targetDay) != 0) { + // Current day slot is for the end date and all in-between + // days. Lets fix also the start & end times. + h = 0; + m = 0; + } + } + + int startFromMinutes = (h * 60) + m; + dayEvent.updatePosition(startFromMinutes, range); + } else { + dayEvent.getElement().getStyle().setDisplay(Display.NONE); + } + } + + public void addEvent(DateCellDayEvent dayEvent) { + Element main = getElement(); + int index = 0; + List<CalendarEvent> events = new ArrayList<CalendarEvent>(); + + // events are the only widgets in this panel + // slots are just elements + for (; index < getWidgetCount(); index++) { + DateCellDayEvent dc = (DateCellDayEvent) getWidget(index); + dc.setDisabled(isDisabled()); + events.add(dc.getCalendarEvent()); + } + events.add(dayEvent.getCalendarEvent()); + + index = 0; + for (CalendarEvent e : weekgrid.getCalendar() + .sortEventsByDuration(events)) { + if (e.equals(dayEvent.getCalendarEvent())) { + break; + } + index++; + } + this.insert(dayEvent, main, index, true); + } + + public void removeEvent(DateCellDayEvent dayEvent) { + remove(dayEvent); + } + + /** + * + * @param event + * @return + * + * This method is not necessary in the long run.. Or here can be + * various types of implementations.. + */ + // Date methods not deprecated in GWT + @SuppressWarnings("deprecation") + private boolean shouldDisplay(CalendarEvent event) { + boolean display = true; + if (event.isTimeOnDifferentDays()) { + display = true; + } else { // only in case of one-day event we are able not to display + // event + // which is placed in unpublished parts on calendar + Date eventStart = event.getStartTime(); + Date eventEnd = event.getEndTime(); + + int eventStartHours = eventStart.getHours(); + int eventEndHours = eventEnd.getHours(); + + display = !(eventEndHours < firstHour + || eventStartHours > lastHour); + } + return display; + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode == KeyCodes.KEY_ESCAPE && eventRangeStart > -1) { + cancelRangeSelect(); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) { + Element e = Element.as(event.getNativeEvent().getEventTarget()); + if (e.getClassName().contains("reserved") || isDisabled() + || !weekgrid.getParentCalendar().isRangeSelectAllowed()) { + eventRangeStart = -1; + } else { + eventRangeStart = event.getY(); + eventRangeStop = eventRangeStart; + Event.setCapture(getElement()); + setFocus(true); + } + } + } + + @Override + @SuppressWarnings("deprecation") + public void onMouseUp(MouseUpEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + Event.releaseCapture(getElement()); + setFocus(false); + int dragDistance = Math.abs(eventRangeStart - event.getY()); + if (dragDistance > 0 && eventRangeStart >= 0) { + Element main = getElement(); + if (eventRangeStart > eventRangeStop) { + if (eventRangeStop <= -1) { + eventRangeStop = 0; + } + int temp = eventRangeStart; + eventRangeStart = eventRangeStop; + eventRangeStop = temp; + } + + NodeList<Node> nodes = main.getChildNodes(); + + int slotStart = -1; + int slotEnd = -1; + + // iterate over all child nodes, until we find first the start, + // and then the end + for (int i = 0; i < nodes.getLength(); i++) { + Element element = (Element) nodes.getItem(i); + boolean isRangeElement = element.getClassName() + .contains("v-daterange"); + + if (isRangeElement && slotStart == -1) { + slotStart = i; + slotEnd = i; // to catch one-slot selections + + } else if (isRangeElement) { + slotEnd = i; + + } else if (slotStart != -1 && slotEnd != -1) { + break; + } + } + + clearSelectionRange(); + + int startMinutes = firstHour * 60 + slotStart * 30; + int endMinutes = (firstHour * 60) + (slotEnd + 1) * 30; + Date currentDate = getDate(); + String yr = (currentDate.getYear() + 1900) + "-" + + (currentDate.getMonth() + 1) + "-" + + currentDate.getDate(); + if (weekgrid.getCalendar().getRangeSelectListener() != null) { + weekgrid.getCalendar().getRangeSelectListener().rangeSelected( + yr + ":" + startMinutes + ":" + endMinutes); + } + eventRangeStart = -1; + } else { + // Click event + eventRangeStart = -1; + cancelRangeSelect(); + + } + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + if (eventRangeStart >= 0) { + int newY = event.getY(); + int fromY = 0; + int toY = 0; + if (newY < eventRangeStart) { + fromY = newY; + toY = eventRangeStart; + } else { + fromY = eventRangeStart; + toY = newY; + } + Element main = getElement(); + eventRangeStop = newY; + NodeList<Node> nodes = main.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + Element c = (Element) nodes.getItem(i); + + if (todaybar != c) { + + int elemStart = c.getOffsetTop(); + int elemStop = elemStart + getSlotHeight(); + if (elemStart >= fromY && elemStart <= toY) { + c.addClassName("v-daterange"); + } else if (elemStop >= fromY && elemStop <= toY) { + c.addClassName("v-daterange"); + } else if (elemStop >= fromY && elemStart <= toY) { + c.addClassName("v-daterange"); + } else { + c.removeClassName("v-daterange"); + } + } + } + } + + event.preventDefault(); + } + + public void cancelRangeSelect() { + Event.releaseCapture(getElement()); + setFocus(false); + + clearSelectionRange(); + } + + private void clearSelectionRange() { + if (eventRangeStart > -1) { + // clear all "selected" class names + Element main = getElement(); + NodeList<Node> nodes = main.getChildNodes(); + + for (int i = 0; i <= 47; i++) { + Element c = (Element) nodes.getItem(i); + if (c == null) { + continue; + } + c.removeClassName("v-daterange"); + } + + eventRangeStart = -1; + } + } + + public void setToday(Date today, int width) { + this.today = today; + addStyleDependentName("today"); + Element lastChild = (Element) getElement().getLastChild(); + if (lastChild.getClassName().equals("v-calendar-current-time")) { + todaybar = lastChild; + } else { + todaybar = DOM.createDiv(); + todaybar.setClassName("v-calendar-current-time"); + getElement().appendChild(todaybar); + } + + if (width != -1) { + todaybar.getStyle().setWidth(width, Unit.PX); + } + + // position is calculated later, when we know the cell heights + } + + public Element getTodaybarElement() { + return todaybar; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + public void setDateColor(String styleName) { + this.setStyleName("v-calendar-datecell " + styleName); + } + + public boolean isToday() { + return today != null; + } + + /** + * @deprecated As of 7.2, call or override + * {@link #addEmphasisStyle(Element)} instead + */ + @Deprecated + public void addEmphasisStyle( + com.google.gwt.user.client.Element elementOver) { + String originalStylename = getStyleName(elementOver); + setStyleName(elementOver, originalStylename + DRAGEMPHASISSTYLE); + } + + /** + * @since 7.2 + */ + public void addEmphasisStyle(Element elementOver) { + addEmphasisStyle(DOM.asOld(elementOver)); + } + + /** + * @deprecated As of 7.2, call or override + * {@link #removeEmphasisStyle(Element)} instead + */ + @Deprecated + public void removeEmphasisStyle( + com.google.gwt.user.client.Element elementOver) { + String originalStylename = getStyleName(elementOver); + setStyleName(elementOver, originalStylename.substring(0, + originalStylename.length() - DRAGEMPHASISSTYLE.length())); + } + + /** + * @since 7.2 + */ + public void removeEmphasisStyle(Element elementOver) { + removeEmphasisStyle(DOM.asOld(elementOver)); + } + + @Override + public void onContextMenu(ContextMenuEvent event) { + if (weekgrid.getCalendar().getMouseEventListener() != null) { + event.preventDefault(); + event.stopPropagation(); + weekgrid.getCalendar().getMouseEventListener().contextMenu(event, + DateCell.this); + } + } + + private void updateEventCellsWidth() { + for (Widget widget : getChildren()) { + if (widget instanceof DateCellDayEvent) { + ((DateCellDayEvent) widget).setMoveWidth(width); + } + } + } + + private void updateEventCellsHeight() { + for (Widget widget : getChildren()) { + if (widget instanceof DateCellDayEvent) { + ((DateCellDayEvent) widget).setSlotHeightInPX(getSlotHeight()); + } + } + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java new file mode 100644 index 0000000000..92c39c0791 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java @@ -0,0 +1,117 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VCalendar; + +/** + * Internally used class by the Calendar + * + * since 7.1 + */ +public class DateCellContainer extends FlowPanel + implements MouseDownHandler, MouseUpHandler { + + private Date date; + + private Widget clickTargetWidget; + + private VCalendar calendar; + + private static int borderWidth = -1; + + public DateCellContainer() { + setStylePrimaryName("v-calendar-datecell"); + } + + public static int measureBorderWidth(DateCellContainer dc) { + if (borderWidth == -1) { + borderWidth = WidgetUtil.measureHorizontalBorder(dc.getElement()); + } + return borderWidth; + } + + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + public boolean hasEvent(int slotIndex) { + return hasDateCell(slotIndex) + && ((WeeklyLongEventsDateCell) getChildren().get(slotIndex)) + .getEvent() != null; + } + + public boolean hasDateCell(int slotIndex) { + return (getChildren().size() - 1) >= slotIndex; + } + + public WeeklyLongEventsDateCell getDateCell(int slotIndex) { + if (!hasDateCell(slotIndex)) { + addEmptyEventCells(slotIndex - (getChildren().size() - 1)); + } + return (WeeklyLongEventsDateCell) getChildren().get(slotIndex); + } + + public void addEmptyEventCells(int eventCount) { + for (int i = 0; i < eventCount; i++) { + addEmptyEventCell(); + } + } + + public void addEmptyEventCell() { + WeeklyLongEventsDateCell dateCell = new WeeklyLongEventsDateCell(); + dateCell.addMouseDownHandler(this); + dateCell.addMouseUpHandler(this); + add(dateCell); + } + + @Override + public void onMouseDown(MouseDownEvent event) { + clickTargetWidget = (Widget) event.getSource(); + + event.stopPropagation(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (event.getSource() == clickTargetWidget + && clickTargetWidget instanceof WeeklyLongEventsDateCell + && !calendar.isDisabledOrReadOnly()) { + CalendarEvent calendarEvent = ((WeeklyLongEventsDateCell) clickTargetWidget) + .getEvent(); + if (calendar.getEventClickListener() != null) { + calendar.getEventClickListener().eventClick(calendarEvent); + } + } + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java new file mode 100644 index 0000000000..7404f557a8 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java @@ -0,0 +1,664 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.EventTarget; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ContextMenuEvent; +import com.google.gwt.event.dom.client.ContextMenuHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.vaadin.client.WidgetUtil; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class DateCellDayEvent extends FocusableHTML + implements MouseDownHandler, MouseUpHandler, MouseMoveHandler, + KeyDownHandler, ContextMenuHandler, HasTooltipKey { + + private final DateCell dateCell; + private Element caption = null; + private final Element eventContent; + private CalendarEvent calendarEvent = null; + private HandlerRegistration moveRegistration; + private int startY = -1; + private int startX = -1; + private String moveWidth; + public static final int halfHourInMilliSeconds = 1800 * 1000; + private Date startDatetimeFrom; + private Date startDatetimeTo; + private boolean mouseMoveStarted; + private int top; + private int startYrelative; + private int startXrelative; + private boolean disabled; + private final WeekGrid weekGrid; + private Element topResizeBar; + private Element bottomResizeBar; + private Element clickTarget; + private final Integer eventIndex; + private int slotHeight; + private final List<HandlerRegistration> handlers; + private boolean mouseMoveCanceled; + + public DateCellDayEvent(DateCell dateCell, WeekGrid parent, + CalendarEvent event) { + super(); + this.dateCell = dateCell; + + handlers = new LinkedList<HandlerRegistration>(); + + setStylePrimaryName("v-calendar-event"); + setCalendarEvent(event); + + weekGrid = parent; + + Style s = getElement().getStyle(); + if (event.getStyleName().length() > 0) { + addStyleDependentName(event.getStyleName()); + } + s.setPosition(Position.ABSOLUTE); + + caption = DOM.createDiv(); + caption.addClassName("v-calendar-event-caption"); + getElement().appendChild(caption); + + eventContent = DOM.createDiv(); + eventContent.addClassName("v-calendar-event-content"); + getElement().appendChild(eventContent); + + if (weekGrid.getCalendar().isEventResizeAllowed()) { + topResizeBar = DOM.createDiv(); + bottomResizeBar = DOM.createDiv(); + + topResizeBar.addClassName("v-calendar-event-resizetop"); + bottomResizeBar.addClassName("v-calendar-event-resizebottom"); + + getElement().appendChild(topResizeBar); + getElement().appendChild(bottomResizeBar); + } + + eventIndex = event.getIndex(); + } + + @Override + protected void onAttach() { + super.onAttach(); + handlers.add(addMouseDownHandler(this)); + handlers.add(addMouseUpHandler(this)); + handlers.add(addKeyDownHandler(this)); + handlers.add(addDomHandler(this, ContextMenuEvent.getType())); + } + + @Override + protected void onDetach() { + for (HandlerRegistration handler : handlers) { + handler.removeHandler(); + } + handlers.clear(); + super.onDetach(); + } + + public void setSlotHeightInPX(int slotHeight) { + this.slotHeight = slotHeight; + } + + public void updatePosition(long startFromMinutes, long durationInMinutes) { + if (startFromMinutes < 0) { + startFromMinutes = 0; + } + top = weekGrid.getPixelTopFor((int) startFromMinutes); + + getElement().getStyle().setTop(top, Unit.PX); + if (durationInMinutes > 0) { + int heightMinutes = weekGrid.getPixelLengthFor( + (int) startFromMinutes, (int) durationInMinutes); + setHeight(heightMinutes); + } else { + setHeight(-1); + } + + boolean multiRowCaption = (durationInMinutes > 30); + updateCaptions(multiRowCaption); + } + + public int getTop() { + return top; + } + + public void setMoveWidth(int width) { + moveWidth = width + "px"; + } + + public void setHeight(int h) { + if (h == -1) { + getElement().getStyle().setProperty("height", ""); + eventContent.getStyle().setProperty("height", ""); + } else { + getElement().getStyle().setHeight(h, Unit.PX); + // FIXME measure the border height (2px) from the DOM + eventContent.getStyle().setHeight(h - 2, Unit.PX); + } + } + + /** + * @param bigMode + * If false, event is so small that caption must be in time-row + */ + private void updateCaptions(boolean bigMode) { + String innerHtml; + String timeAsText = calendarEvent.getTimeAsText(); + String htmlOrText; + + if (dateCell.weekgrid.getCalendar().isEventCaptionAsHtml()) { + htmlOrText = calendarEvent.getCaption(); + } else { + htmlOrText = WidgetUtil.escapeHTML(calendarEvent.getCaption()); + } + + if (bigMode) { + innerHtml = "<span>" + timeAsText + "</span><br />" + htmlOrText; + } else { + innerHtml = "<span>" + timeAsText + "<span>:</span></span> " + + htmlOrText; + } + caption.setInnerHTML(innerHtml); + eventContent.setInnerHTML(""); + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeEvent().getKeyCode(); + if (keycode == KeyCodes.KEY_ESCAPE && mouseMoveStarted) { + cancelMouseMove(); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + startX = event.getClientX(); + startY = event.getClientY(); + if (isDisabled() + || event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + clickTarget = Element.as(event.getNativeEvent().getEventTarget()); + mouseMoveCanceled = false; + + if (weekGrid.getCalendar().isEventMoveAllowed() + || clickTargetsResize()) { + moveRegistration = addMouseMoveHandler(this); + setFocus(true); + try { + startYrelative = (int) ((double) event.getRelativeY(caption) + % slotHeight); + startXrelative = (event.getRelativeX(weekGrid.getElement()) + - weekGrid.timebar.getOffsetWidth()) + % getDateCellWidth(); + } catch (Exception e) { + GWT.log("Exception calculating relative start position", e); + } + mouseMoveStarted = false; + Style s = getElement().getStyle(); + s.setZIndex(1000); + startDatetimeFrom = (Date) calendarEvent.getStartTime().clone(); + startDatetimeTo = (Date) calendarEvent.getEndTime().clone(); + Event.setCapture(getElement()); + } + + // make sure the right cursor is always displayed + if (clickTargetsResize()) { + addGlobalResizeStyle(); + } + + /* + * We need to stop the event propagation or else the WeekGrid range + * select will kick in + */ + event.stopPropagation(); + event.preventDefault(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (mouseMoveCanceled + || event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + Event.releaseCapture(getElement()); + setFocus(false); + if (moveRegistration != null) { + moveRegistration.removeHandler(); + moveRegistration = null; + } + int endX = event.getClientX(); + int endY = event.getClientY(); + int xDiff = 0, yDiff = 0; + if (startX != -1 && startY != -1) { + // Drag started + xDiff = startX - endX; + yDiff = startY - endY; + } + + startX = -1; + startY = -1; + mouseMoveStarted = false; + Style s = getElement().getStyle(); + s.setZIndex(1); + if (!clickTargetsResize()) { + // check if mouse has moved over threshold of 3 pixels + boolean mouseMoved = (xDiff < -3 || xDiff > 3 || yDiff < -3 + || yDiff > 3); + + if (!weekGrid.getCalendar().isDisabledOrReadOnly() && mouseMoved) { + // Event Move: + // - calendar must be enabled + // - calendar must not be in read-only mode + weekGrid.eventMoved(this); + } else if (!weekGrid.getCalendar().isDisabled()) { + // Event Click: + // - calendar must be enabled (read-only is allowed) + EventTarget et = event.getNativeEvent().getEventTarget(); + Element e = Element.as(et); + if (e == caption || e == eventContent + || e.getParentElement() == caption) { + if (weekGrid.getCalendar() + .getEventClickListener() != null) { + weekGrid.getCalendar().getEventClickListener() + .eventClick(calendarEvent); + } + } + } + + } else { // click targeted resize bar + removeGlobalResizeStyle(); + if (weekGrid.getCalendar().getEventResizeListener() != null) { + weekGrid.getCalendar().getEventResizeListener() + .eventResized(calendarEvent); + } + dateCell.recalculateEventWidths(); + } + } + + @Override + @SuppressWarnings("deprecation") + public void onMouseMove(MouseMoveEvent event) { + if (startY < 0 && startX < 0) { + return; + } + if (isDisabled()) { + Event.releaseCapture(getElement()); + mouseMoveStarted = false; + startY = -1; + startX = -1; + removeGlobalResizeStyle(); + return; + } + int currentY = event.getClientY(); + int currentX = event.getClientX(); + int moveY = (currentY - startY); + int moveX = (currentX - startX); + if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) { + return; + } + if (!mouseMoveStarted) { + setWidth(moveWidth); + getElement().getStyle().setMarginLeft(0, Unit.PX); + mouseMoveStarted = true; + } + + HorizontalPanel parent = (HorizontalPanel) getParent().getParent(); + int relativeX = event.getRelativeX(parent.getElement()) + - weekGrid.timebar.getOffsetWidth(); + int halfHourDiff = 0; + if (moveY > 0) { + halfHourDiff = (startYrelative + moveY) / slotHeight; + } else { + halfHourDiff = (moveY - startYrelative) / slotHeight; + } + + int dateCellWidth = getDateCellWidth(); + long dayDiff = 0; + if (moveX >= 0) { + dayDiff = (startXrelative + moveX) / dateCellWidth; + } else { + dayDiff = (moveX - (dateCellWidth - startXrelative)) + / dateCellWidth; + } + + int dayOffset = relativeX / dateCellWidth; + + // sanity check for right side overflow + int dateCellCount = weekGrid.getDateCellCount(); + if (dayOffset >= dateCellCount) { + dayOffset--; + dayDiff--; + } + + int dayOffsetPx = calculateDateCellOffsetPx(dayOffset) + + weekGrid.timebar.getOffsetWidth(); + + GWT.log("DateCellWidth: " + dateCellWidth + " dayDiff: " + dayDiff + + " dayOffset: " + dayOffset + " dayOffsetPx: " + dayOffsetPx + + " startXrelative: " + startXrelative + " moveX: " + moveX); + + if (relativeX < 0 || relativeX >= getDatesWidth()) { + return; + } + + Style s = getElement().getStyle(); + + Date from = calendarEvent.getStartTime(); + Date to = calendarEvent.getEndTime(); + long duration = to.getTime() - from.getTime(); + + if (!clickTargetsResize() + && weekGrid.getCalendar().isEventMoveAllowed()) { + long daysMs = dayDiff * DateConstants.DAYINMILLIS; + from.setTime(startDatetimeFrom.getTime() + daysMs); + from.setTime(from.getTime() + + ((long) halfHourInMilliSeconds * halfHourDiff)); + to.setTime((from.getTime() + duration)); + + calendarEvent.setStartTime(from); + calendarEvent.setEndTime(to); + calendarEvent.setStart(new Date(from.getTime())); + calendarEvent.setEnd(new Date(to.getTime())); + + // Set new position for the event + long startFromMinutes = (from.getHours() * 60) + from.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + startFromMinutes = calculateStartFromMinute(startFromMinutes, from, + to, dayOffsetPx); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + updatePosition(startFromMinutes, range); + + s.setLeft(dayOffsetPx, Unit.PX); + + if (weekGrid.getDateCellWidths() != null) { + s.setWidth(weekGrid.getDateCellWidths()[dayOffset], Unit.PX); + } else { + setWidth(moveWidth); + } + + } else if (clickTarget == topResizeBar) { + long oldStartTime = startDatetimeFrom.getTime(); + long newStartTime = oldStartTime + + ((long) halfHourInMilliSeconds * halfHourDiff); + + if (!isTimeRangeTooSmall(newStartTime, startDatetimeTo.getTime())) { + newStartTime = startDatetimeTo.getTime() - getMinTimeRange(); + } + + from.setTime(newStartTime); + + calendarEvent.setStartTime(from); + calendarEvent.setStart(new Date(from.getTime())); + + // Set new position for the event + long startFromMinutes = (from.getHours() * 60) + from.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + + updatePosition(startFromMinutes, range); + + } else if (clickTarget == bottomResizeBar) { + long oldEndTime = startDatetimeTo.getTime(); + long newEndTime = oldEndTime + + ((long) halfHourInMilliSeconds * halfHourDiff); + + if (!isTimeRangeTooSmall(startDatetimeFrom.getTime(), newEndTime)) { + newEndTime = startDatetimeFrom.getTime() + getMinTimeRange(); + } + + to.setTime(newEndTime); + + calendarEvent.setEndTime(to); + calendarEvent.setEnd(new Date(to.getTime())); + + // Set new position for the event + long startFromMinutes = (startDatetimeFrom.getHours() * 60) + + startDatetimeFrom.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + startFromMinutes = calculateStartFromMinute(startFromMinutes, from, + to, dayOffsetPx); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + updatePosition(startFromMinutes, range); + } + } + + private void cancelMouseMove() { + mouseMoveCanceled = true; + + // reset and remove everything related to the event handling + Event.releaseCapture(getElement()); + setFocus(false); + + if (moveRegistration != null) { + moveRegistration.removeHandler(); + moveRegistration = null; + } + + mouseMoveStarted = false; + removeGlobalResizeStyle(); + + Style s = getElement().getStyle(); + s.setZIndex(1); + + // reset the position of the event + int dateCellWidth = getDateCellWidth(); + int dayOffset = startXrelative / dateCellWidth; + s.clearLeft(); + + calendarEvent.setStartTime(startDatetimeFrom); + calendarEvent.setEndTime(startDatetimeTo); + + long startFromMinutes = (startDatetimeFrom.getHours() * 60) + + startDatetimeFrom.getMinutes(); + long range = calendarEvent.getRangeInMinutes(); + + startFromMinutes = calculateStartFromMinute(startFromMinutes, + startDatetimeFrom, startDatetimeTo, dayOffset); + if (startFromMinutes < 0) { + range += startFromMinutes; + } + + updatePosition(startFromMinutes, range); + + startY = -1; + startX = -1; + + // to reset the event width + ((DateCell) getParent()).recalculateEventWidths(); + } + + // date methods are not deprecated in GWT + @SuppressWarnings("deprecation") + private long calculateStartFromMinute(long startFromMinutes, Date from, + Date to, int dayOffset) { + boolean eventStartAtDifferentDay = from.getDate() != to.getDate(); + if (eventStartAtDifferentDay) { + long minutesOnPrevDay = (getTargetDateByCurrentPosition(dayOffset) + .getTime() - from.getTime()) / DateConstants.MINUTEINMILLIS; + startFromMinutes = -1 * minutesOnPrevDay; + } + + return startFromMinutes; + } + + /** + * @param dateOffset + * @return the amount of pixels the given date is from the left side + */ + private int calculateDateCellOffsetPx(int dateOffset) { + int dateCellOffset = 0; + int[] dateWidths = weekGrid.getDateCellWidths(); + + if (dateWidths != null) { + for (int i = 0; i < dateOffset; i++) { + dateCellOffset += dateWidths[i] + 1; + } + } else { + dateCellOffset = dateOffset * weekGrid.getDateCellWidth(); + } + + return dateCellOffset; + } + + /** + * Check if the given time range is too small for events + * + * @param start + * @param end + * @return + */ + private boolean isTimeRangeTooSmall(long start, long end) { + return (end - start) >= getMinTimeRange(); + } + + /** + * @return the minimum amount of ms that an event must last when resized + */ + private long getMinTimeRange() { + return DateConstants.MINUTEINMILLIS * 30; + } + + /** + * Build the string for sending resize events to server + * + * @param event + * @return + */ + private String buildResizeString(CalendarEvent event) { + StringBuilder buffer = new StringBuilder(); + buffer.append(event.getIndex()); + buffer.append(","); + buffer.append(DateUtil.formatClientSideDate(event.getStart())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event.getStartTime())); + buffer.append(","); + buffer.append(DateUtil.formatClientSideDate(event.getEnd())); + buffer.append("-"); + buffer.append(DateUtil.formatClientSideTime(event.getEndTime())); + + return buffer.toString(); + } + + private Date getTargetDateByCurrentPosition(int left) { + DateCell newParent = (DateCell) weekGrid.content + .getWidget((left / getDateCellWidth()) + 1); + Date targetDate = newParent.getDate(); + return targetDate; + } + + private int getDateCellWidth() { + return weekGrid.getDateCellWidth(); + } + + /* Returns total width of all date cells. */ + private int getDatesWidth() { + if (weekGrid.width == -1) { + // Undefined width. Needs to be calculated by the known cell + // widths. + int count = weekGrid.content.getWidgetCount() - 1; + return count * getDateCellWidth(); + } + + return weekGrid.getInternalWidth(); + } + + /** + * @return true if the current mouse movement is resizing + */ + private boolean clickTargetsResize() { + return weekGrid.getCalendar().isEventResizeAllowed() + && (clickTarget == topResizeBar + || clickTarget == bottomResizeBar); + } + + private void addGlobalResizeStyle() { + if (clickTarget == topResizeBar) { + weekGrid.getCalendar().addStyleDependentName("nresize"); + } else if (clickTarget == bottomResizeBar) { + weekGrid.getCalendar().addStyleDependentName("sresize"); + } + } + + private void removeGlobalResizeStyle() { + weekGrid.getCalendar().removeStyleDependentName("nresize"); + weekGrid.getCalendar().removeStyleDependentName("sresize"); + } + + public void setCalendarEvent(CalendarEvent calendarEvent) { + this.calendarEvent = calendarEvent; + } + + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + @Override + public void onContextMenu(ContextMenuEvent event) { + if (dateCell.weekgrid.getCalendar().getMouseEventListener() != null) { + event.preventDefault(); + event.stopPropagation(); + dateCell.weekgrid.getCalendar().getMouseEventListener() + .contextMenu(event, this); + } + } + + @Override + public Object getTooltipKey() { + return eventIndex; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java new file mode 100644 index 0000000000..907a71c449 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class DateCellGroup { + private WeekGridMinuteTimeRange range; + private final List<Integer> items; + + public DateCellGroup(Integer index) { + items = new ArrayList<Integer>(); + items.add(index); + } + + public WeekGridMinuteTimeRange getDateRange() { + return range; + } + + public Date getStart() { + return range.getStart(); + } + + public Date getEnd() { + return range.getEnd(); + } + + public void setDateRange(WeekGridMinuteTimeRange range) { + this.range = range; + } + + public List<Integer> getItems() { + return items; + } + + public void add(Integer index) { + items.add(index); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java new file mode 100644 index 0000000000..4a11e95245 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java @@ -0,0 +1,70 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * Utility class for {@link Date} operations + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class DateUtil { + + /** + * Checks if dates are same day without checking datetimes. + * + * @param date1 + * @param date2 + * @return + */ + @SuppressWarnings("deprecation") + public static boolean compareDate(Date date1, Date date2) { + if (date1.getDate() == date2.getDate() + && date1.getYear() == date2.getYear() + && date1.getMonth() == date2.getMonth()) { + return true; + } + return false; + } + + /** + * @param date + * the date to format + * + * @return given Date as String, for communicating to server-side + */ + public static String formatClientSideDate(Date date) { + DateTimeFormat dateformat_date = DateTimeFormat + .getFormat(DateConstants.CLIENT_DATE_FORMAT); + return dateformat_date.format(date); + } + + /** + * @param date + * the date to format + * @return given Date as String, for communicating to server-side + */ + public static String formatClientSideTime(Date date) { + DateTimeFormat dateformat_date = DateTimeFormat + .getFormat(DateConstants.CLIENT_TIME_FORMAT); + return dateformat_date.format(date); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java new file mode 100644 index 0000000000..0ba1023945 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java @@ -0,0 +1,179 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Iterator; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class DayToolbar extends HorizontalPanel implements ClickHandler { + private int width = 0; + protected static final int MARGINLEFT = 50; + protected static final int MARGINRIGHT = 20; + protected Button backLabel; + protected Button nextLabel; + private boolean verticalSized; + private boolean horizontalSized; + private VCalendar calendar; + + public DayToolbar(VCalendar vcalendar) { + calendar = vcalendar; + + setStylePrimaryName("v-calendar-header-week"); + backLabel = new Button(); + backLabel.setStylePrimaryName("v-calendar-back"); + nextLabel = new Button(); + nextLabel.addClickHandler(this); + nextLabel.setStylePrimaryName("v-calendar-next"); + backLabel.addClickHandler(this); + setBorderWidth(0); + setSpacing(0); + } + + public void setWidthPX(int width) { + this.width = (width - MARGINLEFT) - MARGINRIGHT; + // super.setWidth(this.width + "px"); + if (getWidgetCount() == 0) { + return; + } + updateCellWidths(); + } + + public void updateCellWidths() { + int count = getWidgetCount(); + if (count > 0) { + setCellWidth(backLabel, MARGINLEFT + "px"); + setCellWidth(nextLabel, MARGINRIGHT + "px"); + setCellHorizontalAlignment(nextLabel, ALIGN_RIGHT); + int cellw = width / (count - 2); + if (cellw > 0) { + int[] cellWidths = VCalendar.distributeSize(width, count - 2, + 0); + for (int i = 1; i < count - 1; i++) { + Widget widget = getWidget(i); + // if (remain > 0) { + // setCellWidth(widget, cellw2 + "px"); + // remain--; + // } else { + // setCellWidth(widget, cellw + "px"); + // } + setCellWidth(widget, cellWidths[i - 1] + "px"); + widget.setWidth(cellWidths[i - 1] + "px"); + } + } + } + } + + public void add(String dayName, final String date, + String localized_date_format, String extraClass) { + Label l = new Label(dayName + " " + localized_date_format); + l.setStylePrimaryName("v-calendar-header-day"); + + if (extraClass != null) { + l.addStyleDependentName(extraClass); + } + + if (verticalSized) { + l.addStyleDependentName("Vsized"); + } + if (horizontalSized) { + l.addStyleDependentName("Hsized"); + } + + l.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (calendar.getDateClickListener() != null) { + calendar.getDateClickListener().dateClick(date); + } + } + }); + + add(l); + } + + public void addBackButton() { + if (!calendar.isBackwardNavigationEnabled()) { + nextLabel.getElement().getStyle().setHeight(0, Unit.PX); + } + add(backLabel); + } + + public void addNextButton() { + if (!calendar.isForwardNavigationEnabled()) { + backLabel.getElement().getStyle().setHeight(0, Unit.PX); + } + add(nextLabel); + } + + @Override + public void onClick(ClickEvent event) { + if (!calendar.isDisabled()) { + if (event.getSource() == nextLabel) { + if (calendar.getForwardListener() != null) { + calendar.getForwardListener().forward(); + } + } else if (event.getSource() == backLabel) { + if (calendar.getBackwardListener() != null) { + calendar.getBackwardListener().backward(); + } + } + } + } + + public void setVerticalSized(boolean sized) { + verticalSized = sized; + updateDayLabelSizedStyleNames(); + } + + public void setHorizontalSized(boolean sized) { + horizontalSized = sized; + updateDayLabelSizedStyleNames(); + } + + private void updateDayLabelSizedStyleNames() { + Iterator<Widget> it = iterator(); + while (it.hasNext()) { + updateWidgetSizedStyleName(it.next()); + } + } + + private void updateWidgetSizedStyleName(Widget w) { + if (verticalSized) { + w.addStyleDependentName("Vsized"); + } else { + w.removeStyleDependentName("VSized"); + } + if (horizontalSized) { + w.addStyleDependentName("Hsized"); + } else { + w.removeStyleDependentName("HSized"); + } + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java new file mode 100644 index 0000000000..a498525c92 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java @@ -0,0 +1,122 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A ComplexPanel that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableComplexPanel extends ComplexPanel + implements HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers, + HasKeyPressHandlers, Focusable { + + protected void makeFocusable() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets/Removes the keyboard focus to the panel. + * + * @param focus + * If set to true then the focus is moved to the panel, if set to + * false the focus is removed + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /** + * Focus the panel + */ + @Override + public void focus() { + setFocus(true); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java new file mode 100644 index 0000000000..fd46f5553b --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java @@ -0,0 +1,134 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A Grid that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableGrid extends Grid implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableGrid() { + super(); + makeFocusable(); + } + + public FocusableGrid(int rows, int columns) { + super(rows, columns); + makeFocusable(); + } + + protected void makeFocusable() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets/Removes the keyboard focus to the panel. + * + * @param focus + * If set to true then the focus is moved to the panel, if set to + * false the focus is removed + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /** + * Focus the panel + */ + @Override + public void focus() { + setFocus(true); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java new file mode 100644 index 0000000000..3a838a58a3 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java @@ -0,0 +1,124 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.dom.client.HasKeyDownHandlers; +import com.google.gwt.event.dom.client.HasKeyPressHandlers; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.impl.FocusImpl; +import com.vaadin.client.Focusable; + +/** + * A HTML widget that can be focused + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class FocusableHTML extends HTML implements HasFocusHandlers, + HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable { + + /** + * Constructor + */ + public FocusableHTML() { + // make focusable, as we don't need access key magic we don't need to + // use FocusImpl.createFocusable + getElement().setTabIndex(0); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com. + * google.gwt.event.dom.client.FocusHandler) + */ + @Override + public HandlerRegistration addFocusHandler(FocusHandler handler) { + return addDomHandler(handler, FocusEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google + * .gwt.event.dom.client.BlurHandler) + */ + @Override + public HandlerRegistration addBlurHandler(BlurHandler handler) { + return addDomHandler(handler, BlurEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler( + * com.google.gwt.event.dom.client.KeyDownHandler) + */ + @Override + public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) { + return addDomHandler(handler, KeyDownEvent.getType()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler + * (com.google.gwt.event.dom.client.KeyPressHandler) + */ + @Override + public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { + return addDomHandler(handler, KeyPressEvent.getType()); + } + + /** + * Sets/Removes the keyboard focus to the panel. + * + * @param focus + * If set to true then the focus is moved to the panel, if set to + * false the focus is removed + */ + public void setFocus(boolean focus) { + if (focus) { + FocusImpl.getFocusImplForPanel().focus(getElement()); + } else { + FocusImpl.getFocusImplForPanel().blur(getElement()); + } + } + + /** + * Focus the panel + */ + @Override + public void focus() { + setFocus(true); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java new file mode 100644 index 0000000000..936f978abb --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +/** + * For Calendar client-side internal use only. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public interface HasTooltipKey { + /** + * Gets the key associated for the Widget implementing this interface. This + * key is used for getting a tooltip title identified by the key + * + * @return the tooltip key + */ + Object getTooltipKey(); +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java new file mode 100644 index 0000000000..c62b21592a --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java @@ -0,0 +1,173 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +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; + +/** + * The label in a month cell + * + * @since 7.1 + */ +public class MonthEventLabel extends HTML implements HasTooltipKey { + + private static final String STYLENAME = "v-calendar-event"; + + private boolean timeSpecificEvent = false; + private Integer eventIndex; + private VCalendar calendar; + private String caption; + private Date time; + + private CalendarEvent calendarEvent; + + /** + * Default constructor + */ + public MonthEventLabel() { + setStylePrimaryName(STYLENAME); + + addDomHandler(new ContextMenuHandler() { + @Override + public void onContextMenu(ContextMenuEvent event) { + calendar.getMouseEventListener().contextMenu(event, + MonthEventLabel.this); + event.stopPropagation(); + event.preventDefault(); + } + }, ContextMenuEvent.getType()); + } + + public void setCalendarEvent(CalendarEvent e) { + calendarEvent = e; + } + + /** + * Set the time of the event label + * + * @param date + * The date object that specifies the time + */ + public void setTime(Date date) { + time = date; + renderCaption(); + } + + /** + * Set the caption of the event label + * + * @param caption + * The caption string, can be HTML if + * {@link VCalendar#isEventCaptionAsHtml()} is true + */ + public void setCaption(String caption) { + this.caption = caption; + renderCaption(); + } + + /** + * Renders the caption in the DIV element + */ + 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("<span class=\"" + STYLENAME + "-time\">"); + html.append(calendar.getTimeFormat().format(time)); + html.append("</span> "); + html.append(textOrHtml); + } else if (caption != null) { + html.append(textOrHtml); + } else if (time != null) { + html.append("<span class=\"" + STYLENAME + "-time\">"); + html.append(calendar.getTimeFormat().format(time)); + html.append("</span>"); + } + super.setHTML(html.toString()); + } + + /** + * Set the (server side) index of the event + * + * @param index + * The integer index + */ + public void setEventIndex(int index) { + eventIndex = index; + } + + /** + * Set the Calendar instance this label belongs to + * + * @param calendar + * The calendar instance + */ + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + /** + * Is the event bound to a specific time + * + * @return + */ + public boolean isTimeSpecificEvent() { + return timeSpecificEvent; + } + + /** + * Is the event bound to a specific time + * + * @param timeSpecificEvent + * True if the event is bound to a time, false if it is only + * bound to the day + */ + public void setTimeSpecificEvent(boolean timeSpecificEvent) { + this.timeSpecificEvent = timeSpecificEvent; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HTML#setHTML(java.lang.String) + */ + @Override + public void setHTML(String html) { + throw new UnsupportedOperationException( + "Use setCaption() and setTime() instead"); + } + + @Override + public Object getTooltipKey() { + return eventIndex; + } + + public CalendarEvent getCalendarEvent() { + return calendarEvent; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java new file mode 100644 index 0000000000..119fe27992 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java @@ -0,0 +1,215 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class MonthGrid extends FocusableGrid implements KeyDownHandler { + + private SimpleDayCell selectionStart; + private SimpleDayCell selectionEnd; + private final VCalendar calendar; + private boolean rangeSelectDisabled; + private boolean enabled = true; + private final HandlerRegistration keyDownHandler; + + public MonthGrid(VCalendar parent, int rows, int columns) { + super(rows, columns); + calendar = parent; + setCellSpacing(0); + setCellPadding(0); + setStylePrimaryName("v-calendar-month"); + + keyDownHandler = addKeyDownHandler(this); + } + + @Override + protected void onUnload() { + keyDownHandler.removeHandler(); + super.onUnload(); + } + + public void setSelectionEnd(SimpleDayCell simpleDayCell) { + selectionEnd = simpleDayCell; + updateSelection(); + } + + public void setSelectionStart(SimpleDayCell simpleDayCell) { + if (!rangeSelectDisabled && isEnabled()) { + selectionStart = simpleDayCell; + setFocus(true); + } + + } + + private void updateSelection() { + if (selectionStart == null) { + return; + } + if (selectionStart != null && selectionEnd != null) { + Date startDate = selectionStart.getDate(); + Date endDate = selectionEnd.getDate(); + for (int row = 0; row < getRowCount(); row++) { + for (int cell = 0; cell < getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell); + if (sdc == null) { + return; + } + Date d = sdc.getDate(); + if (startDate.compareTo(d) <= 0 + && endDate.compareTo(d) >= 0) { + sdc.addStyleDependentName("selected"); + } else if (startDate.compareTo(d) >= 0 + && endDate.compareTo(d) <= 0) { + sdc.addStyleDependentName("selected"); + } else { + sdc.removeStyleDependentName("selected"); + } + } + } + } + } + + public void setSelectionReady() { + if (selectionStart != null && selectionEnd != null) { + String value = ""; + Date startDate = selectionStart.getDate(); + Date endDate = selectionEnd.getDate(); + if (startDate.compareTo(endDate) > 0) { + Date temp = startDate; + startDate = endDate; + endDate = temp; + } + + if (calendar.getRangeSelectListener() != null) { + value = calendar.getDateFormat().format(startDate) + "TO" + + calendar.getDateFormat().format(endDate); + calendar.getRangeSelectListener().rangeSelected(value); + } + selectionStart = null; + selectionEnd = null; + setFocus(false); + } + } + + public void cancelRangeSelection() { + if (selectionStart != null && selectionEnd != null) { + for (int row = 0; row < getRowCount(); row++) { + for (int cell = 0; cell < getCellCount(row); cell++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell); + if (sdc == null) { + return; + } + sdc.removeStyleDependentName("selected"); + } + } + } + setFocus(false); + selectionStart = null; + } + + public void updateCellSizes(int totalWidthPX, int totalHeightPX) { + boolean setHeight = totalHeightPX > 0; + boolean setWidth = totalWidthPX > 0; + int rows = getRowCount(); + int cells = getCellCount(0); + int cellWidth = (totalWidthPX / cells) - 1; + int widthRemainder = totalWidthPX % cells; + // Division for cells might not be even. Distribute it evenly to + // will whole space. + int heightPX = totalHeightPX; + int cellHeight = heightPX / rows; + int heightRemainder = heightPX % rows; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cells; j++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j); + + if (setWidth) { + if (widthRemainder > 0) { + sdc.setWidth(cellWidth + 1 + "px"); + widthRemainder--; + + } else { + sdc.setWidth(cellWidth + "px"); + } + } + + if (setHeight) { + if (heightRemainder > 0) { + sdc.setHeightPX(cellHeight + 1, true); + + } else { + sdc.setHeightPX(cellHeight, true); + } + } else { + sdc.setHeightPX(-1, true); + } + } + heightRemainder--; + } + } + + /** + * Disable or enable possibility to select ranges + */ + public void setRangeSelect(boolean b) { + rangeSelectDisabled = !b; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + @Override + public void onKeyDown(KeyDownEvent event) { + int keycode = event.getNativeKeyCode(); + if (KeyCodes.KEY_ESCAPE == keycode && selectionStart != null) { + cancelRangeSelection(); + } + } + + public int getDayCellIndex(SimpleDayCell dayCell) { + int rows = getRowCount(); + int cells = getCellCount(0); + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cells; j++) { + SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j); + if (dayCell == sdc) { + return i * cells + j; + } + } + } + + return -1; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java new file mode 100644 index 0000000000..45ae5ed8e5 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java @@ -0,0 +1,736 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.FocusableFlowPanel; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * A class representing a single cell within the calendar in month-view + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class SimpleDayCell extends FocusableFlowPanel implements MouseUpHandler, + MouseDownHandler, MouseOverHandler, MouseMoveHandler { + + private static int BOTTOMSPACERHEIGHT = -1; + private static int EVENTHEIGHT = -1; + private static final int BORDERPADDINGSIZE = 1; + + private final VCalendar calendar; + private Date date; + private int intHeight; + private final HTML bottomspacer; + private final Label caption; + private final CalendarEvent[] events = new CalendarEvent[10]; + private final int cell; + private final int row; + private boolean monthNameVisible; + private HandlerRegistration mouseUpRegistration; + private HandlerRegistration mouseDownRegistration; + private HandlerRegistration mouseOverRegistration; + private boolean monthEventMouseDown; + private boolean labelMouseDown; + private int eventCount = 0; + + private int startX = -1; + private int startY = -1; + private int startYrelative; + private int startXrelative; + // "from" date of date which is source of Dnd + private Date dndSourceDateFrom; + // "to" date of date which is source of Dnd + private Date dndSourceDateTo; + // "from" time of date which is source of Dnd + private Date dndSourceStartDateTime; + // "to" time of date which is source of Dnd + private Date dndSourceEndDateTime; + + private int prevDayDiff = 0; + private int prevWeekDiff = 0; + private HandlerRegistration moveRegistration; + private CalendarEvent moveEvent; + private Widget clickedWidget; + private HandlerRegistration bottomSpacerMouseDownHandler; + private boolean scrollable = false; + private MonthGrid monthGrid; + private HandlerRegistration keyDownHandler; + + public SimpleDayCell(VCalendar calendar, int row, int cell) { + this.calendar = calendar; + this.row = row; + this.cell = cell; + setStylePrimaryName("v-calendar-month-day"); + caption = new Label(); + bottomspacer = new HTML(); + bottomspacer.setStyleName("v-calendar-bottom-spacer-empty"); + bottomspacer.setWidth(3 + "em"); + caption.setStyleName("v-calendar-day-number"); + add(caption); + add(bottomspacer); + caption.addMouseDownHandler(this); + caption.addMouseUpHandler(this); + } + + @Override + public void onLoad() { + BOTTOMSPACERHEIGHT = bottomspacer.getOffsetHeight(); + EVENTHEIGHT = BOTTOMSPACERHEIGHT; + } + + public void setMonthGrid(MonthGrid monthGrid) { + this.monthGrid = monthGrid; + } + + public MonthGrid getMonthGrid() { + return monthGrid; + } + + @SuppressWarnings("deprecation") + public void setDate(Date date) { + int dateOfMonth = date.getDate(); + if (monthNameVisible) { + caption.setText(dateOfMonth + " " + + calendar.getMonthNames()[date.getMonth()]); + } else { + caption.setText("" + dateOfMonth); + } + this.date = date; + } + + public Date getDate() { + return date; + } + + public void reDraw(boolean clear) { + setHeightPX(intHeight + BORDERPADDINGSIZE, clear); + } + + /* + * Events and whole cell content are drawn by this method. By the + * clear-argument, you can choose to clear all old content. Notice that + * clearing will also remove all element's event handlers. + */ + public void setHeightPX(int px, boolean clear) { + // measure from DOM if needed + if (px < 0) { + intHeight = getOffsetHeight() - BORDERPADDINGSIZE; + } else { + intHeight = px - BORDERPADDINGSIZE; + } + + // Couldn't measure height or it ended up negative. Don't bother + // continuing + if (intHeight == -1) { + return; + } + + if (clear) { + while (getWidgetCount() > 1) { + remove(1); + } + } + + // How many events can be shown in UI + int slots = 0; + if (scrollable) { + for (int i = 0; i < events.length; i++) { + if (events[i] != null) { + slots = i + 1; + } + } + setHeight(intHeight + "px"); // Fixed height + } else { + // Dynamic height by the content + DOM.removeElementAttribute(getElement(), "height"); + slots = (intHeight - caption.getOffsetHeight() - BOTTOMSPACERHEIGHT) + / EVENTHEIGHT; + if (slots > 10) { + slots = 10; + } + } + + updateEvents(slots, clear); + + } + + public void updateEvents(int slots, boolean clear) { + int eventsAdded = 0; + + for (int i = 0; i < slots; i++) { + CalendarEvent e = events[i]; + if (e == null) { + // Empty slot + HTML slot = new HTML(); + slot.setStyleName("v-calendar-spacer"); + if (!clear) { + remove(i + 1); + insert(slot, i + 1); + } else { + add(slot); + } + } else { + // Event slot + eventsAdded++; + if (!clear) { + Widget w = getWidget(i + 1); + if (!(w instanceof MonthEventLabel)) { + remove(i + 1); + insert(createMonthEventLabel(e), i + 1); + } + } else { + add(createMonthEventLabel(e)); + } + } + } + + int remainingSpace = intHeight - ((slots * EVENTHEIGHT) + + BOTTOMSPACERHEIGHT + caption.getOffsetHeight()); + int newHeight = remainingSpace + BOTTOMSPACERHEIGHT; + if (newHeight < 0) { + newHeight = EVENTHEIGHT; + } + bottomspacer.setHeight(newHeight + "px"); + + if (clear) { + add(bottomspacer); + } + + int more = eventCount - eventsAdded; + if (more > 0) { + if (bottomSpacerMouseDownHandler == null) { + bottomSpacerMouseDownHandler = bottomspacer + .addMouseDownHandler(this); + } + bottomspacer.setStyleName("v-calendar-bottom-spacer"); + bottomspacer.setText("+ " + more); + } else { + if (!scrollable && bottomSpacerMouseDownHandler != null) { + bottomSpacerMouseDownHandler.removeHandler(); + bottomSpacerMouseDownHandler = null; + } + + if (scrollable) { + bottomspacer.setText("[ - ]"); + } else { + bottomspacer.setStyleName("v-calendar-bottom-spacer-empty"); + bottomspacer.setText(""); + } + } + } + + private MonthEventLabel createMonthEventLabel(CalendarEvent e) { + long rangeInMillis = e.getRangeInMilliseconds(); + boolean timeEvent = rangeInMillis <= DateConstants.DAYINMILLIS + && !e.isAllDay(); + Date fromDatetime = e.getStartTime(); + + // Create a new MonthEventLabel + MonthEventLabel eventDiv = new MonthEventLabel(); + eventDiv.addStyleDependentName("month"); + eventDiv.addMouseDownHandler(this); + eventDiv.addMouseUpHandler(this); + eventDiv.setCalendar(calendar); + eventDiv.setEventIndex(e.getIndex()); + eventDiv.setCalendarEvent(e); + + if (timeEvent) { + eventDiv.setTimeSpecificEvent(true); + if (e.getStyleName() != null) { + eventDiv.addStyleDependentName(e.getStyleName()); + } + eventDiv.setCaption(e.getCaption()); + eventDiv.setTime(fromDatetime); + + } else { + eventDiv.setTimeSpecificEvent(false); + Date from = e.getStart(); + Date to = e.getEnd(); + if (e.getStyleName().length() > 0) { + eventDiv.addStyleName("month-event " + e.getStyleName()); + } else { + eventDiv.addStyleName("month-event"); + } + int fromCompareToDate = from.compareTo(date); + int toCompareToDate = to.compareTo(date); + eventDiv.addStyleDependentName("all-day"); + if (fromCompareToDate == 0) { + eventDiv.addStyleDependentName("start"); + eventDiv.setCaption(e.getCaption()); + + } else if (fromCompareToDate < 0 && cell == 0) { + eventDiv.addStyleDependentName("continued-from"); + eventDiv.setCaption(e.getCaption()); + } + if (toCompareToDate == 0) { + eventDiv.addStyleDependentName("end"); + } else if (toCompareToDate > 0 + && (cell + 1) == getMonthGrid().getCellCount(row)) { + eventDiv.addStyleDependentName("continued-to"); + } + if (e.getStyleName() != null) { + eventDiv.addStyleDependentName(e.getStyleName() + "-all-day"); + } + } + + return eventDiv; + } + + private void setUnlimitedCellHeight() { + scrollable = true; + addStyleDependentName("scrollable"); + } + + private void setLimitedCellHeight() { + scrollable = false; + removeStyleDependentName("scrollable"); + } + + public void addCalendarEvent(CalendarEvent e) { + eventCount++; + int slot = e.getSlotIndex(); + if (slot == -1) { + for (int i = 0; i < events.length; i++) { + if (events[i] == null) { + events[i] = e; + e.setSlotIndex(i); + break; + } + } + } else { + events[slot] = e; + } + } + + @SuppressWarnings("deprecation") + public void setMonthNameVisible(boolean b) { + monthNameVisible = b; + int dateOfMonth = date.getDate(); + caption.setText( + dateOfMonth + " " + calendar.getMonthNames()[date.getMonth()]); + } + + public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) { + return addDomHandler(handler, MouseMoveEvent.getType()); + } + + @Override + protected void onAttach() { + super.onAttach(); + mouseUpRegistration = addDomHandler(this, MouseUpEvent.getType()); + mouseDownRegistration = addDomHandler(this, MouseDownEvent.getType()); + mouseOverRegistration = addDomHandler(this, MouseOverEvent.getType()); + } + + @Override + protected void onDetach() { + mouseUpRegistration.removeHandler(); + mouseDownRegistration.removeHandler(); + mouseOverRegistration.removeHandler(); + super.onDetach(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + Widget w = (Widget) event.getSource(); + if (moveRegistration != null) { + Event.releaseCapture(getElement()); + moveRegistration.removeHandler(); + moveRegistration = null; + keyDownHandler.removeHandler(); + keyDownHandler = null; + } + + if (w == bottomspacer && monthEventMouseDown) { + GWT.log("Mouse up over bottomspacer"); + + } else if (clickedWidget instanceof MonthEventLabel + && monthEventMouseDown) { + MonthEventLabel mel = (MonthEventLabel) clickedWidget; + + int endX = event.getClientX(); + int endY = event.getClientY(); + int xDiff = 0, yDiff = 0; + if (startX != -1 && startY != -1) { + xDiff = startX - endX; + yDiff = startY - endY; + } + startX = -1; + startY = -1; + prevDayDiff = 0; + prevWeekDiff = 0; + + if (xDiff < -3 || xDiff > 3 || yDiff < -3 || yDiff > 3) { + eventMoved(moveEvent); + + } else if (calendar.getEventClickListener() != null) { + CalendarEvent e = getEventByWidget(mel); + calendar.getEventClickListener().eventClick(e); + } + + moveEvent = null; + } else if (w == this) { + getMonthGrid().setSelectionReady(); + + } else if (w instanceof Label && labelMouseDown) { + String clickedDate = calendar.getDateFormat().format(date); + if (calendar.getDateClickListener() != null) { + calendar.getDateClickListener().dateClick(clickedDate); + } + } + monthEventMouseDown = false; + labelMouseDown = false; + clickedWidget = null; + } + + @Override + public void onMouseDown(MouseDownEvent event) { + if (calendar.isDisabled() + || event.getNativeButton() != NativeEvent.BUTTON_LEFT) { + return; + } + + Widget w = (Widget) event.getSource(); + clickedWidget = w; + + if (w instanceof MonthEventLabel) { + // event clicks should be allowed even when read-only + monthEventMouseDown = true; + + if (calendar.isEventMoveAllowed()) { + startCalendarEventDrag(event, (MonthEventLabel) w); + } + } else if (w == bottomspacer) { + if (scrollable) { + setLimitedCellHeight(); + } else { + setUnlimitedCellHeight(); + } + reDraw(true); + } else if (w instanceof Label) { + labelMouseDown = true; + } else if (w == this && !scrollable) { + MonthGrid grid = getMonthGrid(); + if (grid.isEnabled() && calendar.isRangeSelectAllowed()) { + grid.setSelectionStart(this); + grid.setSelectionEnd(this); + } + } + + event.stopPropagation(); + event.preventDefault(); + } + + @Override + public void onMouseOver(MouseOverEvent event) { + event.preventDefault(); + getMonthGrid().setSelectionEnd(this); + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + if (clickedWidget instanceof MonthEventLabel && !monthEventMouseDown + || (startY < 0 && startX < 0)) { + return; + } + + MonthEventLabel w = (MonthEventLabel) clickedWidget; + + if (calendar.isDisabledOrReadOnly()) { + Event.releaseCapture(getElement()); + monthEventMouseDown = false; + startY = -1; + startX = -1; + return; + } + + int currentY = event.getClientY(); + int currentX = event.getClientX(); + int moveY = (currentY - startY); + int moveX = (currentX - startX); + if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) { + return; + } + + int dateCellWidth = getWidth(); + int dateCellHeigth = getHeigth(); + + Element parent = getMonthGrid().getElement(); + int relativeX = event.getRelativeX(parent); + int relativeY = event.getRelativeY(parent); + int weekDiff = 0; + if (moveY > 0) { + weekDiff = (startYrelative + moveY) / dateCellHeigth; + } else { + weekDiff = (moveY - (dateCellHeigth - startYrelative)) + / dateCellHeigth; + } + + int dayDiff = 0; + if (moveX >= 0) { + dayDiff = (startXrelative + moveX) / dateCellWidth; + } else { + dayDiff = (moveX - (dateCellWidth - startXrelative)) + / dateCellWidth; + } + // Check boundaries + if (relativeY < 0 + || relativeY >= (calendar.getMonthGrid().getRowCount() + * dateCellHeigth) + || relativeX < 0 + || relativeX >= (calendar.getMonthGrid().getColumnCount() + * dateCellWidth)) { + return; + } + + GWT.log("Event moving delta: " + weekDiff + " weeks " + dayDiff + + " days" + " (" + getCell() + "," + getRow() + ")"); + + CalendarEvent e = moveEvent; + if (e == null) { + e = getEventByWidget(w); + } + + Date from = e.getStart(); + Date to = e.getEnd(); + + long daysMs = dayDiff * DateConstants.DAYINMILLIS; + long weeksMs = weekDiff * DateConstants.WEEKINMILLIS; + + setDates(e, from, to, weeksMs + daysMs, false); + e.setStart(from); + e.setEnd(to); + if (w.isTimeSpecificEvent()) { + Date start = new Date(); + Date end = new Date(); + setDates(e, start, end, weeksMs + daysMs, true); + e.setStartTime(start); + e.setEndTime(end); + } else { + e.setStartTime(new Date(from.getTime())); + e.setEndTime(new Date(to.getTime())); + } + + updateDragPosition(w, dayDiff, weekDiff); + } + + private void setDates(CalendarEvent e, Date start, Date end, long shift, + boolean isDateTime) { + Date currentStart; + Date currentEnd; + if (isDateTime) { + currentStart = e.getStartTime(); + currentEnd = e.getEndTime(); + } else { + currentStart = e.getStart(); + currentEnd = e.getEnd(); + } + long duration = currentEnd.getTime() - currentStart.getTime(); + if (isDateTime) { + start.setTime(dndSourceStartDateTime.getTime() + shift); + } else { + start.setTime(dndSourceDateFrom.getTime() + shift); + } + end.setTime((start.getTime() + duration)); + } + + private void eventMoved(CalendarEvent e) { + calendar.updateEventToMonthGrid(e); + if (calendar.getEventMovedListener() != null) { + calendar.getEventMovedListener().eventMoved(e); + } + } + + public void startCalendarEventDrag(MouseDownEvent event, + final MonthEventLabel w) { + moveRegistration = addMouseMoveHandler(this); + startX = event.getClientX(); + startY = event.getClientY(); + startYrelative = event.getRelativeY(w.getParent().getElement()) + % getHeigth(); + startXrelative = event.getRelativeX(w.getParent().getElement()) + % getWidth(); + + CalendarEvent e = getEventByWidget(w); + dndSourceDateFrom = (Date) e.getStart().clone(); + dndSourceDateTo = (Date) e.getEnd().clone(); + + dndSourceStartDateTime = (Date) e.getStartTime().clone(); + dndSourceEndDateTime = (Date) e.getEndTime().clone(); + + Event.setCapture(getElement()); + keyDownHandler = addKeyDownHandler(new KeyDownHandler() { + + @Override + public void onKeyDown(KeyDownEvent event) { + if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { + cancelEventDrag(w); + } + } + + }); + + focus(); + + GWT.log("Start drag"); + } + + protected void cancelEventDrag(MonthEventLabel w) { + if (moveRegistration != null) { + // reset position + if (moveEvent == null) { + moveEvent = getEventByWidget(w); + } + + moveEvent.setStart(dndSourceDateFrom); + moveEvent.setEnd(dndSourceDateTo); + moveEvent.setStartTime(dndSourceStartDateTime); + moveEvent.setEndTime(dndSourceEndDateTime); + calendar.updateEventToMonthGrid(moveEvent); + + // reset drag-related properties + Event.releaseCapture(getElement()); + moveRegistration.removeHandler(); + moveRegistration = null; + keyDownHandler.removeHandler(); + keyDownHandler = null; + setFocus(false); + monthEventMouseDown = false; + startY = -1; + startX = -1; + moveEvent = null; + labelMouseDown = false; + clickedWidget = null; + } + } + + public void updateDragPosition(MonthEventLabel w, int dayDiff, + int weekDiff) { + // Draw event to its new position only when position has changed + if (dayDiff == prevDayDiff && weekDiff == prevWeekDiff) { + return; + } + + prevDayDiff = dayDiff; + prevWeekDiff = weekDiff; + + if (moveEvent == null) { + moveEvent = getEventByWidget(w); + } + + calendar.updateEventToMonthGrid(moveEvent); + } + + public int getRow() { + return row; + } + + public int getCell() { + return cell; + } + + public int getHeigth() { + return intHeight + BORDERPADDINGSIZE; + } + + public int getWidth() { + return getOffsetWidth() - BORDERPADDINGSIZE; + } + + public void setToday(boolean today) { + if (today) { + addStyleDependentName("today"); + } else { + removeStyleDependentName("today"); + } + } + + public boolean removeEvent(CalendarEvent targetEvent, + boolean reDrawImmediately) { + int slot = targetEvent.getSlotIndex(); + if (slot < 0) { + return false; + } + + CalendarEvent e = getCalendarEvent(slot); + if (targetEvent.equals(e)) { + events[slot] = null; + eventCount--; + if (reDrawImmediately) { + reDraw(moveEvent == null); + } + return true; + } + return false; + } + + private CalendarEvent getEventByWidget(MonthEventLabel eventWidget) { + int index = getWidgetIndex(eventWidget); + return getCalendarEvent(index - 1); + } + + public CalendarEvent getCalendarEvent(int i) { + return events[i]; + } + + public CalendarEvent[] getEvents() { + return events; + } + + public int getEventCount() { + return eventCount; + } + + public CalendarEvent getMoveEvent() { + return moveEvent; + } + + public void addEmphasisStyle() { + addStyleDependentName("dragemphasis"); + } + + public void removeEmphasisStyle() { + removeStyleDependentName("dragemphasis"); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java new file mode 100644 index 0000000000..e83a2cce3a --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; + +/** + * + * @since 7.1.0 + * @author Vaadin Ltd. + * + */ +public class SimpleDayToolbar extends HorizontalPanel { + private int width = 0; + private boolean isWidthUndefined = false; + + public SimpleDayToolbar() { + setStylePrimaryName("v-calendar-header-month"); + } + + public void setDayNames(String[] dayNames) { + clear(); + for (int i = 0; i < dayNames.length; i++) { + Label l = new Label(dayNames[i]); + l.setStylePrimaryName("v-calendar-header-day"); + add(l); + } + updateCellWidth(); + } + + public void setWidthPX(int width) { + this.width = width; + + setWidthUndefined(width == -1); + + if (!isWidthUndefined()) { + super.setWidth(this.width + "px"); + if (getWidgetCount() == 0) { + return; + } + } + updateCellWidth(); + } + + private boolean isWidthUndefined() { + return isWidthUndefined; + } + + private void setWidthUndefined(boolean isWidthUndefined) { + this.isWidthUndefined = isWidthUndefined; + + if (isWidthUndefined) { + addStyleDependentName("Hsized"); + + } else { + removeStyleDependentName("Hsized"); + } + } + + private void updateCellWidth() { + int cellw = -1; + int widgetCount = getWidgetCount(); + if (widgetCount <= 0) { + return; + } + if (isWidthUndefined()) { + Widget widget = getWidget(0); + String w = widget.getElement().getStyle().getWidth(); + if (w.length() > 2) { + cellw = Integer.parseInt(w.substring(0, w.length() - 2)); + } + } else { + cellw = width / getWidgetCount(); + } + if (cellw > 0) { + for (int i = 0; i < getWidgetCount(); i++) { + Widget widget = getWidget(i); + setCellWidth(widget, cellw + "px"); + } + } + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java new file mode 100644 index 0000000000..30c52e1059 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.FlexTable; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class SimpleWeekToolbar extends FlexTable implements ClickHandler { + private int height; + private VCalendar calendar; + private boolean isHeightUndefined; + + public SimpleWeekToolbar(VCalendar parent) { + calendar = parent; + setCellSpacing(0); + setCellPadding(0); + setStyleName("v-calendar-week-numbers"); + } + + public void addWeek(int week, int year) { + WeekLabel l = new WeekLabel(week + "", week, year); + l.addClickHandler(this); + int rowCount = getRowCount(); + insertRow(rowCount); + setWidget(rowCount, 0, l); + updateCellHeights(); + } + + public void updateCellHeights() { + if (!isHeightUndefined()) { + int rowCount = getRowCount(); + if (rowCount == 0) { + return; + } + int cellheight = (height / rowCount) - 1; + int remainder = height % rowCount; + if (cellheight < 0) { + cellheight = 0; + } + for (int i = 0; i < rowCount; i++) { + if (remainder > 0) { + getWidget(i, 0).setHeight(cellheight + 1 + "px"); + } else { + getWidget(i, 0).setHeight(cellheight + "px"); + } + getWidget(i, 0).getElement().getStyle() + .setProperty("lineHeight", cellheight + "px"); + remainder--; + } + } else { + for (int i = 0; i < getRowCount(); i++) { + getWidget(i, 0).setHeight(""); + getWidget(i, 0).getElement().getStyle() + .setProperty("lineHeight", ""); + } + } + } + + public void setHeightPX(int intHeight) { + setHeightUndefined(intHeight == -1); + height = intHeight; + updateCellHeights(); + } + + public boolean isHeightUndefined() { + return isHeightUndefined; + } + + public void setHeightUndefined(boolean isHeightUndefined) { + this.isHeightUndefined = isHeightUndefined; + + if (isHeightUndefined) { + addStyleDependentName("Vsized"); + + } else { + removeStyleDependentName("Vsized"); + } + } + + @Override + public void onClick(ClickEvent event) { + WeekLabel wl = (WeekLabel) event.getSource(); + if (calendar.getWeekClickListener() != null) { + calendar.getWeekClickListener() + .weekClick(wl.getYear() + "w" + wl.getWeek()); + } + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java new file mode 100644 index 0000000000..cfc9d6231a --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java @@ -0,0 +1,692 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Arrays; +import java.util.Date; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.ScrollEvent; +import com.google.gwt.event.dom.client.ScrollHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.DateTimeService; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VCalendar; +import com.vaadin.shared.ui.calendar.DateConstants; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class WeekGrid extends SimplePanel { + + int width = 0; + private int height = 0; + final HorizontalPanel content; + private VCalendar calendar; + private boolean disabled; + final Timebar timebar; + private Panel wrapper; + private boolean verticalScrollEnabled; + private boolean horizontalScrollEnabled; + private int[] cellHeights; + private final int slotInMinutes = 30; + private int dateCellBorder; + private DateCell dateCellOfToday; + private int[] cellWidths; + private int firstHour; + private int lastHour; + + public WeekGrid(VCalendar parent, boolean format24h) { + setCalendar(parent); + content = new HorizontalPanel(); + timebar = new Timebar(format24h); + content.add(timebar); + + wrapper = new SimplePanel(); + wrapper.setStylePrimaryName("v-calendar-week-wrapper"); + wrapper.add(content); + + setWidget(wrapper); + } + + private void setVerticalScroll(boolean isVerticalScrollEnabled) { + if (isVerticalScrollEnabled && !(isVerticalScrollable())) { + verticalScrollEnabled = true; + horizontalScrollEnabled = false; + wrapper.remove(content); + + final ScrollPanel scrollPanel = new ScrollPanel(); + scrollPanel.setStylePrimaryName("v-calendar-week-wrapper"); + scrollPanel.setWidget(content); + + scrollPanel.addScrollHandler(new ScrollHandler() { + @Override + public void onScroll(ScrollEvent event) { + if (calendar.getScrollListener() != null) { + calendar.getScrollListener().scroll( + scrollPanel.getVerticalScrollPosition()); + } + } + }); + + setWidget(scrollPanel); + wrapper = scrollPanel; + + } else if (!isVerticalScrollEnabled && (isVerticalScrollable())) { + verticalScrollEnabled = false; + horizontalScrollEnabled = false; + wrapper.remove(content); + + SimplePanel simplePanel = new SimplePanel(); + simplePanel.setStylePrimaryName("v-calendar-week-wrapper"); + simplePanel.setWidget(content); + + setWidget(simplePanel); + wrapper = simplePanel; + } + } + + public void setVerticalScrollPosition(int verticalScrollPosition) { + if (isVerticalScrollable()) { + ((ScrollPanel) wrapper) + .setVerticalScrollPosition(verticalScrollPosition); + } + } + + public int getInternalWidth() { + return width; + } + + public void addDate(Date d) { + final DateCell dc = new DateCell(this, d); + dc.setDisabled(isDisabled()); + dc.setHorizontalSized(isHorizontalScrollable() || width < 0); + dc.setVerticalSized(isVerticalScrollable()); + content.add(dc); + } + + /** + * @param dateCell + * @return get the index of the given date cell in this week, starting from + * 0 + */ + public int getDateCellIndex(DateCell dateCell) { + return content.getWidgetIndex(dateCell) - 1; + } + + /** + * @return get the slot border in pixels + */ + public int getDateSlotBorder() { + return ((DateCell) content.getWidget(1)).getSlotBorder(); + } + + private boolean isVerticalScrollable() { + return verticalScrollEnabled; + } + + private boolean isHorizontalScrollable() { + return horizontalScrollEnabled; + } + + public void setWidthPX(int width) { + if (isHorizontalScrollable()) { + updateCellWidths(); + + // Otherwise the scroll wrapper is somehow too narrow = horizontal + // scroll + wrapper.setWidth(content.getOffsetWidth() + + WidgetUtil.getNativeScrollbarSize() + "px"); + + this.width = content.getOffsetWidth() - timebar.getOffsetWidth(); + + } else { + this.width = (width == -1) ? width + : width - timebar.getOffsetWidth(); + + if (isVerticalScrollable() && width != -1) { + this.width = this.width - WidgetUtil.getNativeScrollbarSize(); + } + updateCellWidths(); + } + } + + public void setHeightPX(int intHeight) { + height = intHeight; + + setVerticalScroll(height <= -1); + + // if not scrollable, use any height given + if (!isVerticalScrollable() && height > 0) { + + content.setHeight(height + "px"); + setHeight(height + "px"); + wrapper.setHeight(height + "px"); + wrapper.removeStyleDependentName("Vsized"); + updateCellHeights(); + timebar.setCellHeights(cellHeights); + timebar.setHeightPX(height); + + } else if (isVerticalScrollable()) { + updateCellHeights(); + wrapper.addStyleDependentName("Vsized"); + timebar.setCellHeights(cellHeights); + timebar.setHeightPX(height); + } + } + + public void clearDates() { + while (content.getWidgetCount() > 1) { + content.remove(1); + } + + dateCellOfToday = null; + } + + /** + * @return true if this weekgrid contains a date that is today + */ + public boolean hasToday() { + return dateCellOfToday != null; + } + + public void updateCellWidths() { + if (!isHorizontalScrollable() && width != -1) { + int count = content.getWidgetCount(); + int datesWidth = width; + if (datesWidth > 0 && count > 1) { + cellWidths = VCalendar.distributeSize(datesWidth, count - 1, + -1); + + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHorizontalSized( + isHorizontalScrollable() || width < 0); + dc.setWidthPX(cellWidths[i - 1]); + if (dc.isToday()) { + dc.setTimeBarWidth(getOffsetWidth()); + } + } + } + + } else { + int count = content.getWidgetCount(); + if (count > 1) { + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHorizontalSized( + isHorizontalScrollable() || width < 0); + } + } + } + } + + /** + * @return an int-array containing the widths of the cells (days) + */ + public int[] getDateCellWidths() { + return cellWidths; + } + + public void updateCellHeights() { + if (!isVerticalScrollable()) { + int count = content.getWidgetCount(); + if (count > 1) { + DateCell first = (DateCell) content.getWidget(1); + dateCellBorder = first.getSlotBorder(); + cellHeights = VCalendar.distributeSize(height, + first.getNumberOfSlots(), -dateCellBorder); + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setHeightPX(height, cellHeights); + } + } + + } else { + int count = content.getWidgetCount(); + if (count > 1) { + DateCell first = (DateCell) content.getWidget(1); + dateCellBorder = first.getSlotBorder(); + int dateHeight = (first.getOffsetHeight() + / first.getNumberOfSlots()) - dateCellBorder; + cellHeights = new int[48]; + Arrays.fill(cellHeights, dateHeight); + + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + dc.setVerticalSized(isVerticalScrollable()); + } + } + } + } + + public void addEvent(CalendarEvent e) { + int dateCount = content.getWidgetCount(); + Date from = e.getStart(); + Date toTime = e.getEndTime(); + for (int i = 1; i < dateCount; i++) { + DateCell dc = (DateCell) content.getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(from); + int comp2 = dcDate.compareTo(toTime); + if (comp >= 0 && comp2 < 0 || (comp == 0 && comp2 == 0 + && VCalendar.isZeroLengthMidnightEvent(e))) { + // Same event may be over two DateCells if event's date + // range floats over one day. It can't float over two days, + // because event which range is over 24 hours, will be handled + // as a "fullDay" event. + dc.addEvent(dcDate, e); + } + } + } + + public int getPixelLengthFor(int startFromMinutes, int durationInMinutes) { + int pixelLength = 0; + int currentSlot = 0; + + int firstHourInMinutes = firstHour * DateConstants.HOURINMINUTES; + int endHourInMinutes = lastHour * DateConstants.HOURINMINUTES; + + if (firstHourInMinutes > startFromMinutes) { + durationInMinutes = durationInMinutes + - (firstHourInMinutes - startFromMinutes); + startFromMinutes = 0; + } else { + startFromMinutes -= firstHourInMinutes; + } + + int shownHeightInMinutes = endHourInMinutes - firstHourInMinutes + + DateConstants.HOURINMINUTES; + + durationInMinutes = Math.min(durationInMinutes, + shownHeightInMinutes - startFromMinutes); + + // calculate full slots to event + int slotsTillEvent = startFromMinutes / slotInMinutes; + int startOverFlowTime = slotInMinutes + - (startFromMinutes % slotInMinutes); + if (startOverFlowTime == slotInMinutes) { + startOverFlowTime = 0; + currentSlot = slotsTillEvent; + } else { + currentSlot = slotsTillEvent + 1; + } + + int durationInSlots = 0; + int endOverFlowTime = 0; + + if (startOverFlowTime > 0) { + durationInSlots = (durationInMinutes - startOverFlowTime) + / slotInMinutes; + endOverFlowTime = (durationInMinutes - startOverFlowTime) + % slotInMinutes; + + } else { + durationInSlots = durationInMinutes / slotInMinutes; + endOverFlowTime = durationInMinutes % slotInMinutes; + } + + // calculate slot overflow at start + if (startOverFlowTime > 0 && currentSlot < cellHeights.length) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight + / (double) slotInMinutes) * startOverFlowTime); + } + + // calculate length in full slots + int lastFullSlot = currentSlot + durationInSlots; + for (; currentSlot < lastFullSlot + && currentSlot < cellHeights.length; currentSlot++) { + pixelLength += cellHeights[currentSlot] + dateCellBorder; + } + + // calculate overflow at end + if (endOverFlowTime > 0 && currentSlot < cellHeights.length) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight + / (double) slotInMinutes) * endOverFlowTime); + } + + // reduce possible underflow at end + if (endOverFlowTime < 0) { + int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder; + pixelLength += (int) (((double) lastSlotHeight + / (double) slotInMinutes) * endOverFlowTime); + } + + return pixelLength; + } + + public int getPixelTopFor(int startFromMinutes) { + int pixelsToTop = 0; + int slotIndex = 0; + + int firstHourInMinutes = firstHour * 60; + + if (firstHourInMinutes > startFromMinutes) { + startFromMinutes = 0; + } else { + startFromMinutes -= firstHourInMinutes; + } + + // calculate full slots to event + int slotsTillEvent = startFromMinutes / slotInMinutes; + int overFlowTime = startFromMinutes % slotInMinutes; + if (slotsTillEvent > 0) { + for (slotIndex = 0; slotIndex < slotsTillEvent; slotIndex++) { + pixelsToTop += cellHeights[slotIndex] + dateCellBorder; + } + } + + // calculate lengths less than one slot + if (overFlowTime > 0) { + int lastSlotHeight = cellHeights[slotIndex] + dateCellBorder; + pixelsToTop += ((double) lastSlotHeight / (double) slotInMinutes) + * overFlowTime; + } + + return pixelsToTop; + } + + public void eventMoved(DateCellDayEvent dayEvent) { + Style s = dayEvent.getElement().getStyle(); + int left = Integer + .parseInt(s.getLeft().substring(0, s.getLeft().length() - 2)); + DateCell previousParent = (DateCell) dayEvent.getParent(); + DateCell newParent = (DateCell) content + .getWidget((left / getDateCellWidth()) + 1); + CalendarEvent se = dayEvent.getCalendarEvent(); + previousParent.removeEvent(dayEvent); + newParent.addEvent(dayEvent); + if (!previousParent.equals(newParent)) { + previousParent.recalculateEventWidths(); + } + newParent.recalculateEventWidths(); + if (calendar.getEventMovedListener() != null) { + calendar.getEventMovedListener().eventMoved(se); + } + } + + public void setToday(Date todayDate, Date todayTimestamp) { + int count = content.getWidgetCount(); + if (count > 1) { + for (int i = 1; i < count; i++) { + DateCell dc = (DateCell) content.getWidget(i); + if (dc.getDate().getTime() == todayDate.getTime()) { + if (isVerticalScrollable()) { + dc.setToday(todayTimestamp, -1); + } else { + dc.setToday(todayTimestamp, getOffsetWidth()); + } + } + dateCellOfToday = dc; + } + } + } + + public DateCell getDateCellOfToday() { + return dateCellOfToday; + } + + public void setDisabled(boolean disabled) { + this.disabled = disabled; + } + + public boolean isDisabled() { + return disabled; + } + + public Timebar getTimeBar() { + return timebar; + } + + public void setDateColor(Date when, Date to, String styleName) { + int dateCount = content.getWidgetCount(); + for (int i = 1; i < dateCount; i++) { + DateCell dc = (DateCell) content.getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(when); + int comp2 = dcDate.compareTo(to); + if (comp >= 0 && comp2 <= 0) { + dc.setDateColor(styleName); + } + } + } + + /** + * @param calendar + * the calendar to set + */ + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + /** + * @return the calendar + */ + public VCalendar getCalendar() { + return calendar; + } + + /** + * Get width of the single date cell + * + * @return Date cell width + */ + public int getDateCellWidth() { + int count = content.getWidgetCount() - 1; + int cellWidth = -1; + if (count <= 0) { + return cellWidth; + } + + if (width == -1) { + Widget firstWidget = content.getWidget(1); + cellWidth = firstWidget.getElement().getOffsetWidth(); + } else { + cellWidth = getInternalWidth() / count; + } + return cellWidth; + } + + /** + * @return the number of day cells in this week + */ + public int getDateCellCount() { + return content.getWidgetCount() - 1; + } + + public void setFirstHour(int firstHour) { + this.firstHour = firstHour; + timebar.setFirstHour(firstHour); + } + + public void setLastHour(int lastHour) { + this.lastHour = lastHour; + timebar.setLastHour(lastHour); + } + + public int getFirstHour() { + return firstHour; + } + + public int getLastHour() { + return lastHour; + } + + public static class Timebar extends HTML { + + private static final int[] timesFor12h = { 12, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11 }; + + private int height; + + private final int verticalPadding = 7; // FIXME measure this from DOM + + private int[] slotCellHeights; + + private int firstHour; + + private int lastHour; + + public Timebar(boolean format24h) { + createTimeBar(format24h); + } + + public void setLastHour(int lastHour) { + this.lastHour = lastHour; + } + + public void setFirstHour(int firstHour) { + this.firstHour = firstHour; + + } + + public void setCellHeights(int[] cellHeights) { + slotCellHeights = cellHeights; + } + + private void createTimeBar(boolean format24h) { + setStylePrimaryName("v-calendar-times"); + + // Fist "time" is empty + Element e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerText(""); + getElement().appendChild(e); + + DateTimeService dts = new DateTimeService(); + + if (format24h) { + for (int i = firstHour + 1; i <= lastHour; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + String delimiter = dts.getClockDelimeter(); + e.setInnerHTML("<span>" + i + "</span>" + delimiter + "00"); + getElement().appendChild(e); + } + } else { + // FIXME Use dts.getAmPmStrings(); and make sure that + // DateTimeService has a some Locale set. + String[] ampm = new String[] { "AM", "PM" }; + + int amStop = (lastHour < 11) ? lastHour : 11; + int pmStart = (firstHour > 11) ? firstHour % 11 : 0; + + if (firstHour < 12) { + for (int i = firstHour + 1; i <= amStop; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerHTML("<span>" + timesFor12h[i] + "</span>" + + " " + ampm[0]); + getElement().appendChild(e); + } + } + + if (lastHour > 11) { + for (int i = pmStart; i < lastHour - 11; i++) { + e = DOM.createDiv(); + setStyleName(e, "v-calendar-time"); + e.setInnerHTML("<span>" + timesFor12h[i] + "</span>" + + " " + ampm[1]); + getElement().appendChild(e); + } + } + } + } + + public void updateTimeBar(boolean format24h) { + clear(); + createTimeBar(format24h); + } + + private void clear() { + while (getElement().getChildCount() > 0) { + getElement().removeChild(getElement().getChild(0)); + } + } + + public void setHeightPX(int pixelHeight) { + height = pixelHeight; + + if (pixelHeight > -1) { + // as the negative margins on children pulls the whole element + // upwards, we must compensate. otherwise the element would be + // too short + super.setHeight((height + verticalPadding) + "px"); + removeStyleDependentName("Vsized"); + updateChildHeights(); + + } else { + addStyleDependentName("Vsized"); + updateChildHeights(); + } + } + + private void updateChildHeights() { + int childCount = getElement().getChildCount(); + + if (height != -1) { + + // 23 hours + first is empty + // we try to adjust the height of time labels to the distributed + // heights of the time slots + int hoursPerDay = lastHour - firstHour + 1; + + int slotsPerHour = slotCellHeights.length / hoursPerDay; + int[] cellHeights = new int[slotCellHeights.length + / slotsPerHour]; + + int slotHeightPosition = 0; + for (int i = 0; i < cellHeights.length; i++) { + for (int j = slotHeightPosition; j < slotHeightPosition + + slotsPerHour; j++) { + cellHeights[i] += slotCellHeights[j] + 1; + // 1px more for borders + // FIXME measure from DOM + } + slotHeightPosition += slotsPerHour; + } + + for (int i = 0; i < childCount; i++) { + Element e = (Element) getElement().getChild(i); + e.getStyle().setHeight(cellHeights[i], Unit.PX); + } + + } else { + for (int i = 0; i < childCount; i++) { + Element e = (Element) getElement().getChild(i); + e.getStyle().setProperty("height", ""); + } + } + } + } + + public VCalendar getParentCalendar() { + return calendar; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java new file mode 100644 index 0000000000..984d3d48dc --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java @@ -0,0 +1,62 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +/** + * Internally used by the calendar + * + * @since 7.1 + */ +public class WeekGridMinuteTimeRange { + private final Date start; + private final Date end; + + /** + * Creates a Date time range between start and end date. Drops seconds from + * the range. + * + * @param start + * Start time of the range + * @param end + * End time of the range + * @param clearSeconds + * Boolean Indicates, if seconds should be dropped from the range + * start and end + */ + public WeekGridMinuteTimeRange(Date start, Date end) { + this.start = new Date(start.getTime()); + this.end = new Date(end.getTime()); + this.start.setSeconds(0); + this.end.setSeconds(0); + } + + public Date getStart() { + return start; + } + + public Date getEnd() { + return end; + } + + public static boolean doesOverlap(WeekGridMinuteTimeRange a, + WeekGridMinuteTimeRange b) { + boolean overlaps = a.getStart().compareTo(b.getEnd()) < 0 + && a.getEnd().compareTo(b.getStart()) > 0; + return overlaps; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java new file mode 100644 index 0000000000..ae7001cb21 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import com.google.gwt.user.client.ui.Label; + +/** + * A label in the {@link SimpleWeekToolbar} + * + * @since 7.1 + */ +public class WeekLabel extends Label { + private int week; + private int year; + + public WeekLabel(String string, int week2, int year2) { + super(string); + setStylePrimaryName("v-calendar-week-number"); + week = week2; + year = year2; + } + + public int getWeek() { + return week; + } + + public void setWeek(int week) { + this.week = week; + } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java new file mode 100644 index 0000000000..fe1f3e181e --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java @@ -0,0 +1,189 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; +import java.util.List; + +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.vaadin.client.ui.VCalendar; + +/** + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public class WeeklyLongEvents extends HorizontalPanel implements HasTooltipKey { + + public static final int EVENT_HEIGTH = 15; + + public static final int EVENT_MARGIN = 1; + + private int rowCount = 0; + + private VCalendar calendar; + + private boolean undefinedWidth; + + public WeeklyLongEvents(VCalendar calendar) { + setStylePrimaryName("v-calendar-weekly-longevents"); + this.calendar = calendar; + } + + public void addDate(Date d) { + DateCellContainer dcc = new DateCellContainer(); + dcc.setDate(d); + dcc.setCalendar(calendar); + add(dcc); + } + + public void setWidthPX(int width) { + if (getWidgetCount() == 0) { + return; + } + undefinedWidth = (width < 0); + + updateCellWidths(); + } + + public void addEvents(List<CalendarEvent> events) { + for (CalendarEvent e : events) { + addEvent(e); + } + } + + public void addEvent(CalendarEvent calendarEvent) { + updateEventSlot(calendarEvent); + + int dateCount = getWidgetCount(); + Date from = calendarEvent.getStart(); + Date to = calendarEvent.getEnd(); + boolean started = false; + for (int i = 0; i < dateCount; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(from); + int comp2 = dcDate.compareTo(to); + WeeklyLongEventsDateCell eventLabel = dc + .getDateCell(calendarEvent.getSlotIndex()); + eventLabel.setStylePrimaryName("v-calendar-event"); + if (comp >= 0 && comp2 <= 0) { + eventLabel.setEvent(calendarEvent); + eventLabel.setCalendar(calendar); + + eventLabel.addStyleDependentName("all-day"); + if (comp == 0) { + eventLabel.addStyleDependentName("start"); + } + if (comp2 == 0) { + eventLabel.addStyleDependentName("end"); + } + if (!started && comp > 0 && comp2 <= 0) { + eventLabel.addStyleDependentName("continued-from"); + } else if (i == (dateCount - 1)) { + eventLabel.addStyleDependentName("continued-to"); + } + final String extraStyle = calendarEvent.getStyleName(); + if (extraStyle != null && extraStyle.length() > 0) { + eventLabel.addStyleDependentName(extraStyle + "-all-day"); + } + if (!started) { + if (calendar.isEventCaptionAsHtml()) { + eventLabel.setHTML(calendarEvent.getCaption()); + } else { + eventLabel.setText(calendarEvent.getCaption()); + } + started = true; + } + } + } + } + + private void updateEventSlot(CalendarEvent e) { + boolean foundFreeSlot = false; + int slot = 0; + while (!foundFreeSlot) { + if (isSlotFree(slot, e.getStart(), e.getEnd())) { + e.setSlotIndex(slot); + foundFreeSlot = true; + + } else { + slot++; + } + } + } + + private boolean isSlotFree(int slot, Date start, Date end) { + int dateCount = getWidgetCount(); + + // Go over all dates this week + for (int i = 0; i < dateCount; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + Date dcDate = dc.getDate(); + int comp = dcDate.compareTo(start); + int comp2 = dcDate.compareTo(end); + + // check if the date is in the range we need + if (comp >= 0 && comp2 <= 0) { + + // check if the slot is taken + if (dc.hasEvent(slot)) { + return false; + } + } + } + + return true; + } + + public int getRowCount() { + return rowCount; + } + + public void updateCellWidths() { + int cells = getWidgetCount(); + if (cells <= 0) { + return; + } + + int cellWidth = -1; + + // if width is undefined, use the width of the first cell + // otherwise use distributed sizes + if (undefinedWidth) { + cellWidth = calendar.getWeekGrid().getDateCellWidth() + - calendar.getWeekGrid().getDateSlotBorder(); + } + + for (int i = 0; i < cells; i++) { + DateCellContainer dc = (DateCellContainer) getWidget(i); + + if (undefinedWidth) { + dc.setWidth(cellWidth + "px"); + + } else { + dc.setWidth( + calendar.getWeekGrid().getDateCellWidths()[i] + "px"); + } + } + } + + @Override + public String getTooltipKey() { + return null; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java new file mode 100644 index 0000000000..a098ab9c1a --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule; + +import java.util.Date; + +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ui.VCalendar; + +/** + * Represents a cell used in {@link WeeklyLongEvents} + * + * @since 7.1 + */ +public class WeeklyLongEventsDateCell extends HTML implements HasTooltipKey { + private Date date; + private CalendarEvent calendarEvent; + private VCalendar calendar; + + public WeeklyLongEventsDateCell() { + } + + public void setDate(Date date) { + this.date = date; + } + + public Date getDate() { + return date; + } + + public void setEvent(CalendarEvent event) { + calendarEvent = event; + } + + public CalendarEvent getEvent() { + return calendarEvent; + } + + public void setCalendar(VCalendar calendar) { + this.calendar = calendar; + } + + public VCalendar getCalendar() { + return calendar; + } + + @Override + public Object getTooltipKey() { + if (calendarEvent != null) { + return calendarEvent.getIndex(); + } + return null; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java new file mode 100644 index 0000000000..58757b8552 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule.dd; + +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ui.calendar.CalendarConnector; +import com.vaadin.client.ui.dd.VAbstractDropHandler; + +/** + * Abstract base class for calendar drop handlers. + * + * @since 7.1 + * @author Vaadin Ltd. + * + */ +public abstract class CalendarDropHandler extends VAbstractDropHandler { + + protected final CalendarConnector calendarConnector; + + /** + * Constructor + * + * @param connector + * The connector of the calendar + */ + public CalendarDropHandler(CalendarConnector connector) { + calendarConnector = connector; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#getConnector() + */ + @Override + public CalendarConnector getConnector() { + return calendarConnector; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.ui.dd.VDropHandler# + * getApplicationConnection () + */ + @Override + public ApplicationConnection getApplicationConnection() { + return calendarConnector.getClient(); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java new file mode 100644 index 0000000000..663ee1eb98 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java @@ -0,0 +1,171 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule.dd; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.calendar.CalendarConnector; +import com.vaadin.client.ui.calendar.schedule.SimpleDayCell; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragEvent; + +/** + * Handles DD when the monthly view is showing in the Calendar. In the monthly + * view, drops are only allowed in the the day cells. Only the day index is + * included in the drop details sent to the server. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarMonthDropHandler extends CalendarDropHandler { + + public CalendarMonthDropHandler(CalendarConnector connector) { + super(connector); + } + + private Element currentTargetElement; + private SimpleDayCell currentTargetDay; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted + * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + protected void dragAccepted(VDragEvent drag) { + deEmphasis(); + currentTargetElement = drag.getElementOver(); + currentTargetDay = WidgetUtil.findWidget(currentTargetElement, + SimpleDayCell.class); + emphasis(); + } + + /** + * Removed the emphasis CSS style name from the currently emphasized day + */ + private void deEmphasis() { + if (currentTargetElement != null && currentTargetDay != null) { + currentTargetDay.removeEmphasisStyle(); + currentTargetElement = null; + } + } + + /** + * Add CSS style name for the currently emphasized day + */ + private void emphasis() { + if (currentTargetElement != null && currentTargetDay != null) { + currentTargetDay.addEmphasisStyle(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragOver(final VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + } + + /** + * Checks if the one can perform a drop in a element + * + * @param elementOver + * The element to check + * @return + */ + private boolean isLocationValid(Element elementOver) { + Element monthGridElement = calendarConnector.getWidget().getMonthGrid() + .getElement(); + + // drops are not allowed in: + // - weekday header + // - week number bart + return DOM.isOrHasChild(monthGridElement, elementOver); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragEnter(VDragEvent drag) { + // NOOP, we determine drag acceptance in dragOver + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin + * .terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public boolean drop(VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + updateDropDetails(drag); + deEmphasis(); + return super.drop(drag); + + } else { + deEmphasis(); + return false; + } + } + + /** + * Updates the drop details sent to the server + * + * @param drag + * The drag event + */ + private void updateDropDetails(VDragEvent drag) { + int dayIndex = calendarConnector.getWidget().getMonthGrid() + .getDayCellIndex(currentTargetDay); + + drag.getDropDetails().put("dropDayIndex", dayIndex); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(); + super.dragLeave(drag); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java new file mode 100644 index 0000000000..c0ad635ef7 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java @@ -0,0 +1,187 @@ +/* + * Copyright 2000-2016 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.client.ui.calendar.schedule.dd; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.DOM; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.calendar.CalendarConnector; +import com.vaadin.client.ui.calendar.schedule.DateCell; +import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent; +import com.vaadin.client.ui.dd.VAcceptCallback; +import com.vaadin.client.ui.dd.VDragEvent; + +/** + * Handles DD when the weekly view is showing in the Calendar. In the weekly + * view, drops are only allowed in the the time slots for each day. The slot + * index and the day index are included in the drop details sent to the server. + * + * @since 7.1 + * @author Vaadin Ltd. + */ +public class CalendarWeekDropHandler extends CalendarDropHandler { + + private Element currentTargetElement; + private DateCell currentTargetDay; + + public CalendarWeekDropHandler(CalendarConnector connector) { + super(connector); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted + * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + protected void dragAccepted(VDragEvent drag) { + deEmphasis(); + currentTargetElement = drag.getElementOver(); + currentTargetDay = WidgetUtil.findWidget(currentTargetElement, + DateCell.class); + emphasis(); + } + + /** + * Removes the CSS style name from the emphasized element + */ + private void deEmphasis() { + if (currentTargetElement != null) { + currentTargetDay.removeEmphasisStyle(currentTargetElement); + currentTargetElement = null; + } + } + + /** + * Add a CSS stylen name to current target element + */ + private void emphasis() { + currentTargetDay.addEmphasisStyle(currentTargetElement); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragOver(final VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + validate(new VAcceptCallback() { + @Override + public void accepted(VDragEvent event) { + dragAccepted(drag); + } + }, drag); + } + } + + /** + * Checks if the location is a valid drop location + * + * @param elementOver + * The element to check + * @return + */ + private boolean isLocationValid(Element elementOver) { + Element weekGridElement = calendarConnector.getWidget().getWeekGrid() + .getElement(); + Element timeBarElement = calendarConnector.getWidget().getWeekGrid() + .getTimeBar().getElement(); + + Element todayBarElement = null; + if (calendarConnector.getWidget().getWeekGrid().hasToday()) { + todayBarElement = calendarConnector.getWidget().getWeekGrid() + .getDateCellOfToday().getTodaybarElement(); + } + + // drops are not allowed in: + // - weekday header + // - allday event list + // - todaybar + // - timebar + // - events + return DOM.isOrHasChild(weekGridElement, elementOver) + && !DOM.isOrHasChild(timeBarElement, elementOver) + && todayBarElement != elementOver + && (WidgetUtil.findWidget(elementOver, + DateCellDayEvent.class) == null); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragEnter(VDragEvent drag) { + // NOOP, we determine drag acceptance in dragOver + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin + * .terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public boolean drop(VDragEvent drag) { + if (isLocationValid(drag.getElementOver())) { + updateDropDetails(drag); + deEmphasis(); + return super.drop(drag); + + } else { + deEmphasis(); + return false; + } + } + + /** + * Update the drop details sent to the server + * + * @param drag + * The drag event + */ + private void updateDropDetails(VDragEvent drag) { + int slotIndex = currentTargetDay.getSlotIndex(currentTargetElement); + int dayIndex = calendarConnector.getWidget().getWeekGrid() + .getDateCellIndex(currentTargetDay); + + drag.getDropDetails().put("dropDayIndex", dayIndex); + drag.getDropDetails().put("dropSlotIndex", slotIndex); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com + * .vaadin.terminal.gwt.client.ui.dd.VDragEvent) + */ + @Override + public void dragLeave(VDragEvent drag) { + deEmphasis(); + super.dragLeave(drag); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java new file mode 100644 index 0000000000..6f480538e0 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java @@ -0,0 +1,113 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.HasClickHandlers; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.colorpicker.ColorPickerState; + +/** + * An abstract class that defines default implementation for a color picker + * connector. + * + * @since 7.0.0 + */ +public abstract class AbstractColorPickerConnector + extends AbstractComponentConnector implements ClickHandler { + + private static final String DEFAULT_WIDTH_STYLE = "v-default-caption-width"; + + @Override + public ColorPickerState getState() { + return (ColorPickerState) super.getState(); + } + + @Override + public boolean delegateCaptionHandling() { + return false; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + // NOTE: this method is called after @DelegateToWidget + super.onStateChanged(stateChangeEvent); + if (stateChangeEvent.hasPropertyChanged("color")) { + refreshColor(); + + if (getState().showDefaultCaption && (getState().caption == null + || "".equals(getState().caption))) { + + setCaption(getState().color); + } + } + if (stateChangeEvent.hasPropertyChanged("caption") + || stateChangeEvent.hasPropertyChanged("htmlContentAllowed") + || stateChangeEvent.hasPropertyChanged("showDefaultCaption")) { + + setCaption(getCaption()); + refreshDefaultCaptionStyle(); + } + } + + @Override + public void init() { + super.init(); + if (getWidget() instanceof HasClickHandlers) { + ((HasClickHandlers) getWidget()).addClickHandler(this); + } + } + + /** + * Get caption for the color picker widget. + * + * @return + */ + protected String getCaption() { + if (getState().showDefaultCaption && (getState().caption == null + || "".equals(getState().caption))) { + return getState().color; + } + return getState().caption; + } + + /** + * Add/remove default caption style. + */ + protected void refreshDefaultCaptionStyle() { + if (getState().showDefaultCaption + && (getState().caption == null || getState().caption.isEmpty()) + && getState().width.isEmpty()) { + getWidget().addStyleName(DEFAULT_WIDTH_STYLE); + } else { + getWidget().removeStyleName(DEFAULT_WIDTH_STYLE); + } + } + + /** + * Set caption of the color picker widget. + * + * @param caption + */ + protected abstract void setCaption(String caption); + + /** + * Update the widget to show the currently selected color. + */ + protected abstract void refreshColor(); + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java new file mode 100644 index 0000000000..828cc689c7 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.VCaption; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.ui.VColorPickerArea; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc; +import com.vaadin.ui.ColorPickerArea; + +/** + * A class that defines an implementation for a color picker connector. Connects + * the server side {@link com.vaadin.ui.ColorPickerArea} with the client side + * counterpart {@link VColorPickerArea} + * + * @since 7.0.0 + */ +@Connect(value = ColorPickerArea.class, loadStyle = LoadStyle.LAZY) +public class ColorPickerAreaConnector extends AbstractColorPickerConnector { + + private ColorPickerServerRpc rpc = RpcProxy + .create(ColorPickerServerRpc.class, this); + + @Override + protected Widget createWidget() { + return GWT.create(VColorPickerArea.class); + } + + @Override + public VColorPickerArea getWidget() { + return (VColorPickerArea) super.getWidget(); + } + + @Override + public void onClick(ClickEvent event) { + rpc.openPopup(getWidget().isOpen()); + } + + @Override + protected void setCaption(String caption) { + VCaption.setCaptionText(getWidget(), getState()); + } + + @Override + protected void refreshColor() { + getWidget().refreshColor(); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java new file mode 100644 index 0000000000..6254e7adbe --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.ui.VColorPicker; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc; +import com.vaadin.ui.ColorPicker; + +/** + * A class that defines default implementation for a color picker connector. + * Connects the server side {@link com.vaadin.ui.ColorPicker} with the client + * side counterpart {@link VColorPicker} + * + * @since 7.0.0 + */ +@Connect(value = ColorPicker.class, loadStyle = LoadStyle.LAZY) +public class ColorPickerConnector extends AbstractColorPickerConnector { + + private ColorPickerServerRpc rpc = RpcProxy + .create(ColorPickerServerRpc.class, this); + + @Override + protected Widget createWidget() { + return GWT.create(VColorPicker.class); + } + + @Override + public VColorPicker getWidget() { + return (VColorPicker) super.getWidget(); + } + + @Override + public void onClick(ClickEvent event) { + rpc.openPopup(getWidget().isOpen()); + } + + @Override + protected void setCaption(String caption) { + if (getState().captionAsHtml) { + getWidget().setHtml(caption); + } else { + getWidget().setText(caption); + } + } + + @Override + protected void refreshColor() { + getWidget().refreshColor(); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java new file mode 100644 index 0000000000..c05449d7bd --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.colorpicker.ColorPickerGradientServerRpc; +import com.vaadin.shared.ui.colorpicker.ColorPickerGradientState; +import com.vaadin.ui.components.colorpicker.ColorPickerGradient; + +/** + * A class that defines the default implementation for a color picker gradient + * connector. Connects the server side + * {@link com.vaadin.ui.components.colorpicker.ColorPickerGradient} with the + * client side counterpart {@link VColorPickerGradient} + * + * @since 7.0.0 + */ +@Connect(value = ColorPickerGradient.class, loadStyle = LoadStyle.LAZY) +public class ColorPickerGradientConnector extends AbstractComponentConnector + implements MouseUpHandler { + + private ColorPickerGradientServerRpc rpc = RpcProxy + .create(ColorPickerGradientServerRpc.class, this); + + @Override + protected Widget createWidget() { + return GWT.create(VColorPickerGradient.class); + } + + @Override + public VColorPickerGradient getWidget() { + return (VColorPickerGradient) super.getWidget(); + } + + @Override + public ColorPickerGradientState getState() { + return (ColorPickerGradientState) super.getState(); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + rpc.select(getWidget().getCursorX(), getWidget().getCursorY()); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + if (stateChangeEvent.hasPropertyChanged("cursorX") + || stateChangeEvent.hasPropertyChanged("cursorY")) { + + getWidget().setCursor(getState().cursorX, getState().cursorY); + } + if (stateChangeEvent.hasPropertyChanged("bgColor")) { + getWidget().setBGColor(getState().bgColor); + } + } + + @Override + protected void init() { + super.init(); + getWidget().addMouseUpHandler(this); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java new file mode 100644 index 0000000000..cd0a3d1466 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.shared.ui.colorpicker.ColorPickerGridServerRpc; +import com.vaadin.shared.ui.colorpicker.ColorPickerGridState; +import com.vaadin.ui.components.colorpicker.ColorPickerGrid; + +/** + * A class that defines the default implementation for a color picker grid + * connector. Connects the server side + * {@link com.vaadin.ui.components.colorpicker.ColorPickerGrid} with the client + * side counterpart {@link VColorPickerGrid} + * + * @since 7.0.0 + */ +@Connect(value = ColorPickerGrid.class, loadStyle = LoadStyle.LAZY) +public class ColorPickerGridConnector extends AbstractComponentConnector + implements ClickHandler { + + private ColorPickerGridServerRpc rpc = RpcProxy + .create(ColorPickerGridServerRpc.class, this); + + @Override + protected Widget createWidget() { + return GWT.create(VColorPickerGrid.class); + } + + @Override + public VColorPickerGrid getWidget() { + return (VColorPickerGrid) super.getWidget(); + } + + @Override + public ColorPickerGridState getState() { + return (ColorPickerGridState) super.getState(); + } + + @Override + public void onClick(ClickEvent event) { + rpc.select(getWidget().getSelectedX(), getWidget().getSelectedY()); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + if (stateChangeEvent.hasPropertyChanged("rowCount") + || stateChangeEvent.hasPropertyChanged("columnCount") + || stateChangeEvent.hasPropertyChanged("updateGrid")) { + + getWidget().updateGrid(getState().rowCount, getState().columnCount); + } + if (stateChangeEvent.hasPropertyChanged("changedX") + || stateChangeEvent.hasPropertyChanged("changedY") + || stateChangeEvent.hasPropertyChanged("changedColor") + || stateChangeEvent.hasPropertyChanged("updateColor")) { + + getWidget().updateColor(getState().changedColor, + getState().changedX, getState().changedY); + + if (!getWidget().isGridLoaded()) { + rpc.refresh(); + } + } + } + + @Override + protected void init() { + super.init(); + getWidget().addClickHandler(this); + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java new file mode 100644 index 0000000000..b75bee23c1 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java @@ -0,0 +1,211 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.MouseDownEvent; +import com.google.gwt.event.dom.client.MouseDownHandler; +import com.google.gwt.event.dom.client.MouseMoveEvent; +import com.google.gwt.event.dom.client.MouseMoveHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ui.SubPartAware; + +/** + * Client side implementation for ColorPickerGradient. + * + * @since 7.0.0 + * + */ +public class VColorPickerGradient extends FocusPanel implements + MouseDownHandler, MouseUpHandler, MouseMoveHandler, SubPartAware { + + /** Set the CSS class name to allow styling. */ + public static final String CLASSNAME = "v-colorpicker-gradient"; + public static final String CLASSNAME_BACKGROUND = CLASSNAME + "-background"; + public static final String CLASSNAME_FOREGROUND = CLASSNAME + "-foreground"; + public static final String CLASSNAME_LOWERBOX = CLASSNAME + "-lowerbox"; + public static final String CLASSNAME_HIGHERBOX = CLASSNAME + "-higherbox"; + public static final String CLASSNAME_CONTAINER = CLASSNAME + "-container"; + public static final String CLASSNAME_CLICKLAYER = CLASSNAME + "-clicklayer"; + private static final String CLICKLAYER_ID = "clicklayer"; + + private final HTML background; + private final HTML foreground; + private final HTML lowercross; + private final HTML highercross; + private final HTML clicklayer; + private final AbsolutePanel container; + + private boolean mouseIsDown = false; + + private int cursorX; + private int cursorY; + + private int width = 220; + private int height = 220; + + /** + * Instantiates the client side component for a color picker gradient. + */ + public VColorPickerGradient() { + super(); + + setStyleName(CLASSNAME); + + background = new HTML(); + background.setStyleName(CLASSNAME_BACKGROUND); + background.setPixelSize(width, height); + + foreground = new HTML(); + foreground.setStyleName(CLASSNAME_FOREGROUND); + foreground.setPixelSize(width, height); + + clicklayer = new HTML(); + clicklayer.setStyleName(CLASSNAME_CLICKLAYER); + clicklayer.setPixelSize(width, height); + clicklayer.addMouseDownHandler(this); + clicklayer.addMouseUpHandler(this); + clicklayer.addMouseMoveHandler(this); + + lowercross = new HTML(); + lowercross.setPixelSize(width / 2, height / 2); + lowercross.setStyleName(CLASSNAME_LOWERBOX); + + highercross = new HTML(); + highercross.setPixelSize(width / 2, height / 2); + highercross.setStyleName(CLASSNAME_HIGHERBOX); + + container = new AbsolutePanel(); + container.setStyleName(CLASSNAME_CONTAINER); + container.setPixelSize(width, height); + container.add(background, 0, 0); + container.add(foreground, 0, 0); + container.add(lowercross, 0, height / 2); + container.add(highercross, width / 2, 0); + container.add(clicklayer, 0, 0); + + add(container); + } + + /** + * Returns the latest x-coordinate for pressed-down mouse cursor. + */ + protected int getCursorX() { + return cursorX; + } + + /** + * Returns the latest y-coordinate for pressed-down mouse cursor. + */ + protected int getCursorY() { + return cursorY; + } + + /** + * Sets the given css color as the background. + * + * @param bgColor + */ + protected void setBGColor(String bgColor) { + if (bgColor == null) { + background.getElement().getStyle().clearBackgroundColor(); + } else { + background.getElement().getStyle().setBackgroundColor(bgColor); + } + } + + @Override + public void onMouseDown(MouseDownEvent event) { + event.preventDefault(); + + mouseIsDown = true; + setCursor(event.getX(), event.getY()); + } + + @Override + public void onMouseUp(MouseUpEvent event) { + event.preventDefault(); + mouseIsDown = false; + setCursor(event.getX(), event.getY()); + + cursorX = event.getX(); + cursorY = event.getY(); + } + + @Override + public void onMouseMove(MouseMoveEvent event) { + event.preventDefault(); + + if (mouseIsDown) { + setCursor(event.getX(), event.getY()); + } + } + + /** + * Sets the latest coordinates for pressed-down mouse cursor and updates the + * cross elements. + * + * @param x + * @param y + */ + public void setCursor(int x, int y) { + cursorX = x; + cursorY = y; + if (x >= 0) { + lowercross.getElement().getStyle().setWidth(x, Unit.PX); + } + if (y >= 0) { + lowercross.getElement().getStyle().setTop(y, Unit.PX); + } + if (y >= 0) { + lowercross.getElement().getStyle().setHeight(height - y, Unit.PX); + } + + if (x >= 0) { + highercross.getElement().getStyle().setWidth(width - x, Unit.PX); + } + if (x >= 0) { + highercross.getElement().getStyle().setLeft(x, Unit.PX); + } + if (y >= 0) { + highercross.getElement().getStyle().setHeight(y, Unit.PX); + } + } + + @Override + public com.google.gwt.user.client.Element getSubPartElement( + String subPart) { + if (subPart.equals(CLICKLAYER_ID)) { + return clicklayer.getElement(); + } + + return null; + } + + @Override + public String getSubPartName( + com.google.gwt.user.client.Element subElement) { + if (clicklayer.getElement().isOrHasChild(subElement)) { + return CLICKLAYER_ID; + } + + return null; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java new file mode 100644 index 0000000000..67f2ce07dd --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java @@ -0,0 +1,147 @@ +/* + * Copyright 2000-2016 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.client.ui.colorpicker; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.HasClickHandlers; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.AbsolutePanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HTMLTable.Cell; + +/** + * Client side implementation for ColorPickerGrid. + * + * @since 7.0.0 + * + */ +public class VColorPickerGrid extends AbsolutePanel + implements ClickHandler, HasClickHandlers { + + private int rows = 1; + private int columns = 1; + + private Grid grid; + + private boolean gridLoaded = false; + + private int selectedX; + private int selectedY; + + /** + * Instantiates the client side component for a color picker grid. + */ + public VColorPickerGrid() { + super(); + + this.add(createGrid(), 0, 0); + } + + /** + * Creates a grid according to the current row and column count information. + * + * @return grid + */ + private Grid createGrid() { + grid = new Grid(rows, columns); + grid.setWidth("100%"); + grid.setHeight("100%"); + grid.addClickHandler(this); + return grid; + } + + /** + * Updates the row and column count and creates a new grid based on them. + * The new grid replaces the old grid if one existed. + * + * @param rowCount + * @param columnCount + */ + protected void updateGrid(int rowCount, int columnCount) { + rows = rowCount; + columns = columnCount; + this.remove(grid); + this.add(createGrid(), 0, 0); + } + + /** + * Updates the changed colors within the grid based on the given x- and + * y-coordinates. Nothing happens if any of the parameters is null or the + * parameter lengths don't match. + * + * @param changedColor + * @param changedX + * @param changedY + */ + protected void updateColor(String[] changedColor, String[] changedX, + String[] changedY) { + if (changedColor != null && changedX != null && changedY != null) { + if (changedColor.length == changedX.length + && changedX.length == changedY.length) { + for (int c = 0; c < changedColor.length; c++) { + Element element = grid.getCellFormatter().getElement( + Integer.parseInt(changedX[c]), + Integer.parseInt(changedY[c])); + element.getStyle().setProperty("background", + changedColor[c]); + } + } + + gridLoaded = true; + } + } + + /** + * Returns currently selected x-coordinate of the grid. + */ + protected int getSelectedX() { + return selectedX; + } + + /** + * Returns currently selected y-coordinate of the grid. + */ + protected int getSelectedY() { + return selectedY; + } + + /** + * Returns true if the colors have been successfully updated at least once, + * false otherwise. + */ + protected boolean isGridLoaded() { + return gridLoaded; + } + + @Override + public void onClick(ClickEvent event) { + Cell cell = grid.getCellForEvent(event); + if (cell == null) { + return; + } + + selectedY = cell.getRowIndex(); + selectedX = cell.getCellIndex(); + } + + @Override + public HandlerRegistration addClickHandler(ClickHandler handler) { + return addDomHandler(handler, ClickEvent.getType()); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java new file mode 100644 index 0000000000..f6dff893f5 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java @@ -0,0 +1,378 @@ +/* + * Copyright 2000-2016 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.client.ui.combobox; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.Paintable; +import com.vaadin.client.Profiler; +import com.vaadin.client.UIDL; +import com.vaadin.client.communication.RpcProxy; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractFieldConnector; +import com.vaadin.client.ui.SimpleManagedLayout; +import com.vaadin.client.ui.VFilterSelect; +import com.vaadin.client.ui.VFilterSelect.DataReceivedHandler; +import com.vaadin.client.ui.VFilterSelect.FilterSelectSuggestion; +import com.vaadin.shared.EventId; +import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.combobox.ComboBoxServerRpc; +import com.vaadin.shared.ui.combobox.ComboBoxState; +import com.vaadin.ui.ComboBox; + +@Connect(ComboBox.class) +public class ComboBoxConnector extends AbstractFieldConnector + implements Paintable, SimpleManagedLayout { + + protected ComboBoxServerRpc rpc = RpcProxy.create(ComboBoxServerRpc.class, + this); + + protected FocusAndBlurServerRpc focusAndBlurRpc = RpcProxy + .create(FocusAndBlurServerRpc.class, this); + + @Override + protected void init() { + super.init(); + getWidget().connector = this; + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + Profiler.enter("ComboBoxConnector.onStateChanged update content"); + + getWidget().readonly = isReadOnly(); + getWidget().updateReadOnly(); + + getWidget().setTextInputEnabled(getState().textInputAllowed); + + if (getState().inputPrompt != null) { + getWidget().inputPrompt = getState().inputPrompt; + } else { + getWidget().inputPrompt = ""; + } + + getWidget().pageLength = getState().pageLength; + + getWidget().filteringmode = getState().filteringMode; + + getWidget().suggestionPopupWidth = getState().suggestionPopupWidth; + + Profiler.leave("ComboBoxConnector.onStateChanged update content"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.Paintable#updateFromUIDL(com.vaadin.client.UIDL, + * com.vaadin.client.ApplicationConnection) + */ + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + // not a FocusWidget -> needs own tabindex handling + getWidget().tb.setTabIndex(getState().tabIndex); + + getWidget().nullSelectionAllowed = uidl.hasAttribute("nullselect"); + + getWidget().nullSelectItem = uidl.hasAttribute("nullselectitem") + && uidl.getBooleanAttribute("nullselectitem"); + + getWidget().currentPage = uidl.getIntVariable("page"); + + getWidget().suggestionPopup.updateStyleNames(getState()); + + getWidget().allowNewItem = uidl.hasAttribute("allownewitem"); + getWidget().lastNewItemString = null; + + final UIDL options = uidl.getChildUIDL(0); + if (uidl.hasAttribute("totalMatches")) { + getWidget().totalMatches = uidl.getIntAttribute("totalMatches"); + } else { + getWidget().totalMatches = 0; + } + + List<FilterSelectSuggestion> newSuggestions = new ArrayList<FilterSelectSuggestion>(); + + for (final Iterator<?> i = options.getChildIterator(); i.hasNext();) { + final UIDL optionUidl = (UIDL) i.next(); + String key = optionUidl.getStringAttribute("key"); + String caption = optionUidl.getStringAttribute("caption"); + String style = optionUidl.getStringAttribute("style"); + + String untranslatedIconUri = null; + if (optionUidl.hasAttribute("icon")) { + untranslatedIconUri = optionUidl.getStringAttribute("icon"); + } + + final FilterSelectSuggestion suggestion = getWidget().new FilterSelectSuggestion( + key, caption, style, untranslatedIconUri); + newSuggestions.add(suggestion); + } + + // only close the popup if the suggestions list has actually changed + boolean suggestionsChanged = !getWidget().initDone + || !newSuggestions.equals(getWidget().currentSuggestions); + + // An ItemSetChangeEvent on server side clears the current suggestion + // popup. Popup needs to be repopulated with suggestions from UIDL. + boolean popupOpenAndCleared = false; + + // oldSuggestionTextMatchTheOldSelection is used to detect when it's + // safe to update textbox text by a changed item caption. + boolean oldSuggestionTextMatchesTheOldSelection = false; + + if (suggestionsChanged) { + oldSuggestionTextMatchesTheOldSelection = isWidgetsCurrentSelectionTextInTextBox(); + getWidget().currentSuggestions.clear(); + + if (!getDataReceivedHandler().isWaitingForFilteringResponse()) { + /* + * Clear the current suggestions as the server response always + * includes the new ones. Exception is when filtering, then we + * need to retain the value if the user does not select any of + * the options matching the filter. + */ + getWidget().currentSuggestion = null; + /* + * Also ensure no old items in menu. Unless cleared the old + * values may cause odd effects on blur events. Suggestions in + * menu might not necessary exist in select at all anymore. + */ + getWidget().suggestionPopup.menu.clearItems(); + popupOpenAndCleared = getWidget().suggestionPopup.isAttached(); + + } + + for (FilterSelectSuggestion suggestion : newSuggestions) { + getWidget().currentSuggestions.add(suggestion); + } + } + + // handle selection (null or a single value) + if (uidl.hasVariable("selected") + + // In case we're switching page no need to update the selection as the + // selection process didn't finish. + // && getWidget().selectPopupItemWhenResponseIsReceived == + // VFilterSelect.Select.NONE + // + ) { + + // single selected key (can be empty string) or empty array for null + // selection + String[] selectedKeys = uidl.getStringArrayVariable("selected"); + String selectedKey = null; + if (selectedKeys.length == 1) { + selectedKey = selectedKeys[0]; + } + // selected item caption in case it is not on the current page + String selectedCaption = null; + if (uidl.hasAttribute("selectedCaption")) { + selectedCaption = uidl.getStringAttribute("selectedCaption"); + } + + getDataReceivedHandler().updateSelectionFromServer(selectedKey, + selectedCaption, oldSuggestionTextMatchesTheOldSelection); + } + + // TODO even this condition should probably be moved to the handler + if ((getDataReceivedHandler().isWaitingForFilteringResponse() + && getWidget().lastFilter.toLowerCase() + .equals(uidl.getStringVariable("filter"))) + || popupOpenAndCleared) { + getDataReceivedHandler().dataReceived(); + } + + // Calculate minimum textarea width + getWidget().updateSuggestionPopupMinWidth(); + + /* + * if this is our first time we need to recalculate the root width. + */ + if (!getWidget().initDone) { + + getWidget().updateRootWidth(); + } + + // Focus dependent style names are lost during the update, so we add + // them here back again + if (getWidget().focused) { + getWidget().addStyleDependentName("focus"); + } + + getWidget().initDone = true; + + // TODO this should perhaps be moved to be a part of dataReceived() + getDataReceivedHandler().serverReplyHandled(); + } + + private boolean isWidgetsCurrentSelectionTextInTextBox() { + return getWidget().currentSuggestion != null + && getWidget().currentSuggestion.getReplacementString() + .equals(getWidget().tb.getText()); + } + + @Override + public VFilterSelect getWidget() { + return (VFilterSelect) super.getWidget(); + } + + private DataReceivedHandler getDataReceivedHandler() { + return getWidget().getDataReceivedHandler(); + } + + @Override + public ComboBoxState getState() { + return (ComboBoxState) super.getState(); + } + + @Override + public void layout() { + VFilterSelect widget = getWidget(); + if (widget.initDone) { + widget.updateRootWidth(); + } + } + + @Override + public void setWidgetEnabled(boolean widgetEnabled) { + super.setWidgetEnabled(widgetEnabled); + getWidget().enabled = widgetEnabled; + getWidget().tb.setEnabled(widgetEnabled); + } + + /* + * These methods exist to move communications out of VFilterSelect, and may + * be refactored/removed in the future + */ + + /** + * Send a message about a newly created item to the server. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + * @param itemValue + * user entered string value for the new item + */ + public void sendNewItem(String itemValue) { + rpc.createNewItem(itemValue); + afterSendRequestToServer(); + } + + /** + * Send a message to the server to request the first page of items without + * filtering or selection. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + */ + public void requestFirstPage() { + sendSelection(null); + requestPage("", 0); + } + + /** + * Send a message to the server to request a page of items with a given + * filter. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + * @param filter + * the current filter string + * @param page + * the page number to get + */ + public void requestPage(String filter, int page) { + rpc.requestPage(filter, page); + afterSendRequestToServer(); + } + + /** + * Send a message to the server updating the current selection. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + * @param selection + * the current selection + */ + public void sendSelection(String selection) { + rpc.setSelectedItem(selection); + afterSendRequestToServer(); + } + + /** + * Notify the server that the combo box received focus. + * + * For timing reasons, ConnectorFocusAndBlurHandler is not used at the + * moment. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + */ + public void sendFocusEvent() { + boolean registeredListeners = hasEventListener(EventId.FOCUS); + if (registeredListeners) { + focusAndBlurRpc.focus(); + afterSendRequestToServer(); + } + } + + /** + * Notify the server that the combo box lost focus. + * + * For timing reasons, ConnectorFocusAndBlurHandler is not used at the + * moment. + * + * This method is for internal use only and may be removed in future + * versions. + * + * @since + */ + public void sendBlurEvent() { + boolean registeredListeners = hasEventListener(EventId.BLUR); + if (registeredListeners) { + focusAndBlurRpc.blur(); + afterSendRequestToServer(); + } + } + + /* + * Called after any request to server. + */ + private void afterSendRequestToServer() { + getDataReceivedHandler().anyRequestSentToServer(); + } + +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java new file mode 100644 index 0000000000..b02f8df147 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2016 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.client.ui.dd; + +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.UIDL; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.ui.AbstractSelect; + +@AcceptCriterion(AbstractSelect.TargetItemIs.class) +final public class VIsOverId extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + try { + + String pid = configuration.getStringAttribute("s"); + VDropHandler currentDropHandler = VDragAndDropManager.get() + .getCurrentDropHandler(); + ComponentConnector dropHandlerConnector = currentDropHandler + .getConnector(); + + String pid2 = dropHandlerConnector.getConnectorId(); + if (pid2.equals(pid)) { + Object searchedId = drag.getDropDetails().get("itemIdOver"); + String[] stringArrayAttribute = configuration + .getStringArrayAttribute("keys"); + for (String string : stringArrayAttribute) { + if (string.equals(searchedId)) { + return true; + } + } + } + } catch (Exception e) { + } + return false; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java new file mode 100644 index 0000000000..6992ebcaad --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2016 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.client.ui.dd; + +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.UIDL; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.ui.AbstractSelect; + +@AcceptCriterion(AbstractSelect.AcceptItem.class) +final public class VItemIdIs extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + try { + String pid = configuration.getStringAttribute("s"); + ComponentConnector dragSource = drag.getTransferable() + .getDragSource(); + String pid2 = dragSource.getConnectorId(); + if (pid2.equals(pid)) { + Object searchedId = drag.getTransferable().getData("itemId"); + String[] stringArrayAttribute = configuration + .getStringArrayAttribute("keys"); + for (String string : stringArrayAttribute) { + if (string.equals(searchedId)) { + return true; + } + } + } + } catch (Exception e) { + } + return false; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java new file mode 100644 index 0000000000..c28a58aaad --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java @@ -0,0 +1,392 @@ +/* + * Copyright 2000-2016 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.client.ui.tree; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.google.gwt.aria.client.Roles; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.EventTarget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.BrowserInfo; +import com.vaadin.client.Paintable; +import com.vaadin.client.TooltipInfo; +import com.vaadin.client.UIDL; +import com.vaadin.client.VConsole; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.client.ui.VTree; +import com.vaadin.client.ui.VTree.TreeNode; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.shared.ui.tree.TreeConstants; +import com.vaadin.shared.ui.tree.TreeServerRpc; +import com.vaadin.shared.ui.tree.TreeState; +import com.vaadin.ui.Tree; + +@Connect(Tree.class) +public class TreeConnector extends AbstractComponentConnector + implements Paintable { + + protected final Map<TreeNode, TooltipInfo> tooltipMap = new HashMap<TreeNode, TooltipInfo>(); + + @Override + protected void init() { + getWidget().connector = this; + } + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + if (!isRealUpdate(uidl)) { + return; + } + + getWidget().rendering = true; + + getWidget().client = client; + + if (uidl.hasAttribute("partialUpdate")) { + handleUpdate(uidl); + + getWidget().rendering = false; + return; + } + + getWidget().paintableId = uidl.getId(); + + getWidget().immediate = getState().immediate; + + getWidget().disabled = !isEnabled(); + getWidget().readonly = isReadOnly(); + + getWidget().dragMode = uidl.hasAttribute("dragMode") + ? uidl.getIntAttribute("dragMode") : 0; + + getWidget().isNullSelectionAllowed = uidl + .getBooleanAttribute("nullselect"); + getWidget().isHtmlContentAllowed = uidl + .getBooleanAttribute(TreeConstants.ATTRIBUTE_HTML_ALLOWED); + + if (uidl.hasAttribute("alb")) { + getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb"); + } + + getWidget().body.clear(); + // clear out any references to nodes that no longer are attached + getWidget().clearNodeToKeyMap(); + tooltipMap.clear(); + + TreeNode childTree = null; + UIDL childUidl = null; + for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) { + childUidl = (UIDL) i.next(); + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } else if ("-ac".equals(childUidl.getTag())) { + getWidget().updateDropHandler(childUidl); + continue; + } + childTree = getWidget().new TreeNode(); + getConnection().getVTooltip().connectHandlersToWidget(childTree); + updateNodeFromUIDL(childTree, childUidl, 1); + getWidget().body.add(childTree); + childTree.addStyleDependentName("root"); + childTree.childNodeContainer.addStyleDependentName("root"); + } + if (childTree != null && childUidl != null) { + boolean leaf = !childUidl.getTag().equals("node"); + childTree.addStyleDependentName(leaf ? "leaf-last" : "last"); + childTree.childNodeContainer.addStyleDependentName("last"); + } + final String selectMode = uidl.getStringAttribute("selectmode"); + getWidget().selectable = !"none".equals(selectMode); + getWidget().isMultiselect = "multi".equals(selectMode); + + if (getWidget().isMultiselect) { + Roles.getTreeRole().setAriaMultiselectableProperty( + getWidget().getElement(), true); + + if (BrowserInfo.get().isTouchDevice()) { + // Always use the simple mode for touch devices that do not have + // shift/ctrl keys (#8595) + getWidget().multiSelectMode = MultiSelectMode.SIMPLE; + } else { + getWidget().multiSelectMode = MultiSelectMode + .valueOf(uidl.getStringAttribute("multiselectmode")); + } + } else { + Roles.getTreeRole().setAriaMultiselectableProperty( + getWidget().getElement(), false); + } + + getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected"); + + // Update lastSelection and focusedNode to point to *actual* nodes again + // after the old ones have been cleared from the body. This fixes focus + // and keyboard navigation issues as described in #7057 and other + // tickets. + if (getWidget().lastSelection != null) { + getWidget().lastSelection = getWidget() + .getNodeByKey(getWidget().lastSelection.key); + } + + if (getWidget().focusedNode != null) { + + Set<String> selectedIds = getWidget().selectedIds; + + // If the focused node is not between the selected nodes, we need to + // refresh the focused node to prevent an undesired scroll. #12618. + if (!selectedIds.isEmpty() + && !selectedIds.contains(getWidget().focusedNode.key)) { + String keySelectedId = selectedIds.iterator().next(); + + TreeNode nodeToSelect = getWidget().getNodeByKey(keySelectedId); + + getWidget().setFocusedNode(nodeToSelect); + } else { + getWidget().setFocusedNode( + getWidget().getNodeByKey(getWidget().focusedNode.key)); + } + } + + if (getWidget().lastSelection == null && getWidget().focusedNode == null + && !getWidget().selectedIds.isEmpty()) { + getWidget().setFocusedNode(getWidget() + .getNodeByKey(getWidget().selectedIds.iterator().next())); + getWidget().focusedNode.setFocused(false); + } + + getWidget().rendering = false; + + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + // VTree does not implement Focusable + getWidget().setTabIndex(getState().tabIndex); + } + + @Override + public VTree getWidget() { + return (VTree) super.getWidget(); + } + + private void handleUpdate(UIDL uidl) { + final TreeNode rootNode = getWidget() + .getNodeByKey(uidl.getStringAttribute("rootKey")); + if (rootNode != null) { + if (!rootNode.getState()) { + // expanding node happened server side + rootNode.setState(true, false); + } + String levelPropertyString = Roles.getTreeitemRole() + .getAriaLevelProperty(rootNode.getElement()); + int levelProperty; + try { + levelProperty = Integer.valueOf(levelPropertyString); + } catch (NumberFormatException e) { + levelProperty = 1; + VConsole.error(e); + } + + renderChildNodes(rootNode, (Iterator) uidl.getChildIterator(), + levelProperty + 1); + } + } + + /** + * Registers action for the root and also for individual nodes + * + * @param uidl + */ + private void updateActionMap(UIDL uidl) { + final Iterator<?> it = uidl.getChildIterator(); + while (it.hasNext()) { + final UIDL action = (UIDL) it.next(); + final String key = action.getStringAttribute("key"); + final String caption = action + .getStringAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION); + String iconUrl = null; + if (action.hasAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON)) { + iconUrl = getConnection() + .translateVaadinUri(action.getStringAttribute( + TreeConstants.ATTRIBUTE_ACTION_ICON)); + } + getWidget().registerAction(key, caption, iconUrl); + } + + } + + public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl, int level) { + Roles.getTreeitemRole().setAriaLevelProperty(treeNode.getElement(), + level); + + String nodeKey = uidl.getStringAttribute("key"); + String caption = uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION); + if (getWidget().isHtmlContentAllowed) { + treeNode.setHtml(caption); + } else { + treeNode.setText(caption); + } + treeNode.key = nodeKey; + + getWidget().registerNode(treeNode); + + if (uidl.hasAttribute("al")) { + treeNode.actionKeys = uidl.getStringArrayAttribute("al"); + } + + if (uidl.getTag().equals("node")) { + if (uidl.getChildCount() == 0) { + treeNode.childNodeContainer.setVisible(false); + } else { + renderChildNodes(treeNode, (Iterator) uidl.getChildIterator(), + level + 1); + treeNode.childrenLoaded = true; + } + } else { + treeNode.addStyleName(TreeNode.CLASSNAME + "-leaf"); + } + if (uidl.hasAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE)) { + treeNode.setNodeStyleName(uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE)); + } + + String description = uidl.getStringAttribute("descr"); + if (description != null) { + tooltipMap.put(treeNode, + new TooltipInfo(description, null, treeNode)); + } + + if (uidl.getBooleanAttribute("expanded") && !treeNode.getState()) { + treeNode.setState(true, false); + } + + if (uidl.getBooleanAttribute("selected")) { + treeNode.setSelected(true); + // ensure that identifier is in selectedIds array (this may be a + // partial update) + getWidget().selectedIds.add(nodeKey); + } + + String iconUrl = uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON); + String iconAltText = uidl + .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT); + treeNode.setIcon(iconUrl, iconAltText); + } + + void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i, int level) { + containerNode.childNodeContainer.clear(); + containerNode.childNodeContainer.setVisible(true); + while (i.hasNext()) { + final UIDL childUidl = i.next(); + // actions are in bit weird place, don't mix them with children, + // but current node's actions + if ("actions".equals(childUidl.getTag())) { + updateActionMap(childUidl); + continue; + } + final TreeNode childTree = getWidget().new TreeNode(); + getConnection().getVTooltip().connectHandlersToWidget(childTree); + updateNodeFromUIDL(childTree, childUidl, level); + containerNode.childNodeContainer.add(childTree); + if (!i.hasNext()) { + childTree.addStyleDependentName( + childTree.isLeaf() ? "leaf-last" : "last"); + childTree.childNodeContainer.addStyleDependentName("last"); + } + } + containerNode.childrenLoaded = true; + } + + @Override + public boolean isReadOnly() { + return super.isReadOnly() || getState().propertyReadOnly; + } + + @Override + public TreeState getState() { + return (TreeState) super.getState(); + } + + @Override + public TooltipInfo getTooltipInfo(Element element) { + + TooltipInfo info = null; + + // Try to find a tooltip for a node + if (element != getWidget().getElement()) { + Object node = WidgetUtil.findWidget(element, TreeNode.class); + + if (node != null) { + TreeNode tnode = (TreeNode) node; + if (tnode.isCaptionElement(element)) { + info = tooltipMap.get(tnode); + } + } + } + + // If no tooltip found for the node or if the target was not a node, use + // the default tooltip + if (info == null) { + info = super.getTooltipInfo(element); + } + + return info; + } + + @Override + public boolean hasTooltip() { + /* + * Item tooltips are not processed until updateFromUIDL, so we can't be + * sure that there are no tooltips during onStateChange when this method + * is used. + */ + return true; + } + + @Override + protected void sendContextClickEvent(MouseEventDetails details, + EventTarget eventTarget) { + if (!Element.is(eventTarget)) { + return; + } + + Element e = Element.as(eventTarget); + String key = null; + + if (getWidget().body.getElement().isOrHasChild(e)) { + TreeNode t = WidgetUtil.findWidget(e, TreeNode.class); + if (t != null) { + key = t.key; + } + } + + getRpcProxy(TreeServerRpc.class).contextClick(key, details); + + WidgetUtil.clearTextSelection(); + } +} + diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java new file mode 100644 index 0000000000..34b8428582 --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2016 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.client.ui.tree; + +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.UIDL; +import com.vaadin.client.ui.VTree; +import com.vaadin.client.ui.VTree.TreeNode; +import com.vaadin.client.ui.dd.VAcceptCriterion; +import com.vaadin.client.ui.dd.VDragAndDropManager; +import com.vaadin.client.ui.dd.VDragEvent; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.ui.Tree; + +@AcceptCriterion(Tree.TargetInSubtree.class) +final public class VTargetInSubtree extends VAcceptCriterion { + + @Override + protected boolean accept(VDragEvent drag, UIDL configuration) { + + VTree tree = (VTree) VDragAndDropManager.get().getCurrentDropHandler() + .getConnector().getWidget(); + TreeNode treeNode = tree + .getNodeByKey((String) drag.getDropDetails().get("itemIdOver")); + if (treeNode != null) { + Widget parent2 = treeNode; + int depth = configuration.getIntAttribute("depth"); + if (depth < 0) { + depth = Integer.MAX_VALUE; + } + final String searchedKey = configuration.getStringAttribute("key"); + for (int i = 0; i <= depth && parent2 instanceof TreeNode; i++) { + if (searchedKey.equals(((TreeNode) parent2).key)) { + return true; + } + // panel -> next level node + parent2 = parent2.getParent().getParent(); + } + } + + return false; + } +} diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java new file mode 100644 index 0000000000..46089af16e --- /dev/null +++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java @@ -0,0 +1,26 @@ +/* + * Copyright 2000-2016 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.client.ui.tree; + +import com.vaadin.client.ui.dd.VLazyInitItemIdentifiers; +import com.vaadin.shared.ui.dd.AcceptCriterion; +import com.vaadin.ui.Tree; + +@AcceptCriterion(Tree.TreeDropCriterion.class) +public final class VTreeLazyInitItemIdentifiers + extends VLazyInitItemIdentifiers { + // all logic in superclass +} |