Search code examples
javajavafxdatepickerdate-range

Select multiple dates with DatePicker


I am trying to create a DatePicker that selects multiple dates. I am able to select multiple dates but I would like to keep the DatePicker open while I select them. Problem is, the DatePicker will close every time I select a date.

enter image description here

I don't want to use a private API. I was thinking of adding this:

datePicker.setOnHiding(event -> {            
    event.consume();
});

But it doesn't work.

Here is my code:

public static DatePicker getDatePicker() {
    ObservableList<LocalDate> selectedDates = FXCollections.observableArrayList();
    String pattern = "yyyy-MM-dd";
    DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(pattern);
    DatePicker datePicker = new DatePicker();
    datePicker.setPromptText(pattern);

    datePicker.setConverter(new StringConverter<LocalDate>() {
        @Override
        public String toString(LocalDate date) {
            return (date == null) ? "" : dateFormatter.format(date);
        }

        @Override
        public LocalDate fromString(String string) {
            return ((string == null) || string.isEmpty()) ? null : LocalDate.parse(string, dateFormatter);
        }
    });

    datePicker.setOnAction(event -> {
        selectedDates.add(datePicker.getValue());
        event.consume();
    });

    datePicker.setDayCellFactory((DatePicker param) -> new DateCell() {
        @Override
        public void updateItem(LocalDate item, boolean empty) {
            super.updateItem(item, empty);
            boolean alreadySelected = selectedDates.contains(item);
            setDisable(alreadySelected);
            setStyle(alreadySelected ? "-fx-background-color: #09a30f;" : "");
        }
    });

    return datePicker;
}

Solution

  • If you check DatePickerContent class, you can find that each time a new DateCell is created, an EventHandler of type MOUSE_CLICKED is added to it. This handler will call selectDayCell(DateCell) when the user clicks on a cell. selectDayCell(DateCell) sets the new date value and hides the DatePicker:

    protected void createDayCells() {
        final EventHandler<MouseEvent> dayCellActionHandler = ev -> {
            if (ev.getButton() != MouseButton.PRIMARY) {
                return;
            }
            DateCell dayCell = (DateCell)ev.getSource();
            selectDayCell(dayCell);
            lastFocusedDayCell = dayCell;
        };
        for (int row = 0; row < 6; row++) {
            for (int col = 0; col < daysPerWeek; col++) {
                DateCell dayCell = createDayCell();
                dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, dayCellActionHandler);
                dayCells.add(dayCell);
            }
        }
        dayCellDates = new LocalDate[6 * daysPerWeek];
    }
    
    public void selectDayCell(DateCell dateCell) {
        datePicker.setValue(dayCellDate(dateCell));
        datePicker.hide();
    }
    

    If you're using Java 9 or newer, you can extend DatePickerContent class and override the selectDayCell(DateCell) method to not hide the DatePicker after a cell is selected:

    public void selectDayCell(DateCell dateCell) {
        datePicker.setValue(dayCellDate(dateCell));
    }
    

    Unfortunately, in Java 8, DatePickerContent has a package-private constructor so you can't extend from it. As a workaround, you can add another EventHandler on mouse click that will show the DatePicker again after a cell is clicked:

    EventHandler<MouseEvent> mouseClickedEventHandler = clickEvent -> {
        if (clickEvent.getButton() == MouseButton.PRIMARY) {
            datePicker.show();
        }
        clickEvent.consume();
    };
    

    In your cell factory:

    @Override
    public void updateItem(LocalDate item, boolean empty) {
        super.updateItem(item, empty);
        //...
        if (item != null && !empty) {
            //...
            addEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
        } else {
            //...
            removeEventHandler(MouseEvent.MOUSE_CLICKED, mouseClickedEventHandler);
        }
    }