Skip to content

Commit becd21b

Browse files
Felipe Langjavier-godoy
authored andcommitted
feat: add support for min and max limits
Close #75
1 parent 18b07ef commit becd21b

File tree

3 files changed

+206
-17
lines changed

3 files changed

+206
-17
lines changed

src/main/java/com/flowingcode/addons/ycalendar/YearMonthField.java

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,23 @@
2626
import com.vaadin.flow.component.dependency.JsModule;
2727
import com.vaadin.flow.function.SerializableFunction;
2828
import com.vaadin.flow.internal.JsonSerializer;
29+
import elemental.json.Json;
30+
import elemental.json.JsonValue;
2931
import java.time.YearMonth;
3032
import java.util.Objects;
3133
import java.util.Optional;
3234

3335
@SuppressWarnings("serial")
3436
@Tag("fc-year-month-field")
3537
@JsModule("./fc-year-month-field/fc-year-month-field.js")
36-
public class YearMonthField extends AbstractSinglePropertyField<YearMonthField, YearMonth> implements HasTheme {
38+
public class YearMonthField extends AbstractSinglePropertyField<YearMonthField, YearMonth>
39+
implements HasTheme {
3740

3841
private static final String VALUE_PROPERTY = "value";
3942

43+
private YearMonth max;
44+
private YearMonth min;
45+
4046
private static <R,S> SerializableFunction<R,S> map(SerializableFunction<R,S> f) {
4147
return r->Optional.ofNullable(r).map(f).orElse(null);
4248
}
@@ -57,4 +63,50 @@ public void setI18n(DatePickerI18n i18n) {
5763
getElement().setPropertyJson("i18n", JsonSerializer.toJson(i18n));
5864
}
5965

66+
/**
67+
* Sets the minimum year/month in the field.
68+
*
69+
* @param min the minimum year/month that is allowed to be selected, or <code>null</code> to
70+
* remove any minimum constraints
71+
*/
72+
public void setMin(YearMonth min) {
73+
JsonValue value = min == null ? Json.createNull()
74+
: Json.parse("{'month': " + min.getMonth().ordinal() + ", 'year': " + min.getYear() + "}");
75+
getElement().setPropertyJson("min", value);
76+
this.min = min;
77+
}
78+
79+
/**
80+
* Gets the minimum year/month in the field.
81+
*
82+
* @return the minimum year/month that is allowed to be selected, or <code>null</code> if there's
83+
* no minimum
84+
*/
85+
public YearMonth getMin() {
86+
return min;
87+
}
88+
89+
/**
90+
* Sets the maximum year/month in the field.
91+
*
92+
* @param min the maximum year/month that is allowed to be selected, or <code>null</code> to
93+
* remove any maximum constraints
94+
*/
95+
public void setMax(YearMonth max) {
96+
JsonValue value = max == null ? Json.createNull()
97+
: Json.parse("{'month': " + max.getMonth().ordinal() + ", 'year': " + max.getYear() + "}");
98+
getElement().setPropertyJson("max", value);
99+
this.max = max;
100+
}
101+
102+
/**
103+
* Gets the maximum year/month in the field.
104+
*
105+
* @return the maximum year/month that is allowed to be selected, or <code>null</code> if there's
106+
* no maximum
107+
*/
108+
public YearMonth getMax() {
109+
return max;
110+
}
111+
60112
}

src/main/resources/META-INF/frontend/fc-year-month-field/fc-year-month-field.js

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* #L%
1919
*/
2020
import { css, html, LitElement } from 'lit';
21-
21+
2222
export class YearMonthField extends LitElement {
2323

2424
static get is() { return 'fc-year-month-field'; }
@@ -29,6 +29,8 @@ export class YearMonthField extends LitElement {
2929
date: {type: Date},
3030
year: {type: Number, readOnly: true, state: true},
3131
month: {type: Number, readOnly: true, state: true},
32+
min: {type: Object, readOnly: true},
33+
max: {type: Object, readOnly: true},
3234
i18n: {type: Object}
3335
}
3436
}
@@ -39,6 +41,10 @@ export class YearMonthField extends LitElement {
3941
this._i18n = {};
4042
this.__setDefaultFormatTitle(this._i18n);
4143
this.__setDefaultMonthNames(this._i18n);
44+
this._decMonthDisabled = false;
45+
this._incMonthDisabled = false;
46+
this._decYearDisabled = false;
47+
this._incYearDisabled = false;
4248
}
4349

4450
set i18n(value) {
@@ -56,19 +62,42 @@ export class YearMonthField extends LitElement {
5662

5763
get i18n() { return this._i18n; }
5864

65+
/**
66+
* Checks if value is between min and max (inclusive).
67+
*/
68+
__checkRange(value) {
69+
return (!this.min || value >= this._minAsNumber)
70+
&& (!this.max || value <= this._maxAsNumber);
71+
}
72+
5973
willUpdate(changedProperties) {
6074
if (changedProperties.has('value')) {
61-
this.date = this.value ? new Date(this.value.substring(0,7)+'-02') : new Date();
62-
}
63-
if (changedProperties.has('date')) {
64-
this.value = this.date.toISOString().substring(0,7);
65-
this.date.setDate(1);
66-
this._year = this.date.getFullYear();
67-
this._month = this.date.getMonth()+1;
75+
this.date = this.value ? new Date(this.value.substring(0,7)+'-02') : new Date();
6876
}
6977
if (changedProperties.has('i18n') && !this.i18n) {
7078
this.i18n = changedProperties.get('i18n');
7179
}
80+
if (changedProperties.has('date') || changedProperties.has('min') || changedProperties.has('max')) {
81+
const strValue = this.date.toISOString().substring(0,7);
82+
this.__normalizeValue(strValue);
83+
this.date.setDate(1);
84+
this._year = this.date.getFullYear();
85+
this._month = this.date.getMonth() + 1;
86+
this.__toggleButtons(this.min, this.max);
87+
}
88+
}
89+
90+
__normalizeValue(value) {
91+
const intValue = parseInt(this.__yearMonthAsNumber(this.date.getFullYear(), this.date.getMonth()));
92+
if (this.min && intValue < this._minAsNumber){
93+
this.value = this.min.year + '-' + String(this.min.month + 1).padStart(2, '0');
94+
this.date = new Date(this.min.year, this.min.month);
95+
} if (this.max && intValue > this._maxAsNumber){
96+
this.value = this.max.year + '-' + String(this.max.month + 1).padStart(2, '0');
97+
this.date = new Date(this.max.year, this.max.month);
98+
} else {
99+
this.value = value;
100+
}
72101
}
73102

74103
updated(changedProperties) {
@@ -96,11 +125,11 @@ export class YearMonthField extends LitElement {
96125

97126
render() {
98127
return html`
99-
<vaadin-button theme="small icon" @click="${this.__decYear}">&lt;&lt;</vaadin-button>
100-
<vaadin-button theme="small icon" @click="${this.__decMonth}">&lt;</vaadin-button>
128+
<vaadin-button id="dec-year-button"theme="small icon" @click="${this.__decYear}" ?disabled="${this._decYearDisabled}">&lt;&lt;</vaadin-button>
129+
<vaadin-button id="dec-month-button" theme="small icon" @click="${this.__decMonth}" ?disabled="${this._decMonthDisabled}">&lt;</vaadin-button>
101130
<div part="header">${this.formatTitle(this._month, this._year)}</div>
102-
<vaadin-button theme="small icon" @click="${this.__incMonth}">&gt;</vaadin-button>
103-
<vaadin-button theme="small icon" @click="${this.__incYear}">&gt;&gt;</vaadin-button>
131+
<vaadin-button id="inc-month-button" theme="small icon" @click="${this.__incMonth}" ?disabled="${this._incMonthDisabled}">&gt;</vaadin-button>
132+
<vaadin-button id="inc-year-button" theme="small icon" @click="${this.__incYear}" ?disabled="${this._incYearDisabled}">&gt;&gt;</vaadin-button>
104133
`;
105134
}
106135

@@ -116,19 +145,27 @@ export class YearMonthField extends LitElement {
116145
}
117146

118147
__decYear() {
119-
this.__addMonths(-12);
148+
if(!this._minAsNumber || this._minAsNumber < this.__dateAsNumber()){
149+
this.__addMonths(-12);
150+
}
120151
}
121152

122153
__decMonth() {
123-
this.__addMonths(-1);
154+
if(!this._minAsNumber || this._minAsNumber < this.__dateAsNumber()){
155+
this.__addMonths(-1);
156+
}
124157
}
125158

126159
__incYear() {
127-
this.__addMonths(12);
160+
if(!this._maxAsNumber || this._maxAsNumber > this.__dateAsNumber()){
161+
this.__addMonths(12);
162+
}
128163
}
129164

130165
__incMonth() {
131-
this.__addMonths(1);
166+
if(!this._maxAsNumber || this._maxAsNumber > this.__dateAsNumber()){
167+
this.__addMonths(1);
168+
}
132169
}
133170

134171
__setDefaultFormatTitle(obj){
@@ -138,6 +175,65 @@ export class YearMonthField extends LitElement {
138175
__setDefaultMonthNames(obj){
139176
obj.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
140177
}
178+
179+
/**
180+
* Returns a number joining year and month. Month is left padded with zero up to two chars length.
181+
*/
182+
__yearMonthAsNumber(year, month) {
183+
return parseInt(year + '' + String(month).padStart(2, '0'));
184+
}
185+
186+
/**
187+
* Converts this.date to yearMonth number
188+
*/
189+
__dateAsNumber() {
190+
return this.date ? this.__yearMonthAsNumber(this.date.getFullYear(), this.date.getMonth()) : undefined;
191+
}
192+
193+
/**
194+
* Converts min or max value to yearMonth number.
195+
*/
196+
__minMaxAsNumber(value) {
197+
return value ? this.__yearMonthAsNumber(value.year, value.month) : undefined;
198+
}
199+
200+
/**
201+
* Converts this.min to number
202+
* @private
203+
*/
204+
get _minAsNumber() {
205+
return this.__minMaxAsNumber(this.min);
206+
}
207+
208+
/**
209+
* Converts this.max to number
210+
* @private
211+
*/
212+
get _maxAsNumber() {
213+
return this.__minMaxAsNumber(this.max);
214+
}
215+
216+
/**
217+
* Returns true if delta between minOrMax and this.date is less than 1 year (12 months)
218+
*/
219+
__dateDeltaLtYear(minOrMax) {
220+
const toMonths = (year, month) => year * 12 + month;
221+
const dateToMonths = toMonths(this.date.getFullYear(), this.date.getMonth());
222+
return Math.abs(dateToMonths - toMonths(minOrMax.year, minOrMax.month)) < 12;
223+
}
224+
225+
/**
226+
* Enable or disabled navigation buttons
227+
*/
228+
__toggleButtons(min, max) {
229+
const minAsNumber = this.__minMaxAsNumber(min);
230+
const maxAsNumber = this.__minMaxAsNumber(max);
231+
232+
this._decMonthDisabled = minAsNumber && minAsNumber >= this.__dateAsNumber();
233+
this._incMonthDisabled = maxAsNumber && maxAsNumber <= this.__dateAsNumber();
234+
this._decYearDisabled = minAsNumber && (minAsNumber >= this.__dateAsNumber() || this.__dateDeltaLtYear(min));
235+
this._incYearDisabled = maxAsNumber && (maxAsNumber <= this.__dateAsNumber() || this.__dateDeltaLtYear(max));
236+
}
141237

142238
}
143239

src/test/java/com/flowingcode/addons/ycalendar/YearMonthFieldDemo.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@
2020
package com.flowingcode.addons.ycalendar;
2121

2222
import com.flowingcode.vaadin.addons.demo.DemoSource;
23+
import com.vaadin.flow.component.Text;
24+
import com.vaadin.flow.component.button.Button;
2325
import com.vaadin.flow.component.datepicker.DatePicker.DatePickerI18n;
2426
import com.vaadin.flow.component.html.Div;
27+
import com.vaadin.flow.component.html.Span;
28+
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
2529
import com.vaadin.flow.component.radiobutton.RadioButtonGroup;
2630
import com.vaadin.flow.router.PageTitle;
2731
import com.vaadin.flow.router.Route;
32+
import java.time.YearMonth;
2833
import java.util.List;
34+
import java.util.Optional;
2935

3036
@DemoSource
3137
@PageTitle("Year-Month Field")
@@ -62,6 +68,41 @@ public YearMonthFieldDemo() {
6268
});
6369

6470
add(languageSelector);
71+
72+
Span minRangeValue = new Span("-");
73+
Span maxRangeValue = new Span("-");
74+
add(new Div(new Text("Min: "), minRangeValue, new Text(" :: Max: "), maxRangeValue));
75+
76+
YearMonth min = YearMonth.now().minusYears(2);
77+
Button setMinRangeButton = new Button("Set min " + min.toString());
78+
setMinRangeButton.addClickListener(e -> {
79+
field.setMin(min);
80+
minRangeValue
81+
.setText(Optional.ofNullable(field.getMin()).map(YearMonth::toString).orElse("-"));
82+
});
83+
84+
YearMonth max = YearMonth.now().plusYears(2);
85+
Button setMaxRangeButton = new Button("Set max " + max.toString());
86+
setMaxRangeButton.addClickListener(e -> {
87+
field.setMax(max);
88+
maxRangeValue
89+
.setText(Optional.ofNullable(field.getMax()).map(YearMonth::toString).orElse("-"));
90+
});
91+
92+
Button clearRangeButton = new Button("Clear range");
93+
clearRangeButton.addClickListener(e -> {
94+
field.setMin(null);
95+
field.setMax(null);
96+
minRangeValue.setText("-");
97+
maxRangeValue.setText("-");
98+
});
99+
100+
YearMonth newValue = YearMonth.now().plusYears(3);
101+
Button setValueButton = new Button("Set value " + newValue.toString());
102+
setValueButton.addClickListener(e -> field.setValue(newValue));
103+
104+
add(new HorizontalLayout(setMinRangeButton, setMaxRangeButton, clearRangeButton,
105+
setValueButton));
65106
}
66107

67108
}

0 commit comments

Comments
 (0)