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 417717fb..dc8ab7ba 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java +++ b/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java @@ -294,7 +294,11 @@ 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); + 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.YYYYMMDD, DatePatterns.DDMMYY + ); final Button clearPattern = new Button("Clear pattern", event -> ((HasDatePattern)component).setDatePattern(null)); clearPattern.setDisableOnClick(true); final Component clearPatternOrContainer; 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 6589425b..e02dac64 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 @@ -1,5 +1,8 @@ package org.vaadin.miki.shared.dates; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.Serializable; import java.util.Objects; @@ -17,9 +20,21 @@ public class DatePattern implements Serializable { */ public enum Order {DAY_MONTH_YEAR, MONTH_DAY_YEAR, YEAR_MONTH_DAY} + /** + * Shorthand for no separator (character 0). + */ + public static final char NO_SEPARATOR = 0; + + /** + * Default separator, {@code -}. + */ + public static final char DEFAULT_SEPARATOR = '-'; + + private static final Logger LOGGER = LoggerFactory.getLogger(DatePattern.class); + private final String displayName; - private char separator = '-'; + private char separator = DEFAULT_SEPARATOR; private boolean zeroPrefixedDay = true; @@ -58,12 +73,25 @@ public char getSeparator() { return separator; } + /** + * Checks whether or not there is a separator present. + * @return Whether or not {@link #getSeparator()} returns something else than {@link #NO_SEPARATOR}. + */ + public boolean hasSeparator() { + return this.getSeparator() != NO_SEPARATOR; + } + /** * Sets new separator. + * If the separator is {@link #NO_SEPARATOR} (zero), zero-prefixed month and zero-prefixed day will be automatically enabled. * @param separator Separator between parts. */ public void setSeparator(char separator) { this.separator = separator; + if(separator == NO_SEPARATOR) { + this.withZeroPrefixedDay(true).setZeroPrefixedMonth(true); + LOGGER.warn("disabling date pattern separator, turning on zero-prefixed day and zero-prefixed month"); + } } /** @@ -71,12 +99,23 @@ public void setSeparator(char separator) { * @param separator Separator. * @return This. * @see #setSeparator(char) + * @see #withoutSeparator() */ public DatePattern withSeparator(char separator) { this.setSeparator(separator); return this; } + /** + * Identical to {@code withSeparator(DatePattern.NO_SEPARATOR}. + * @return This. + * @see #withSeparator(char) + * @see #setSeparator(char) + */ + public DatePattern withoutSeparator() { + return this.withSeparator(NO_SEPARATOR); + } + /** * Checks whether days should be prefixed with {@code 0}. * @return Whether or not days will be zero-prefixed ({@code 09} instead of {@code 9}); {@code true} by default. @@ -87,10 +126,15 @@ public boolean isZeroPrefixedDay() { /** * Sets whether or not days should be prefixed with {@code 0}. + * When there is no separator and this flag is turned off, the separator will be set to {@link #DEFAULT_SEPARATOR}. * @param zeroPrefixedDay When {@code true} and day is one digit, zero will be added in front of that number. */ public void setZeroPrefixedDay(boolean zeroPrefixedDay) { this.zeroPrefixedDay = zeroPrefixedDay; + if(!zeroPrefixedDay && !this.hasSeparator()) { + this.setSeparator(DEFAULT_SEPARATOR); + LOGGER.warn("turning off zero-prefixed day requires a separator, setting it to be the default one ({})", DEFAULT_SEPARATOR); + } } /** @@ -118,6 +162,10 @@ public boolean isZeroPrefixedMonth() { */ public void setZeroPrefixedMonth(boolean zeroPrefixedMonth) { this.zeroPrefixedMonth = zeroPrefixedMonth; + if(!zeroPrefixedMonth && !this.hasSeparator()) { + this.setSeparator(DEFAULT_SEPARATOR); + LOGGER.warn("turning off zero-prefixed month requires a separator, setting it to be the default one ({})", DEFAULT_SEPARATOR); + } } /** 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 7763af49..7d5a81ba 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 @@ -15,25 +15,41 @@ public final class DatePatterns { /** * Uses zero-prefixed day and month, full year, separated by {@code .}. */ - public static final DatePattern DD_MM_YYYY_DOTTED = new DatePattern("dd.MM.yyyy"). - withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR). - withSeparator('.'); + public static final DatePattern DD_MM_YYYY_DOTTED = new DatePattern("dd.MM.yyyy") + .withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR) + .withSeparator('.'); /** * Uses day, month and short year with century boundary year 40 (years less than 40 are from 21st century), separated by {@code .}. */ - public static final DatePattern D_M_YY_DOTTED = new DatePattern("d.M.yy"). - withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR). - withShortYear(true). - withSeparator('.').withZeroPrefixedDay(false).withZeroPrefixedMonth(false). - withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false); + public static final DatePattern D_M_YY_DOTTED = new DatePattern("d.M.yy") + .withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR) + .withShortYear(true) + .withSeparator('.').withZeroPrefixedDay(false).withZeroPrefixedMonth(false) + .withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false); /** * Uses month, day and full year, separated by {@code /}. */ - public static final DatePattern M_D_YYYY_SLASH = new DatePattern("M/d/yyyy"). - withDisplayOrder(DatePattern.Order.MONTH_DAY_YEAR). - withSeparator('/').withZeroPrefixedDay(false).withZeroPrefixedMonth(false); + public static final DatePattern M_D_YYYY_SLASH = new DatePattern("M/d/yyyy") + .withDisplayOrder(DatePattern.Order.MONTH_DAY_YEAR) + .withSeparator('/').withZeroPrefixedDay(false).withZeroPrefixedMonth(false); + + /** + * Uses full year, zero-prefixed month and day, and no separator. + */ + public static final DatePattern YYYYMMDD = new DatePattern("yyyyMMdd") + .withDisplayOrder(DatePattern.Order.YEAR_MONTH_DAY) + .withSeparator(DatePattern.NO_SEPARATOR); + + /** + * Uses zero-prefixed day and month with short year (century boundary year 40, years less than 40 in 21st century), and no separator. + */ + public static final DatePattern DDMMYY = new DatePattern("ddMMyy") + .withDisplayOrder(DatePattern.Order.DAY_MONTH_YEAR) + .withSeparator(DatePattern.NO_SEPARATOR) + .withShortYear(true) + .withBaseCentury(21).withCenturyBoundaryYear(40).withPreviousCenturyBelowBoundary(false); private DatePatterns() {} // instances not needed } 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 6fd4e2b4..231483bb 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 @@ -38,7 +38,8 @@ private static String convertDatePatternToClientPattern(DatePattern pattern) { final String yearPart = pattern.isShortYear() ? "0y" : "_y"; StringBuilder builder = new StringBuilder(); - builder.append(pattern.getSeparator()); + if(pattern.hasSeparator()) + builder.append(pattern.getSeparator()); switch (pattern.getDisplayOrder()) { case DAY_MONTH_YEAR: builder.append(dayPart).append(monthPart).append(yearPart); 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 7bd8ca5c..fbe65532 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 @@ -32,12 +32,18 @@ export class DatePatternMixin { return } console.log('SDP: MAIN - set display pattern'); - // pattern is a string, first character is a separator + // the pattern parts: + // - separator (1) - optional + // - day, month, year (3 x 2 = 6) - required + // - century indicator, default century and boundary year (1 + 2 + 2 = 5) - optional + // this gives: 7 or 12 characters when the separator is present, 6 or 11 when it is not, and 0/null for none + + // pattern is a string, first optional character is a separator // next six characters define, in groups of 2, the order and what should be displayed // 0d or _d - (zero prefixed) day // 0M or _M - (zero prefixed) month // 0y or _y - short year or full year - // the remaining of the pattern is optional, only when yy is used + // the remaining of the pattern is optional, only when 0y is used // + or - - corresponds to previous century in short year (+ when true, - when false) // XX - number corresponding to the default century in short years: MUST BE TWO DIGITS // YY - number corresponding to the boundary year: MUST BE TWO DIGITS @@ -57,8 +63,9 @@ export class DatePatternMixin { } datepicker.set('i18n.formatDate', date => { const ddp = datepicker.i18n.dateDisplayPattern; + const startIndex = (ddp.length === 7 || ddp.length === 12) ? 1 : 0; console.log('SDP: custom formatting for ' + ddp); - return [ddp.substr(1, 2), ddp.substr(3, 2), ddp.substr(5, 2)].map(part => { + return [ddp.substr(startIndex, 2), ddp.substr(startIndex+2, 2), ddp.substr(startIndex+4, 2)].map(part => { if (part === '0d') { return String(date.day).padStart(2, '0') } else if (part === '_d') { @@ -72,40 +79,88 @@ export class DatePatternMixin { } else if (part === '_y') { return String(date.year) } - }).join(ddp[0]); + }).join(startIndex === 1 ? ddp[0] : ''); }); datepicker.set('i18n.parseDate', text => { const ddp = datepicker.i18n.dateDisplayPattern; + const shortYear = ddp.indexOf('0y') !== -1; console.log('SDP: custom parsing for ' + ddp); const today = new Date(); let date, month = today.getMonth(), year = today.getFullYear(); - const parts = text.split(ddp[0]); - if (parts.length === 3) { - // d, M, y can be at index 2, 4 or 6 in the pattern - year = parseInt(parts[(ddp.indexOf('y') / 2) - 1]); - month = parseInt(parts[(ddp.indexOf('M') / 2) - 1]) - 1; - date = parseInt(parts[(ddp.indexOf('d') / 2) - 1]); - // now, if short year is used - if (ddp.indexOf('0y') !== -1) { - const boundaryYear = parseInt(ddp.substr(-2)); - const defaultCentury = parseInt(ddp.substr(-4, 2)); - if (year < boundaryYear) { - year += (ddp[7] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100; - } else if (year < 100) { - year += (ddp[7] === '+' ? defaultCentury - 2 : defaultCentury - 1) * 100; + // this part is triggered when there is a separator + if (ddp.length === 7 || ddp.length === 12) { + const parts = text.split(ddp[0]); + if (parts.length === 3) { + // d, M, y can be at index 2, 4 or 6 in the pattern + year = parseInt(parts[(ddp.indexOf('y') / 2) - 1]); + month = parseInt(parts[(ddp.indexOf('M') / 2) - 1]) - 1; + date = parseInt(parts[(ddp.indexOf('d') / 2) - 1]); + } else if (parts.length === 2) { + if (ddp.indexOf('d') < ddp.indexOf('M')) { + date = parseInt(parts[0]); + month = parseInt(parts[1]) - 1; + } else { + date = parseInt(parts[1]); + month = parseInt(parts[0]) - 1; } - } - } else if (parts.length === 2) { - if (ddp.indexOf('d') < ddp.indexOf('M')) { + } else if (parts.length === 1) { date = parseInt(parts[0]); - month = parseInt(parts[1]) - 1; - } else { - date = parseInt(parts[1]); - month = parseInt(parts[0]) - 1; } - } else if (parts.length === 1) { - date = parseInt(parts[0]); } + // there is no separator and thus by definition month and day are zero-based + // this means the pattern starts directly with day/month/year parts, each taking two characters + // it also means that the input is composed of parts with two eventually up to four characters + else { + const dayOrder = Math.floor(ddp.indexOf('d') / 2); + const monthOrder = Math.floor(ddp.indexOf('M') / 2); + const yearOrder = Math.floor(ddp.indexOf('y') / 2); + // if the length of the input is 1 or 2, it is a day + if (text.length <= 2) { + date = parseInt(text); + } + // length being 3 or 4 means there is a day and a month, in order defined by the pattern + else if (text.length <= 4) { + // day first + if (dayOrder < monthOrder) { + date = parseInt(text.substr(0, 2)); + month = parseInt(text.substr(2)) - 1; + } + // month first + else { + month = parseInt(text.substr(0, 2)); + date = parseInt(text.substr(2)) - 1; + } + } + // length is more than 4 characters, which means there is also year involved + else { + let yearPosition = yearOrder * 2; + let dayPosition = dayOrder * 2; + let monthPosition = monthOrder * 2; + // if year is full, month and day can potentially be offset by 2 extra characters + if (!shortYear) { + if (dayOrder > yearOrder) + dayPosition += 2; + if (monthOrder > yearOrder) + monthPosition += 2; + } + date = parseInt(text.substr(dayPosition, 2)); + month = parseInt(text.substr(monthPosition, 2)) - 1; + year = parseInt(text.substr(yearPosition, shortYear ? 2 : 4)); + } + } + // end of parsing stuff + + // now, if short year is used + if (shortYear) { + 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; + } + } + // return result if (date !== undefined) { return {day: date, month, year}; } diff --git a/superfields/src/main/resources/META-INF/resources/frontend/super-date-picker.js b/superfields/src/main/resources/META-INF/resources/frontend/super-date-picker.js index e180743e..0eb44c76 100644 --- a/superfields/src/main/resources/META-INF/resources/frontend/super-date-picker.js +++ b/superfields/src/main/resources/META-INF/resources/frontend/super-date-picker.js @@ -8,7 +8,7 @@ class SuperDatePicker extends TextSelectionMixin.to(DatePatternMixin.to(DatePick setCallingServer(callingServer) { console.log('SDP: configuring text selection listeners; callingServer flag is '+callingServer); - this.listenToEvents(this.shadowRoot.querySelector('vaadin-text-field').inputElement, this, callingServer); + this.listenToEvents(this.shadowRoot.querySelector('vaadin-date-picker-text-field').inputElement, this, callingServer); } } diff --git a/superfields/src/test/java/org/vaadin/miki/shared/dates/DatePatternTest.java b/superfields/src/test/java/org/vaadin/miki/shared/dates/DatePatternTest.java new file mode 100644 index 00000000..448f3138 --- /dev/null +++ b/superfields/src/test/java/org/vaadin/miki/shared/dates/DatePatternTest.java @@ -0,0 +1,34 @@ +package org.vaadin.miki.shared.dates; + +import org.junit.Assert; +import org.junit.Test; + +public class DatePatternTest { + + @Test + public void noSeparatorMeansZeroPrefixedDayAndMonth() { + final DatePattern pattern = new DatePattern().withZeroPrefixedDay(false).withZeroPrefixedMonth(false); + Assert.assertTrue(pattern.hasSeparator()); + pattern.withoutSeparator(); + Assert.assertFalse(pattern.hasSeparator()); + Assert.assertTrue("zero prefixed day must be set when there is no separator", pattern.isZeroPrefixedDay()); + Assert.assertTrue("zero prefixed month must be set when there is no separator", pattern.isZeroPrefixedMonth()); + } + + @Test + public void turningOffZeroPrefixedDaySetsDefaultSeparatorWhenWasNone() { + final DatePattern pattern = new DatePattern().withoutSeparator(); + Assert.assertFalse(pattern.hasSeparator()); + pattern.setZeroPrefixedDay(false); + Assert.assertEquals("separator should be reverted to default", DatePattern.DEFAULT_SEPARATOR, pattern.getSeparator()); + } + + @Test + public void turningOffZeroPrefixedMonthSetsDefaultSeparatorWhenWasNone() { + final DatePattern pattern = new DatePattern().withoutSeparator(); + Assert.assertFalse(pattern.hasSeparator()); + pattern.setZeroPrefixedMonth(false); + Assert.assertEquals("separator should be reverted to default", DatePattern.DEFAULT_SEPARATOR, pattern.getSeparator()); + } + +} \ No newline at end of file