diff --git a/README.md b/README.md index a419c2e0..613bdf7f 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is the relevant dependency: org.vaadin.miki superfields - 0.8.0 + 0.9.0 ``` @@ -47,6 +47,20 @@ This repository has a branch `java-8` which contains the most recent release com You are more than welcome to contribute. Feel free to make PRs, submit issues, ideas etc. +### Contributors + +The author of the majority of the code is Miki, but this project would not be possible without these wonderful people - listed in alphabetical order: + +* Wolfgang Fischlein +* Jean-Christophe Gueriaud +* Holger Hähnel +* Gerald Koch +* Sebastian Kühnau +* Jean-François Lamy +* Stuart Robinson +* Kaspar Scherrer +* Tomi Virkki + ## Small print All components are provided "as is", with no warranty or liability. See license for details. diff --git a/demo-v14/pom.xml b/demo-v14/pom.xml index 6ce960e1..726099ba 100644 --- a/demo-v14/pom.xml +++ b/demo-v14/pom.xml @@ -4,11 +4,11 @@ superfields-parent org.vaadin.miki - 0.8.0 + 0.9.0 superfields-demo-v14 - 0.8.0 + 0.9.0 V14 demo app for SuperFields Showcase application for V14 and SuperFields. war @@ -23,7 +23,7 @@ org.vaadin.miki superfields - 0.8.0 + 0.9.0 javax.servlet diff --git a/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java b/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java index dc8ab7ba..3ff9f821 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java +++ b/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java @@ -37,6 +37,7 @@ import org.vaadin.miki.superfields.buttons.SimpleButtonState; import org.vaadin.miki.superfields.dates.SuperDatePicker; import org.vaadin.miki.superfields.dates.SuperDateTimePicker; +import org.vaadin.miki.superfields.gridselect.GridSelect; import org.vaadin.miki.superfields.itemgrid.ItemGrid; import org.vaadin.miki.superfields.lazyload.ComponentObserver; import org.vaadin.miki.superfields.lazyload.LazyLoad; @@ -53,6 +54,7 @@ import org.vaadin.miki.superfields.text.SuperTextField; import org.vaadin.miki.superfields.unload.UnloadObserver; +import java.io.Serializable; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Collection; @@ -63,13 +65,14 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Stores information about components to demo. * @author miki * @since 2020-07-04 */ -public final class DemoComponentFactory { +public final class DemoComponentFactory implements Serializable { private static final int NOTIFICATION_TIME = 1500; @@ -126,6 +129,8 @@ private DemoComponentFactory() { new SimpleButtonState("Are you sure?", VaadinIcon.INFO_CIRCLE.create()), new SimpleButtonState("Really navigate away?", VaadinIcon.INFO.create()).withThemeVariant(ButtonVariant.LUMO_ERROR) ).withId("multi-click-button")); + final GridSelect gridSelect = new GridSelect<>(SuperFieldsGridItem.class, true); + this.components.put(GridSelect.class, gridSelect); // note: extra config below to include all fields this.components.put(ComponentObserver.class, new ComponentObserver()); this.components.put(UnloadObserver.class, UnloadObserver.get().withoutQueryingOnUnload()); this.components.put(ItemGrid.class, new ItemGrid>( @@ -157,6 +162,8 @@ private DemoComponentFactory() { return result; }) ); + gridSelect.getGrid().getColumnByKey("nameLength").setAutoWidth(true); + gridSelect.setItems(this.components.keySet().stream().map(SuperFieldsGridItem::new).collect(Collectors.toList())); this.contentBuilders.put(CanSelectText.class, this::buildCanSelectText); this.contentBuilders.put(HasValue.class, this::buildHasValue); @@ -296,7 +303,7 @@ private void buildItemGrid(Component component, Consumer callback) private void buildHasDatePattern(Component component, Consumer callback) { final ComboBox patterns = new ComboBox<>("Select date display pattern:", DatePatterns.YYYY_MM_DD, DatePatterns.M_D_YYYY_SLASH, - DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED, + DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.DD_MM_YY_OR_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED, DatePatterns.YYYYMMDD, DatePatterns.DDMMYY ); final Button clearPattern = new Button("Clear pattern", event -> ((HasDatePattern)component).setDatePattern(null)); diff --git a/demo-v14/src/main/java/org/vaadin/miki/DemoPage.java b/demo-v14/src/main/java/org/vaadin/miki/DemoPage.java index 9cab4853..25fa686c 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/DemoPage.java +++ b/demo-v14/src/main/java/org/vaadin/miki/DemoPage.java @@ -8,6 +8,9 @@ import com.vaadin.flow.router.HasUrlParameter; import com.vaadin.flow.router.Route; +import java.util.HashMap; +import java.util.Map; + /** * Page that shows a demo of a component. * @author miki @@ -18,24 +21,31 @@ public class DemoPage extends VerticalLayout implements HasUrlParameter, private final DemoComponentFactory demoComponentFactory = DemoComponentFactory.get(); + private final Map pages = new HashMap<>(); + private Class componentType; @Override public void setParameter(BeforeEvent event, String parameter) { this.removeAll(); - this.demoComponentFactory.getDemoableComponentTypes().stream() - .filter(type -> type.getSimpleName().equalsIgnoreCase(parameter)) - .findFirst().ifPresentOrElse(this::buildDemoPageFor, this::buildErrorPage); + + this.add(this.pages.computeIfAbsent(parameter, s -> + this.demoComponentFactory.getDemoableComponentTypes().stream() + .filter(type -> type.getSimpleName().equalsIgnoreCase(s)) + .findFirst() + .map(this::buildDemoPageFor) + .orElseGet(this::buildErrorPage) + )); } - private void buildDemoPageFor(Class type) { + private Component buildDemoPageFor(Class type) { this.componentType = type; - this.add(this.demoComponentFactory.buildDemoPageFor(type)); + return this.demoComponentFactory.buildDemoPageFor(type); } - private void buildErrorPage() { + private Component buildErrorPage() { this.componentType = null; - this.add(new Span("You are seeing this because there was a problem in navigating to the demo page for your selected component.")); + return new Span("You are seeing this because there was a problem in navigating to the demo page for your selected component."); } @Override diff --git a/demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java b/demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java new file mode 100644 index 00000000..1e1753df --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java @@ -0,0 +1,36 @@ +package org.vaadin.miki; + +/** + * Simple data class to showcase in grid select demo. + * @author miki + * @since 2020-08-07 + */ +// needs to be public, otherwise grid cannot access it +public class SuperFieldsGridItem { + + private final int nameLength; + + private String name; + + SuperFieldsGridItem(Class type) { + this.name = type.getSimpleName(); + this.nameLength = this.name.length(); + } + + public int getNameLength() { + return this.nameLength; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/pom.xml b/pom.xml index 3532021d..0f6c00e7 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.vaadin.miki superfields-parent - 0.8.0 + 0.9.0 superfields demo-v14 diff --git a/superfields/pom.xml b/superfields/pom.xml index 855cffcf..d13004e0 100644 --- a/superfields/pom.xml +++ b/superfields/pom.xml @@ -8,7 +8,7 @@ superfields SuperFields Code for various V14+ fields and other components. - 0.8.0 + 0.9.0 10 diff --git a/superfields/release-notes.md b/superfields/release-notes.md index 1546e97b..90eb5a62 100644 --- a/superfields/release-notes.md +++ b/superfields/release-notes.md @@ -1,3 +1,13 @@ +# 0.9.0 - GridSelect +## New features and enhancements +* \#211 - [GridSelect](https://github.com/vaadin-miki/super-fields/issues/211) +* \#212 - [Date pickers should have an option to always accept short year as input](https://github.com/vaadin-miki/super-fields/issues/212) +## Changes to API +(nothing reported) +## Bug fixes +* \#206 - [Client-side listener for UnloadObserver is not cleared when navigating away](https://github.com/vaadin-miki/super-fields/issues/206) +* \#214 - [Demo app events are triggered multiple times](https://github.com/vaadin-miki/super-fields/issues/214) +* \#216 - [Year calculation in date pickers for short year is incorrect](https://github.com/vaadin-miki/super-fields/issues/216) # 0.8.0 - MultiClickButton ## New features and enhancements * \#160 - [Confirm-on-click button](https://github.com/vaadin-miki/super-fields/issues/160) diff --git a/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePattern.java b/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePattern.java index e02dac64..742b70fb 100644 --- a/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePattern.java +++ b/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePattern.java @@ -48,6 +48,8 @@ public enum Order {DAY_MONTH_YEAR, MONTH_DAY_YEAR, YEAR_MONTH_DAY} private boolean previousCenturyBelowBoundary = false; + private boolean shortYearAlwaysAccepted = false; + private Order displayOrder = Order.YEAR_MONTH_DAY; /** @@ -316,6 +318,37 @@ public DatePattern withPreviousCenturyBelowBoundary(boolean belowBoundaryIsPrevi return this; } + /** + * Allows short year to be always accepted as input. + * @param shortYearAlwaysAccepted When {@code true}, short year can always be entered and will be parsed properly and displayed as full year. + * @see #setBaseCentury(int) + * @see #setCenturyBoundaryYear(int) + * @see #setPreviousCenturyBelowBoundary(boolean) + * @see #setShortYear(boolean) + */ + public void setShortYearAlwaysAccepted(boolean shortYearAlwaysAccepted) { + this.shortYearAlwaysAccepted = shortYearAlwaysAccepted; + } + + /** + * Whether or not short year is accepted as user input even if {@link #isShortYear()} returns {@code false}. + * @return When {@code true}, user can input last two digits of the year and it will be properly parsed. + */ + public boolean isShortYearAlwaysAccepted() { + return shortYearAlwaysAccepted; + } + + /** + * Chains {@link #setShortYearAlwaysAccepted(boolean)} and returns itself. + * @param shortYearAlwaysAccepted Whether or not to always accept short year as user input. + * @return This. + * @see #setShortYearAlwaysAccepted(boolean) + */ + public DatePattern withShortYearAlwaysAccepted(boolean shortYearAlwaysAccepted) { + this.setShortYearAlwaysAccepted(shortYearAlwaysAccepted); + return this; + } + /** * Returns display name defined in the constructor. The display name is irrelevant in {@link #equals(Object)} and {@link #hashCode()}. * @return Display name. May be {@code null} when no-arg constructor has been used. @@ -336,12 +369,13 @@ public boolean equals(Object o) { getBaseCentury() == pattern.getBaseCentury() && getCenturyBoundaryYear() == pattern.getCenturyBoundaryYear() && isPreviousCenturyBelowBoundary() == pattern.isPreviousCenturyBelowBoundary() && + isShortYearAlwaysAccepted() == pattern.isShortYearAlwaysAccepted() && getDisplayOrder() == pattern.getDisplayOrder(); } @Override public int hashCode() { - return Objects.hash(getSeparator(), isZeroPrefixedDay(), isZeroPrefixedMonth(), isShortYear(), getBaseCentury(), getCenturyBoundaryYear(), isPreviousCenturyBelowBoundary(), getDisplayOrder()); + return Objects.hash(getSeparator(), isZeroPrefixedDay(), isZeroPrefixedMonth(), isShortYear(), getBaseCentury(), getCenturyBoundaryYear(), isPreviousCenturyBelowBoundary(), isShortYearAlwaysAccepted(), getDisplayOrder()); } @Override @@ -355,6 +389,7 @@ public String toString() { ", baseCentury=" + baseCentury + ", centuryBoundaryYear=" + centuryBoundaryYear + ", previousCenturyBelowBoundary=" + previousCenturyBelowBoundary + + ", shortYearAlwaysAccepted=" + shortYearAlwaysAccepted + ", displayOrder=" + displayOrder + '}' : getDisplayName(); diff --git a/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePatterns.java b/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePatterns.java index 7d5a81ba..b76da894 100644 --- a/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePatterns.java +++ b/superfields/src/main/java/org/vaadin/miki/shared/dates/DatePatterns.java @@ -19,6 +19,16 @@ public final class DatePatterns { .withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR) .withSeparator('.'); + /** + * Uses zero-prefixed day and month, optionally full year, separated by {@code .}. + * For short years the century boundary year is 40 (years less than 40 are from 21st century). + */ + public static final DatePattern DD_MM_YY_OR_YYYY_DOTTED = new DatePattern("dd.MM.(yy)yy") + .withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR) + .withShortYearAlwaysAccepted(true) + .withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false) + .withSeparator('.'); + /** * Uses day, month and short year with century boundary year 40 (years less than 40 are from 21st century), separated by {@code .}. */ diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/dates/DatePatternDelegate.java b/superfields/src/main/java/org/vaadin/miki/superfields/dates/DatePatternDelegate.java index 231483bb..53e9bef8 100644 --- a/superfields/src/main/java/org/vaadin/miki/superfields/dates/DatePatternDelegate.java +++ b/superfields/src/main/java/org/vaadin/miki/superfields/dates/DatePatternDelegate.java @@ -51,7 +51,7 @@ private static String convertDatePatternToClientPattern(DatePattern pattern) { builder.append(yearPart).append(monthPart).append(dayPart); break; } - if (pattern.isShortYear()) { + if (pattern.isShortYear() || pattern.isShortYearAlwaysAccepted()) { builder.append(pattern.isPreviousCenturyBelowBoundary() ? '+' : '-'); builder.append(String.format("%02d", pattern.getBaseCentury() % 100)); builder.append(String.format("%02d", pattern.getCenturyBoundaryYear() % 100)); diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/gridselect/GridSelect.java b/superfields/src/main/java/org/vaadin/miki/superfields/gridselect/GridSelect.java new file mode 100644 index 00000000..97a9e7ce --- /dev/null +++ b/superfields/src/main/java/org/vaadin/miki/superfields/gridselect/GridSelect.java @@ -0,0 +1,124 @@ +package org.vaadin.miki.superfields.gridselect; + +import com.vaadin.flow.component.AbstractField; +import com.vaadin.flow.component.customfield.CustomField; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.data.selection.SelectionEvent; +import org.vaadin.miki.markers.WithIdMixin; +import org.vaadin.miki.markers.WithItemsMixin; +import org.vaadin.miki.markers.WithValueMixin; + +import java.util.Collection; +import java.util.Set; + +/** + * A single-selection {@link Grid} that also is a value component that broadcasts value change events. + * @param Type of value to include in the grid. + * @author miki + * @since 2020-08-07 + */ +public class GridSelect extends CustomField + implements WithIdMixin>, WithItemsMixin>, + WithValueMixin, V>, V, GridSelect> { + + private final Grid grid; + + /** + * Creates the component. + * This requires a subsequent configuration of grid's columns. + * @see #getGrid() + */ + public GridSelect() { + this(new Grid<>()); + } + + /** + * Constructs the component. This is the recommended constructor. + * @param type Type of items displayed in the grid. + * @param createColumns Whether or not to create default columns. + * @param items Items to add to the grid. + */ + @SafeVarargs + public GridSelect(Class type, boolean createColumns, V... items) { + this(new Grid<>(type, createColumns)); + this.setItems(items); + } + + /** + * More advanced constructor that allows using predefined grid. + * It is not public, as usage of this constructor implies you know what you are doing. + * @param underlyingGrid A grid to use. + */ + protected GridSelect(Grid underlyingGrid) { + this.grid = underlyingGrid; + this.configureGrid(this.grid); + this.setSizeFull(); + } + + /** + * Configures the grid. + * @param grid Grid to configure. + */ + protected void configureGrid(Grid grid) { + this.add(grid); + + this.grid.addSelectionListener(this::onGridSelected); + this.grid.addFocusListener(gridFocusEvent -> this.fireEvent(new FocusEvent<>(this, gridFocusEvent.isFromClient()))); + this.grid.addBlurListener(gridBlurEvent -> this.fireEvent(new BlurEvent<>(this, gridBlurEvent.isFromClient()))); + + // this js snippet courtesy of Tomi Virkki + // prevents selection on-click and on-space-pressed, basically turning the grid into read-only when needed + this.grid.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.grid, executionContext -> + this.grid.getElement().executeJs("this.$.table.addEventListener('click', function(e) {this.preventSelection && e.stopPropagation()}.bind(this)); " + + "this.$.table.addEventListener('keydown', function(e){this.preventSelection && e.keyCode === 32 && e.stopPropagation()}.bind(this));"))); + + grid.addClassName("grid-select-inner-grid"); + grid.setSelectionMode(Grid.SelectionMode.SINGLE); + } + + private void onGridSelected(SelectionEvent, V> event) { + this.updateValue(); + } + + /** + * Returns the underlying {@link Grid}. Use with caution. Please do not mess with grid's selection. + * @return The {@link Grid}. Any changes to the grid will affect this component. + */ + public Grid getGrid() { + return this.grid; + } + + @Override + protected V generateModelValue() { + final Set items = this.grid.getSelectedItems(); + return items.isEmpty() ? null : items.iterator().next(); + } + + @Override + protected void setPresentationValue(V v) { + this.grid.select(v); + } + + @Override + public void setItems(Collection collection) { + this.grid.setItems(collection); + this.grid.recalculateColumnWidths(); + } + + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + this.grid.getElement().setProperty("preventSelection", readOnly); + } + + @Override + public void setEnabled(boolean enabled) { + this.grid.setEnabled(enabled); + } + + @Override + public boolean isEnabled() { + return this.grid.isEnabled(); + } + +} diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/unload/UnloadObserver.java b/superfields/src/main/java/org/vaadin/miki/superfields/unload/UnloadObserver.java index 2bb40884..efb9d93d 100644 --- a/superfields/src/main/java/org/vaadin/miki/superfields/unload/UnloadObserver.java +++ b/superfields/src/main/java/org/vaadin/miki/superfields/unload/UnloadObserver.java @@ -186,6 +186,12 @@ protected void onAttach(AttachEvent attachEvent) { @Override protected void onDetach(DetachEvent detachEvent) { this.clientInitialised = false; + + // getElement().callJsFunction(..) doesn't work in this phase, so let's execute the necessary js directly without the element involved. + detachEvent.getUI().getPage().executeJs( + "if (window.Vaadin.unloadObserver.attemptHandler !== undefined) {" + + " window.removeEventListener('beforeunload', window.Vaadin.unloadObserver.attemptHandler);" + + "}"); super.onDetach(detachEvent); } diff --git a/superfields/src/main/resources/META-INF/resources/frontend/date-pattern-mixin.js b/superfields/src/main/resources/META-INF/resources/frontend/date-pattern-mixin.js index fbe65532..2d640c28 100644 --- a/superfields/src/main/resources/META-INF/resources/frontend/date-pattern-mixin.js +++ b/superfields/src/main/resources/META-INF/resources/frontend/date-pattern-mixin.js @@ -149,16 +149,16 @@ export class DatePatternMixin { } } // end of parsing stuff - - // now, if short year is used - if (shortYear) { + // now, if short year is allowed (i.e. there is a description of default century and boundary year) + if (ddp.length === 12 || ddp.length === 11) { const boundaryYear = parseInt(ddp.substr(-2)); const defaultCentury = parseInt(ddp.substr(-4, 2)); if (year < boundaryYear) { year += (ddp[ddp.length-5] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100; } else if (year < 100) { - year += (ddp[ddp.length-5] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100; + year += (ddp[ddp.length-5] === '+' ? defaultCentury - 1 : defaultCentury - 2) * 100; } + console.log("SDP: after fixing the year is "+year); } // return result if (date !== undefined) {