Skip to content

Commit

Permalink
#141 done, #123 done for SuperDatePicker
Browse files Browse the repository at this point in the history
slightly updated demo app
  • Loading branch information
vaadin-miki committed Jun 4, 2020
1 parent 5824a39 commit 5173e80
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 21 deletions.
2 changes: 1 addition & 1 deletion demo-v14/src/main/java/org/vaadin/miki/MainView.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private void buildCanSelectText(Component component, Consumer<Component[]> callb
icon.setColor("green");
icon.getElement().setAttribute("title", "When the component does not receive events from the browser, selection events will only be called for server-side initiated actions.");
callback.accept(new Component[]{
new HorizontalLayout(new Span("Current selection: <"), selection, new Span(">"), icon)
new HorizontalLayout(new Span("Most recently selected text: <"), selection, new Span(">"), icon)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;

import java.io.Serializable;
import java.time.LocalDate;

/**
* Helper class that calls a method on a client side.
* Helper class that contains code related handling date patterns.
* Internal use only.
*
* @author miki
* @since 2020-05-02
*/
final class DatePatternHelper<C extends Component & HasDatePattern> {
final class DatePatternDelegate<C extends Component & HasDatePattern> implements Serializable {

private static final long serialVersionUID = 20200506L;

/**
* Returns the pattern descriptor string expected by the client-side code.
Expand Down Expand Up @@ -60,7 +65,7 @@ private static String convertDatePatternToClientPattern(DatePattern pattern) {
* The client-side representation should have mixed in methods from {@code date-pattern-mixin.js}.
* @param source Source to use.
*/
DatePatternHelper(C source) {
DatePatternDelegate(C source) {
this.source = source;
this.source.addAttachListener(this::onAttached);
}
Expand Down Expand Up @@ -97,4 +102,27 @@ protected void updateClientSidePattern() {
));
}

private int[] getDayMonthYearPositions(DatePattern.Order order) {
if(order == DatePattern.Order.DAY_MONTH_YEAR)
return new int[]{0, 1, 2};
else if(order == DatePattern.Order.MONTH_DAY_YEAR)
return new int[]{1, 0, 2};
else return new int[]{2, 1, 0};
}

/**
* Formats the date according to the pattern present in the source.
* @param date Date to format. Must not be {@code null}.
* @return Formatted date.
*/
String formatDate(LocalDate date) {
final DatePattern pattern = this.source.getDatePattern();
final String[] parts = new String[3];
final int[] indices = this.getDayMonthYearPositions(pattern.getDisplayOrder());
parts[indices[0]] = pattern.isZeroPrefixedDay() ? String.format("%02d", date.getDayOfMonth()) : String.valueOf(date.getDayOfMonth());
parts[indices[1]] = pattern.isZeroPrefixedMonth() ? String.format("%02d", date.getMonthValue()) : String.valueOf(date.getMonthValue());
parts[indices[2]] = pattern.isShortYear() ? String.format("%02d", date.getYear() % 100) : String.valueOf(date.getYear());
return String.join(String.valueOf(pattern.getSeparator()), parts);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.vaadin.miki.superfields.dates;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.DetachEvent;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.shared.Registration;
import org.vaadin.miki.markers.HasLabel;
import org.vaadin.miki.markers.HasLocale;
import org.vaadin.miki.markers.HasPlaceholder;
Expand All @@ -12,8 +16,17 @@
import org.vaadin.miki.markers.WithLocaleMixin;
import org.vaadin.miki.markers.WithPlaceholderMixin;
import org.vaadin.miki.markers.WithValueMixin;
import org.vaadin.miki.superfields.text.CanReceiveSelectionEventsFromClient;
import org.vaadin.miki.superfields.text.CanSelectText;
import org.vaadin.miki.superfields.text.TextSelectionDelegate;
import org.vaadin.miki.superfields.text.TextSelectionEvent;
import org.vaadin.miki.superfields.text.TextSelectionListener;
import org.vaadin.miki.superfields.text.TextSelectionNotifier;
import org.vaadin.miki.superfields.text.WithReceivingSelectionEventsFromClientMixin;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Locale;

/**
Expand All @@ -26,12 +39,18 @@
@SuppressWarnings("squid:S110") // there is no way to reduce the number of parent classes
public class SuperDatePicker extends DatePicker
implements HasLocale, HasLabel, HasPlaceholder, HasDatePattern,
CanSelectText, CanReceiveSelectionEventsFromClient, WithReceivingSelectionEventsFromClientMixin<SuperDatePicker>,
TextSelectionNotifier<SuperDatePicker>,
WithLocaleMixin<SuperDatePicker>, WithLabelMixin<SuperDatePicker>,
WithPlaceholderMixin<SuperDatePicker>, WithDatePatternMixin<SuperDatePicker>,
WithValueMixin<AbstractField.ComponentValueChangeEvent<DatePicker, LocalDate>, LocalDate, SuperDatePicker>,
WithIdMixin<SuperDatePicker> {

private final DatePatternHelper<SuperDatePicker> delegate = new DatePatternHelper<>(this);
private final DatePatternDelegate<SuperDatePicker> datePatternDelegate = new DatePatternDelegate<>(this);

private final TextSelectionDelegate<SuperDatePicker> textSelectionDelegate = new TextSelectionDelegate<>(this);

private boolean receivingClientSideSelectionEvent = false;

private DatePattern datePattern;

Expand Down Expand Up @@ -88,8 +107,8 @@ public SuperDatePicker(LocalDate initialDate, Locale locale) {
public final void setLocale(Locale locale) {
// there is a call for setting locale from the superclass' constructor
// and when that happens, the field is not yet initialised
if(this.delegate != null) {
this.delegate.initPatternSetting();
if(this.datePatternDelegate != null) {
this.datePatternDelegate.initPatternSetting();
SuperDatePickerI18nHelper.updateI18N(locale, this::getI18n, this::setI18n);
}
super.setLocale(locale);
Expand All @@ -98,11 +117,83 @@ public final void setLocale(Locale locale) {
@Override
public void setDatePattern(DatePattern datePattern) {
this.datePattern = datePattern;
this.delegate.updateClientSidePattern();
this.datePatternDelegate.updateClientSidePattern();
}

@Override
public DatePattern getDatePattern() {
return this.datePattern;
}

@Override
public void setValue(LocalDate value) {
this.textSelectionDelegate.updateAttributeOnValueChange(this::getEventBus);
super.setValue(value);
}

@Override
protected void onAttach(AttachEvent attachEvent) {
this.textSelectionDelegate.informClientAboutSendingEvents(this.isReceivingSelectionEventsFromClient());
super.onAttach(attachEvent);
}

@Override
protected void onDetach(DetachEvent detachEvent) {
// detaching means server should not be informed
if(this.isReceivingSelectionEventsFromClient())
this.textSelectionDelegate.informClientAboutSendingEvents(false);
super.onDetach(detachEvent);
}

@ClientCallable
private void selectionChanged(int start, int end, String selection) {
TextSelectionEvent<SuperDatePicker> event = new TextSelectionEvent<>(this, true, start, end, selection);
this.textSelectionDelegate.fireTextSelectionEvent(this.getEventBus(), event);
}

@Override
public boolean isReceivingSelectionEventsFromClient() {
return this.receivingClientSideSelectionEvent;
}

@Override
public void setReceivingSelectionEventsFromClient(boolean receivingSelectionEventsFromClient) {
this.receivingClientSideSelectionEvent = receivingSelectionEventsFromClient;
this.textSelectionDelegate.informClientAboutSendingEvents(receivingSelectionEventsFromClient);
}

/**
* Returns the current value formatted with current locale or pattern.
* @return Current date, formatted. Will return {@code null} if the current date is {@code null}.
*/
public String getFormattedValue() {
final LocalDate value = this.getValue();
if(value == null)
return null;
else if(this.getDatePattern() == null)
return value.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(this.getLocale()));
else
return this.datePatternDelegate.formatDate(value);
}

@Override
public void selectAll() {
this.textSelectionDelegate.selectAll(this::getFormattedValue, this::getEventBus);
}

@Override
public void selectNone() {
this.textSelectionDelegate.selectNone(this::getEventBus);
}

@Override
public void select(int from, int to) {
this.textSelectionDelegate.select(this::getFormattedValue, this::getEventBus, from, to);
}

@Override
@SuppressWarnings("unchecked")
public Registration addTextSelectionListener(TextSelectionListener<SuperDatePicker> listener) {
return this.getEventBus().addListener((Class<TextSelectionEvent<SuperDatePicker>>)(Class<?>)TextSelectionEvent.class, listener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class SuperDateTimePicker extends DateTimePicker
private static final String INTERNAL_DATE_PICKER_FIELD_NAME = "datePicker";
private static final String INTERNAL_TIME_PICKER_FIELD_NAME = "timePicker";

private final DatePatternHelper<SuperDateTimePicker> delegate = new DatePatternHelper<>(this);
private final DatePatternDelegate<SuperDateTimePicker> delegate = new DatePatternDelegate<>(this);

private DatePattern datePattern;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.vaadin.miki.superfields.text;

/**
* Marker interface for objects capable of receiving text selection events from client-side code.
* @author miki
* @since 2020-06-04
*/
public interface CanReceiveSelectionEventsFromClient {
/**
* Check if client will inform server on selection change.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@

/**
* Marker interface for components that can select text.
* Handles selection using client-side JavaScript.
* What selecting text means and how it is done is left to the implementations.
* @author miki
* @since 2020-05-29
*/
public interface CanSelectText extends HasElement {

/**
* Selects entire text in the component.
*/
void selectAll();

/**
* Removes the current selection and selects no text.
*/
void selectNone();

/**
* Selects text starting from index {@code from} (inclusive) and ending at index {@code to} (exclusive).
* @param from Starting index (inclusive).
* @param to Ending index (exclusive).
*/
void select(int from, int to);

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
import java.util.function.Supplier;

/**
* Internal class that handles common behaviour related to text selection.
* Note: this is for internal use only.
* A class that handles common behaviour related to text selection.
* This assumes that the client-side component mixes in {@code text-selection-mikin.js}.
* @author miki
* @since 2020-06-01
*/
class TextSelectionDelegate<C extends Component & CanSelectText & CanReceiveSelectionEventsFromClient> implements Serializable {
public class TextSelectionDelegate<C extends Component & CanSelectText & CanReceiveSelectionEventsFromClient> implements Serializable {

/**
* Defines the name of the HTML attribute that contains the selected text.
Expand All @@ -27,15 +27,15 @@ class TextSelectionDelegate<C extends Component & CanSelectText & CanReceiveSele
* Creates the delegate for a given component.
* @param source Source of all events, data, etc.
*/
TextSelectionDelegate(C source) {
public TextSelectionDelegate(C source) {
this.source = source;
}

/**
* Sends information to the client side about whether or not it should forward text selection change events.
* @param value When {@code true}, client-side will notify server about changes in text selection.
*/
protected void informClientAboutSendingEvents(boolean value) {
public void informClientAboutSendingEvents(boolean value) {
this.source.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.source, context ->
this.source.getElement().callJsFunction(
"setCallingServer",
Expand All @@ -49,7 +49,7 @@ protected void informClientAboutSendingEvents(boolean value) {
* @param eventBus Event bus.
* @param event Event with information about text selection.
*/
protected void fireTextSelectionEvent(ComponentEventBus eventBus, TextSelectionEvent<C> event) {
public void fireTextSelectionEvent(ComponentEventBus eventBus, TextSelectionEvent<C> event) {
eventBus.fireEvent(event);
}

Expand All @@ -63,7 +63,7 @@ private void selectionChanged(ComponentEventBus eventBus, int start, int end, St
* @param valueSupplier Way of getting current value. Needed if no client notifications.
* @param eventBusSupplier Way of getting event bus. Needed if no client notifications.
*/
void selectAll(Supplier<String> valueSupplier, Supplier<ComponentEventBus> eventBusSupplier) {
public void selectAll(Supplier<String> valueSupplier, Supplier<ComponentEventBus> eventBusSupplier) {
this.source.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.source, context ->
this.source.getElement().callJsFunction("selectAll", this.source.getElement())
));
Expand All @@ -79,7 +79,7 @@ void selectAll(Supplier<String> valueSupplier, Supplier<ComponentEventBus> event
* Selects no text.
* @param eventBusSupplier Way of getting event bus. Needed if no client notifications.
*/
void selectNone(Supplier<ComponentEventBus> eventBusSupplier) {
public void selectNone(Supplier<ComponentEventBus> eventBusSupplier) {
this.source.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.source, context ->
this.source.getElement().callJsFunction("selectNone", this.source.getElement())
));
Expand All @@ -97,7 +97,7 @@ void selectNone(Supplier<ComponentEventBus> eventBusSupplier) {
* @param from Selection starting index, inclusive.
* @param to Selection end index, exclusive.
*/
void select(Supplier<String> valueSupplier, Supplier<ComponentEventBus> eventBusSupplier, int from, int to) {
public void select(Supplier<String> valueSupplier, Supplier<ComponentEventBus> eventBusSupplier, int from, int to) {
if(from <= to)
this.source.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.source, context ->
this.source.getElement().callJsFunction("select", this.source.getElement(), from, to)
Expand All @@ -115,7 +115,7 @@ void select(Supplier<String> valueSupplier, Supplier<ComponentEventBus> eventBus
* Does nothing if the component is receiving client-side notifications.
* @param eventBusSupplier Way of getting event bus.
*/
void updateAttributeOnValueChange(Supplier<ComponentEventBus> eventBusSupplier) {
public void updateAttributeOnValueChange(Supplier<ComponentEventBus> eventBusSupplier) {
// special case here: if there was selection, no client-side events are caught and value is set, event must be fired
if(!this.source.isReceivingSelectionEventsFromClient()) {
final String lastSelected = Optional.ofNullable(this.source.getElement().getAttribute(SELECTED_TEXT_ATTRIBUTE_NAME)).orElse("");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import {DatePickerElement} from '@vaadin/vaadin-date-picker/src/vaadin-date-picker.js';
import {DatePatternMixin} from "./date-pattern-mixin";
import {TextSelectionMixin} from "./text-selection-mixin";

class SuperDatePicker extends DatePatternMixin.to(DatePickerElement) {
class SuperDatePicker extends TextSelectionMixin.to(DatePatternMixin.to(DatePickerElement)) {

static get is() {return 'super-date-picker'}

setCallingServer(callingServer) {
console.log('SDP: configuring text selection listeners; callingServer flag is '+callingServer);
this.listenToEvents(this.shadowRoot.querySelector('vaadin-text-field').inputElement, this, callingServer);
}

}

customElements.define(SuperDatePicker.is, SuperDatePicker);
Loading

0 comments on commit 5173e80

Please sign in to comment.