getDatePickerI18n()
145 | {
146 | return this.datePickerI18n;
147 | }
148 |
149 | /**
150 | * Sets the locale used for formatting the "expand" button.
151 | * If the locale is null (default) the clientside locale will be used or {@link Locale#US} if none
152 | * could be detected.
153 | *
154 | */
155 | public DateRangePicker withFormatLocale(final Locale locale)
156 | {
157 | this.useClientSideLocale = locale == null;
158 | this.formatLocale = Optional.ofNullable(locale);
159 | return this;
160 | }
161 |
162 | public Locale getFormatLocale()
163 | {
164 | return this.formatLocale.orElse(DEFAULT_LOCALE);
165 | }
166 |
167 | public DateRangePicker withDateRangeLocalizerFunction(final ItemLabelGenerator dateRangeLocalizerFunction)
168 | {
169 | this.dateRangeLocalizerFunction = dateRangeLocalizerFunction;
170 | return this;
171 | }
172 |
173 | public ItemLabelGenerator getDateRangeLocalizerFunction()
174 | {
175 | return this.dateRangeLocalizerFunction;
176 | }
177 |
178 | /**
179 | * Shortcut for {@link DateRangePicker#setStartLabel(String)}
180 | */
181 | public DateRangePicker withStartLabel(final String label)
182 | {
183 | this.setStartLabel(label);
184 | return this;
185 | }
186 |
187 | /**
188 | * Shortcut for {@link DateRangePicker#setEndLabel(String)}
189 | */
190 | public DateRangePicker withEndLabel(final String label)
191 | {
192 | this.setEndLabel(label);
193 | return this;
194 | }
195 |
196 | /**
197 | * Shortcut for {@link DateRangePicker#setDateRangeOptionsLabel(String)}
198 | */
199 | public DateRangePicker withDateRangeOptionsLabel(final String label)
200 | {
201 | this.setDateRangeOptionsLabel(label);
202 | return this;
203 | }
204 |
205 | /**
206 | * Shortcut for {@link DateRangePicker#setAllowRangeLimitExceeding(boolean)}
207 | */
208 | public DateRangePicker withAllowRangeLimitExceeding(final boolean allowRangeLimitExceeding)
209 | {
210 | this.setAllowRangeLimitExceeding(allowRangeLimitExceeding);
211 | return this;
212 | }
213 |
214 | // endregion
215 |
216 | protected void initUI()
217 | {
218 | // Set an unique ID for each element
219 | this.setId("DateRangePickerID" + nextID.incrementAndGet());
220 |
221 | this.btnOverview.addClassNames(DateRangePickerStyles.BUTTON, DateRangePickerStyles.CLICKABLE);
222 | this.btnOverview.setWidthFull();
223 |
224 | this.btnOverview.setDisableOnClick(true);
225 |
226 | this.overlay.addClassName(DateRangePickerStyles.OVERLAY_LAYOUT);
227 |
228 | this.overlay.setWidthFull();
229 | this.overlay.setHeight("auto");
230 |
231 | this.overlayContainer.setWidthFull();
232 | this.overlayContainer.addClassName(DateRangePickerStyles.OVERLAY_BASE);
233 | this.overlayContainer.add(this.overlay);
234 |
235 | this.getContent().setSpacing(false);
236 | this.getContent().setPadding(false);
237 | this.setSizeUndefined();
238 | this.add(this.btnOverview, this.overlayContainer);
239 |
240 | this.setExpanded(false);
241 | }
242 |
243 | protected void registerListeners()
244 | {
245 | this.btnOverview.addClickListener(ev ->
246 | {
247 | this.toggle();
248 | ev.getSource().setEnabled(true);
249 | });
250 | this.overlay.addValueChangeListener(ev ->
251 | {
252 | this.model = ev.getSource().getModel();
253 |
254 | this.updateFromModel(false);
255 | this.fireEvent(new DateRangeValueChangeEvent<>(this, ev.getOldValue(), ev.isFromClient()));
256 | });
257 | }
258 |
259 | @Override
260 | protected void onAttach(final AttachEvent attachEvent)
261 | {
262 | this.setLocaleFromClient();
263 |
264 | this.updateFromModel(true);
265 |
266 | this.addClickOutsideListener();
267 | }
268 |
269 | protected void setLocaleFromClient()
270 | {
271 | if(this.useClientSideLocale)
272 | {
273 | this.formatLocale = Optional.ofNullable(VaadinService.getCurrentRequest().getLocale());
274 | }
275 | }
276 |
277 | protected void addClickOutsideListener()
278 | {
279 | if(!this.isCloseOnOutsideClick())
280 | {
281 | return;
282 | }
283 |
284 | final String funcName = "outsideClickFunc" + this.getId().orElseThrow();
285 |
286 | final String jsCommand = String.join(
287 | "\r\n",
288 | // Define Click-Function
289 | "var " + funcName + " = function(event) {",
290 | // Get the current Element
291 | " var spEl = document.getElementById('" + this.getId().orElseThrow() + "');",
292 | " if (!spEl) {",
293 | // If the element got detached/removed, then als delete the listener of the base element
294 | " document.removeEventListener('click'," + funcName + ");",
295 | " return;",
296 | " }",
297 | // Check if a Vaadin overlay caused the click
298 | " let parent = event.target;",
299 | // Check all parents of clicked element
300 | " while(parent) {",
301 | // Check if a vaadin overlay was clicked:
302 | // Fist check if the tagName indicates a Vaadin overlay
303 | // If not fallback to id='overlay'
304 | " let tagName = parent.tagName.toLowerCase();",
305 | " if((tagName.includes('vaadin') && tagName.includes('overlay')) || parent.id == 'overlay') {",
306 | " return;",
307 | " }",
308 | " parent = parent.parentElement;",
309 | " }",
310 | // Check if the click was done on this element
311 | " var isClickInside = spEl.contains(event.target);",
312 | " if (!isClickInside) {",
313 | " spEl.$server.clickOutsideOccurred();",
314 | " }",
315 | "}; ",
316 | "document.body.addEventListener('click'," + funcName + ");"
317 | );
318 |
319 | this.getContent().getElement().executeJs(jsCommand);
320 | }
321 |
322 | @ClientCallable
323 | protected void clickOutsideOccurred()
324 | {
325 | if(!this.isCloseOnOutsideClick())
326 | {
327 | return;
328 | }
329 |
330 | if(this.isExpanded())
331 | {
332 | this.setExpanded(false);
333 | }
334 | }
335 |
336 | protected void updateFromModel(final boolean updateOverlay)
337 | {
338 | if(updateOverlay)
339 | {
340 | this.tryFixInvalidModel();
341 | }
342 |
343 | final DateTimeFormatter formatter =
344 | DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(this.getFormatLocale());
345 |
346 | this.btnOverview.setText(this.model.getStart().format(formatter)
347 | + (this.model.getStart().equals(this.model.getEnd()) ? "" : " - " + this.model.getEnd().format(formatter))
348 | );
349 |
350 | if(updateOverlay)
351 | {
352 | this.overlay.setModel(this.model);
353 | }
354 | }
355 |
356 | protected void tryFixInvalidModel()
357 | {
358 | this.model.getDateRange()
359 | .calcFor(this.model.getStart())
360 | .ifPresent(result -> {
361 | this.model.setStart(result.getStart());
362 | this.model.setEnd(result.getEnd());
363 | });
364 | }
365 |
366 | protected void toggle()
367 | {
368 | this.setExpanded(!this.isExpanded());
369 | }
370 |
371 | protected synchronized void setExpanded(final boolean expanded)
372 | {
373 | this.expanded = expanded;
374 | this.btnOverview.setIcon(expanded ? VaadinIcon.CARET_DOWN.create() : VaadinIcon.CARET_UP.create());
375 |
376 | this.overlay.setVisible(expanded);
377 | }
378 |
379 | public synchronized boolean isExpanded()
380 | {
381 | return this.expanded;
382 | }
383 |
384 | // region Get UI elements
385 |
386 | public DateRangePickerOverlay getOverlay()
387 | {
388 | return this.overlay;
389 | }
390 |
391 | public Button getBtnOverview()
392 | {
393 | return this.btnOverview;
394 | }
395 |
396 | public Div getOverlayContainer()
397 | {
398 | return this.overlayContainer;
399 | }
400 |
401 | // endregion
402 |
403 | // region Labels
404 |
405 | /**
406 | * Sets the label for the overlay Start-DatePicker
407 | */
408 | public void setStartLabel(final String label)
409 | {
410 | Objects.requireNonNull(label);
411 | this.getOverlay().getDpStart().setLabel(label);
412 | }
413 |
414 | /**
415 | * Sets the label for the overlay End-DatePicker
416 | */
417 | public void setEndLabel(final String label)
418 | {
419 | Objects.requireNonNull(label);
420 | this.getOverlay().getDpEnd().setLabel(label);
421 | }
422 |
423 | /**
424 | * Sets the label for the overlay DateRange-ComboBox
425 | */
426 | public void setDateRangeOptionsLabel(final String label)
427 | {
428 | Objects.requireNonNull(label);
429 | this.getOverlay().getCbDateRange().setLabel(label);
430 | }
431 |
432 | // endregion
433 |
434 | // region AllowRangeLimitExceeding
435 |
436 | /**
437 | * Allows the maximum start and end date to be greater or less than the configured end or start date.
438 | *
439 | * This is only the case when {@link DateRange#isCalcable()} is true. Otherwise incorrect values (e.g.
440 | * start before end) could be set.
441 | */
442 | public void setAllowRangeLimitExceeding(final boolean allowRangeLimitExceeding)
443 | {
444 | this.allowRangeLimitExceeding = allowRangeLimitExceeding;
445 | }
446 |
447 | public boolean isAllowRangeLimitExceeding()
448 | {
449 | return this.allowRangeLimitExceeding;
450 | }
451 |
452 | // endregion
453 |
454 | // region Data
455 |
456 | /**
457 | * Uses the given {@link DateRange} and calculates with the current Date the {@link DateRangeModel}, which is then
458 | * set by {@link DateRangePicker#setValue(DateRangeModel)}
459 | */
460 | public void setDateRangeForToday(final D range)
461 | {
462 | range.calcFor(LocalDate.now()).ifPresent(
463 | result -> this.setValue(new DateRangeModel<>(result.getStart(), result.getEnd(), range)));
464 | }
465 |
466 | @Override
467 | public void setItems(final Collection items)
468 | {
469 | this.overlay.setItems(items);
470 | }
471 |
472 | @Override
473 | public LocalDate getStart()
474 | {
475 | return this.model.getStart();
476 | }
477 |
478 | @Override
479 | public DateRangePicker setStart(final LocalDate start)
480 | {
481 | this.model.setStart(start);
482 | this.updateFromModel(true);
483 | return this;
484 | }
485 |
486 | @Override
487 | public LocalDate getEnd()
488 | {
489 | return this.model.getEnd();
490 | }
491 |
492 | @Override
493 | public DateRangePicker setEnd(final LocalDate end)
494 | {
495 | this.model.setEnd(end);
496 | this.updateFromModel(true);
497 | return this;
498 | }
499 |
500 | @Override
501 | public D getDateRange()
502 | {
503 | return this.model.getDateRange();
504 | }
505 |
506 | @Override
507 | public DateRangePicker setDateRange(final D dateRange)
508 | {
509 | this.model.setDateRange(dateRange);
510 | this.updateFromModel(true);
511 | return this;
512 | }
513 |
514 | @Override
515 | public void setValue(final DateRangeModel value)
516 | {
517 | Objects.requireNonNull(value);
518 |
519 | this.model = value;
520 | this.updateFromModel(true);
521 | }
522 |
523 | @Override
524 | public DateRangeModel getValue()
525 | {
526 | return this.model;
527 | }
528 |
529 | @SuppressWarnings("unchecked")
530 | @Override
531 | public Registration addValueChangeListener(final ValueChangeListener super DateRangeValueChangeEvent> listener)
532 | {
533 | @SuppressWarnings("rawtypes")
534 | final ComponentEventListener componentListener =
535 | event -> listener.valueChanged((DateRangeValueChangeEvent)event);
536 |
537 | return ComponentUtil.addListener(this, DateRangeValueChangeEvent.class, componentListener);
538 | }
539 |
540 | /**
541 | * DateRangePicker always has a value
542 | * However for compatibility reasons (with Vaadin) this returns {@code null}
543 | * @return {@code null}
544 | */
545 | @Override
546 | public DateRangeModel getEmptyValue()
547 | {
548 | return null;
549 | }
550 |
551 | /**
552 | * DateRangePicker always has a value
553 | * Therefore this always returns {@code false}
554 | *
555 | * @return {@code false}
556 | */
557 | @Override
558 | public boolean isEmpty()
559 | {
560 | return false;
561 | }
562 |
563 | /**
564 | * Do not use this method, as it throws a {@link UnsupportedOperationException}
565 | * The calling of clear is not supported because DateRangePicker always has a value
566 | * Use {@link DateRangePicker#setValue(DateRangeModel)} instead.
567 | *
568 | * @throws UnsupportedOperationException DateRangePicker always has a value
569 | */
570 | @Override
571 | public void clear()
572 | {
573 | throw new UnsupportedOperationException(
574 | "The calling of clear is not supported because DateRangePicker always has a value");
575 | }
576 |
577 | @Override
578 | public void setReadOnly(final boolean readOnly)
579 | {
580 | this.getOverlay().setReadOnly(readOnly);
581 | }
582 |
583 | @Override
584 | public boolean isReadOnly()
585 | {
586 | return this.getOverlay().isReadOnly();
587 | }
588 |
589 | /**
590 | * The required indicator is not implemented
591 | *
592 | * This method doesn't have any functionallity
593 | */
594 | @Override
595 | public void setRequiredIndicatorVisible(final boolean requiredIndicatorVisible)
596 | {
597 | // Not required/implemented
598 | }
599 |
600 | /**
601 | * The required indicator is not implemented
This will always return {@code false}
602 | *
603 | * @return {@code false}
604 | */
605 | @Override
606 | public boolean isRequiredIndicatorVisible()
607 | {
608 | return false;
609 | }
610 |
611 | // endregion
612 | }
613 |
--------------------------------------------------------------------------------