123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737 |
- /*
- * Copyright 2000-2016 Vaadin Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
- package com.vaadin.client.ui;
-
- import java.util.Date;
-
- import com.google.gwt.aria.client.Id;
- import com.google.gwt.aria.client.LiveValue;
- import com.google.gwt.aria.client.Roles;
- import com.google.gwt.dom.client.Element;
- import com.google.gwt.event.dom.client.ClickEvent;
- import com.google.gwt.event.dom.client.ClickHandler;
- import com.google.gwt.event.dom.client.DomEvent;
- import com.google.gwt.event.dom.client.KeyCodes;
- import com.google.gwt.event.dom.client.MouseOutEvent;
- import com.google.gwt.event.dom.client.MouseOutHandler;
- import com.google.gwt.event.dom.client.MouseOverEvent;
- import com.google.gwt.event.dom.client.MouseOverHandler;
- import com.google.gwt.event.logical.shared.CloseEvent;
- import com.google.gwt.event.logical.shared.CloseHandler;
- import com.google.gwt.i18n.client.DateTimeFormat;
- import com.google.gwt.user.client.DOM;
- import com.google.gwt.user.client.Event;
- import com.google.gwt.user.client.Timer;
- import com.google.gwt.user.client.Window;
- import com.google.gwt.user.client.ui.Button;
- import com.google.gwt.user.client.ui.FlowPanel;
- import com.google.gwt.user.client.ui.Label;
- import com.google.gwt.user.client.ui.PopupPanel;
- import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
- import com.google.gwt.user.client.ui.RootPanel;
- import com.google.gwt.user.client.ui.Widget;
- import com.vaadin.client.BrowserInfo;
- import com.vaadin.client.ComputedStyle;
- import com.vaadin.client.VConsole;
- import com.vaadin.client.ui.VAbstractCalendarPanel.FocusOutListener;
- import com.vaadin.client.ui.VAbstractCalendarPanel.SubmitListener;
- import com.vaadin.client.ui.aria.AriaHelper;
- import com.vaadin.shared.ui.datefield.TextualDateFieldState;
-
- /**
- * Represents a date selection component with a text field and a popup date/time
- * selector.
- *
- * <b>Note:</b> To change the keyboard assignments used in the popup dialog you
- * should extend <code>com.vaadin.client.ui.VAbstractCalendarPanel</code> and
- * then pass set it by calling the
- * <code>setCalendarPanel(VAbstractCalendarPanel panel)</code> method.
- *
- * @since 8.0
- */
- public abstract class VAbstractPopupCalendar<PANEL extends VAbstractCalendarPanel<R>, R extends Enum<R>>
- extends VAbstractTextualDate<R>
- implements Field, ClickHandler, CloseHandler<PopupPanel>, SubPartAware {
-
- /** For internal use only. May be removed or replaced in the future. */
- public final Button calendarToggle = new Button();
-
- /** For internal use only. May be removed or replaced in the future. */
- public PANEL calendar;
-
- /** For internal use only. May be removed or replaced in the future. */
- public final VOverlay popup;
-
- /** For internal use only. May be removed or replaced in the future. */
- public boolean parsable = true;
-
- private boolean open;
-
- /*
- * #14857: If calendarToggle button is clicked when calendar popup is
- * already open we should prevent calling openCalendarPanel() in onClick,
- * since we don't want to reopen it again right after it closes.
- */
- private boolean preventOpenPopupCalendar;
- private boolean cursorOverCalendarToggleButton;
- private boolean toggleButtonClosesWithGuarantee;
-
- private boolean textFieldEnabled = true;
-
- private String captionId;
-
- private Label selectedDate;
-
- private Element descriptionForAssistiveDevicesElement;
-
- private static final String CALENDAR_TOGGLE_ID = "popupButton";
-
- public VAbstractPopupCalendar(PANEL calendarPanel, R resolution) {
- super(resolution);
-
- calendarToggle.setText("");
- calendarToggle.addClickHandler(this);
-
- calendarToggle.addDomHandler(new MouseOverHandler() {
- @Override
- public void onMouseOver(MouseOverEvent event) {
- cursorOverCalendarToggleButton = true;
- }
- }, MouseOverEvent.getType());
-
- calendarToggle.addDomHandler(new MouseOutHandler() {
- @Override
- public void onMouseOut(MouseOutEvent event) {
- cursorOverCalendarToggleButton = false;
- }
- }, MouseOutEvent.getType());
-
- // -2 instead of -1 to avoid FocusWidget.onAttach to reset it
- calendarToggle.getElement().setTabIndex(-2);
-
- Roles.getButtonRole().set(calendarToggle.getElement());
- Roles.getButtonRole().setAriaHiddenState(calendarToggle.getElement(),
- true);
-
- add(calendarToggle);
-
- // Description of the usage of the widget for assistive device users
- descriptionForAssistiveDevicesElement = DOM.createDiv();
- descriptionForAssistiveDevicesElement.setInnerText(
- TextualDateFieldState.DESCRIPTION_FOR_ASSISTIVE_DEVICES);
- AriaHelper.ensureHasId(descriptionForAssistiveDevicesElement);
- Roles.getTextboxRole().setAriaDescribedbyProperty(text.getElement(),
- Id.of(descriptionForAssistiveDevicesElement));
- AriaHelper.setVisibleForAssistiveDevicesOnly(
- descriptionForAssistiveDevicesElement, true);
-
- calendar = calendarPanel;
- calendar.setParentField(this);
- calendar.setFocusOutListener(new FocusOutListener() {
- @Override
- public boolean onFocusOut(DomEvent<?> event) {
- event.preventDefault();
- closeCalendarPanel();
- return true;
- }
- });
-
- // FIXME: Problem is, that the element with the provided id does not
- // exist yet in html. This is the same problem as with the context menu.
- // Apply here the same fix (#11795)
- Roles.getTextboxRole().setAriaControlsProperty(text.getElement(),
- Id.of(calendar.getElement()));
- Roles.getButtonRole().setAriaControlsProperty(
- calendarToggle.getElement(), Id.of(calendar.getElement()));
-
- calendar.setSubmitListener(new SubmitListener() {
- @Override
- public void onSubmit() {
- // Update internal value and send valuechange event if immediate
- updateValue(calendar.getDate());
-
- // Update text field (a must when not immediate).
- buildDate(true);
-
- closeCalendarPanel();
- }
-
- @Override
- public void onCancel() {
- closeCalendarPanel();
- }
- });
-
- popup = new VOverlay(true, false);
- popup.setOwner(this);
-
- FlowPanel wrapper = new FlowPanel();
- selectedDate = new Label();
- selectedDate.setStyleName(getStylePrimaryName() + "-selecteddate");
- AriaHelper.setVisibleForAssistiveDevicesOnly(selectedDate.getElement(),
- true);
-
- Roles.getTextboxRole().setAriaLiveProperty(selectedDate.getElement(),
- LiveValue.ASSERTIVE);
- Roles.getTextboxRole().setAriaAtomicProperty(selectedDate.getElement(),
- true);
- wrapper.add(selectedDate);
- wrapper.add(calendar);
-
- popup.setWidget(wrapper);
- popup.addCloseHandler(this);
-
- DOM.setElementProperty(calendar.getElement(), "id",
- "PID_VAADIN_POPUPCAL");
-
- sinkEvents(Event.ONKEYDOWN);
-
- updateStyleNames();
- }
-
- @Override
- protected void onAttach() {
- super.onAttach();
- DOM.appendChild(RootPanel.get().getElement(),
- descriptionForAssistiveDevicesElement);
- }
-
- @Override
- protected void onDetach() {
- super.onDetach();
- descriptionForAssistiveDevicesElement.removeFromParent();
- closeCalendarPanel();
- }
-
- /**
- * Changes the current date, and updates the
- * {@link VDateField#bufferedResolutions}, possibly
- * {@link VDateField#sendBufferedValues()} to the server if needed
- *
- * @param newDate
- * the new {@code Date} to update
- */
- @SuppressWarnings("deprecation")
- public void updateValue(Date newDate) {
- Date currentDate = getCurrentDate();
- R resolution = getCurrentResolution();
- if (currentDate == null || newDate.getTime() != currentDate.getTime()) {
- setCurrentDate((Date) newDate.clone());
- bufferedResolutions.put(calendar.getResolution(calendar::isYear),
- newDate.getYear() + 1900);
- if (!calendar.isYear(resolution)) {
- bufferedResolutions.put(
- calendar.getResolution(calendar::isMonth),
- newDate.getMonth() + 1);
- if (!calendar.isMonth(resolution)) {
- bufferedResolutions.put(
- calendar.getResolution(calendar::isDay),
- newDate.getDate());
- }
- }
- }
- }
-
- /**
- * Checks whether the text field is enabled.
- *
- * @see VAbstractPopupCalendar#setTextFieldEnabled(boolean)
- * @return The current state of the text field.
- */
- public boolean isTextFieldEnabled() {
- return textFieldEnabled;
- }
-
- /**
- * Sets the state of the text field of this component. By default the text
- * field is enabled. Disabling it causes only the button for date selection
- * to be active, thus preventing the user from entering invalid dates. See
- * <a href="http://dev.vaadin.com/ticket/6790>#6790</a>.
- *
- * @param textFieldEnabled
- */
- public void setTextFieldEnabled(boolean textFieldEnabled) {
- this.textFieldEnabled = textFieldEnabled;
- updateTextFieldEnabled();
- }
-
- protected void updateTextFieldEnabled() {
- boolean reallyEnabled = isEnabled() && isTextFieldEnabled();
- // IE has a non input disabled themeing that can not be overridden so we
- // must fake the functionality using readonly and unselectable
- if (BrowserInfo.get().isIE()) {
- if (!reallyEnabled) {
- text.getElement().setAttribute("unselectable", "on");
- text.getElement().setAttribute("readonly", "");
- text.setTabIndex(-2);
- } else if (reallyEnabled
- && text.getElement().hasAttribute("unselectable")) {
- text.getElement().removeAttribute("unselectable");
- text.getElement().removeAttribute("readonly");
- text.setTabIndex(0);
- }
- } else {
- text.setEnabled(reallyEnabled);
- }
-
- if (reallyEnabled) {
- calendarToggle.setTabIndex(-1);
- Roles.getButtonRole()
- .setAriaHiddenState(calendarToggle.getElement(), true);
- } else {
- calendarToggle.setTabIndex(0);
- Roles.getButtonRole()
- .setAriaHiddenState(calendarToggle.getElement(), false);
- }
-
- handleAriaAttributes();
- }
-
- /**
- * Set correct tab index for disabled text field in IE as the value set in
- * setTextFieldEnabled(...) gets overridden in
- * TextualDateConnection.updateFromUIDL(...).
- *
- * @since 7.3.1
- */
- public void setTextFieldTabIndex() {
- if (BrowserInfo.get().isIE() && !textFieldEnabled) {
- // index needs to be -2 because FocusWidget updates -1 to 0 onAttach
- text.setTabIndex(-2);
- }
- }
-
- @Override
- public void bindAriaCaption(
- com.google.gwt.user.client.Element captionElement) {
- if (captionElement == null) {
- captionId = null;
- } else {
- captionId = captionElement.getId();
- }
-
- if (isTextFieldEnabled()) {
- super.bindAriaCaption(captionElement);
- } else {
- AriaHelper.bindCaption(calendarToggle, captionElement);
- }
-
- handleAriaAttributes();
- }
-
- private void handleAriaAttributes() {
- Widget removeFromWidget;
- Widget setForWidget;
-
- if (isTextFieldEnabled()) {
- setForWidget = text;
- removeFromWidget = calendarToggle;
- } else {
- setForWidget = calendarToggle;
- removeFromWidget = text;
- }
-
- Roles.getFormRole()
- .removeAriaLabelledbyProperty(removeFromWidget.getElement());
- if (captionId == null) {
- Roles.getFormRole()
- .removeAriaLabelledbyProperty(setForWidget.getElement());
- } else {
- Roles.getFormRole().setAriaLabelledbyProperty(
- setForWidget.getElement(), Id.of(captionId));
- }
- }
-
- /*
- * (non-Javadoc)
- *
- * @see
- * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String)
- */
- @Override
- public void setStyleName(String style) {
- super.setStyleName(style);
- updateStyleNames();
- }
-
- @Override
- public void setStylePrimaryName(String style) {
- removeStyleName(getStylePrimaryName() + "-popupcalendar");
- super.setStylePrimaryName(style);
- updateStyleNames();
- }
-
- @Override
- protected void updateStyleNames() {
- super.updateStyleNames();
- if (getStylePrimaryName() != null && calendarToggle != null) {
- addStyleName(getStylePrimaryName() + "-popupcalendar");
- calendarToggle.setStyleName(getStylePrimaryName() + "-button");
- popup.setStyleName(getStylePrimaryName() + "-popup");
- calendar.setStyleName(getStylePrimaryName() + "-calendarpanel");
- }
- }
-
- /**
- * Opens the calendar panel popup.
- */
- public void openCalendarPanel() {
-
- if (!open && !readonly && isEnabled()) {
- open = true;
-
- if (getCurrentDate() != null) {
- calendar.setDate((Date) getCurrentDate().clone());
- } else if (getDefaultDate() != null) {
- calendar.setDate(getDefaultDate());
- } else {
- calendar.setDate(new Date());
- }
-
- // clear previous values
- popup.setWidth("");
- popup.setHeight("");
- popup.setPopupPositionAndShow(new PopupPositionCallback());
- } else {
- VConsole.error("Cannot reopen popup, it is already open!");
- }
- }
-
- /*
- * (non-Javadoc)
- *
- * @see
- * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event
- * .dom.client.ClickEvent)
- */
- @Override
- public void onClick(ClickEvent event) {
- if (event.getSource() == calendarToggle && isEnabled()) {
- if (open) {
- closeCalendarPanel();
- } else if (!preventOpenPopupCalendar) {
- openCalendarPanel();
- }
- preventOpenPopupCalendar = false;
- }
- }
-
- /*
- * (non-Javadoc)
- *
- * @see
- * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt
- * .event.logical.shared.CloseEvent)
- */
- @Override
- public void onClose(CloseEvent<PopupPanel> event) {
- if (event.getSource() == popup) {
- buildDate();
- if (!BrowserInfo.get().isTouchDevice() && textFieldEnabled) {
- /*
- * Move focus to textbox, unless on touch device (avoids opening
- * virtual keyboard) or if textField is disabled.
- */
- focus();
- }
-
- open = false;
-
- if (cursorOverCalendarToggleButton
- && !toggleButtonClosesWithGuarantee) {
- preventOpenPopupCalendar = true;
- }
-
- toggleButtonClosesWithGuarantee = false;
- }
- }
-
- /**
- * Sets focus to Calendar panel.
- *
- * @param focus
- */
- public void setFocus(boolean focus) {
- calendar.setFocus(focus);
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- updateTextFieldEnabled();
- calendarToggle.setEnabled(enabled);
- Roles.getButtonRole().setAriaDisabledState(calendarToggle.getElement(),
- !enabled);
- }
-
- /**
- * Sets the content of a special field for assistive devices, so that they
- * can recognize the change and inform the user (reading out in case of
- * screen reader).
- *
- * @param selectedDate
- * Date that is currently selected
- */
- public void setFocusedDate(Date selectedDate) {
- this.selectedDate.setText(DateTimeFormat.getFormat("dd, MMMM, yyyy")
- .format(selectedDate));
- }
-
- /**
- * For internal use only. May be removed or replaced in the future.
- *
- * @see com.vaadin.client.ui.VAbstractTextualDate#buildDate()
- */
- @Override
- public void buildDate() {
- // Save previous value
- String previousValue = getText();
- super.buildDate();
-
- // Restore previous value if the input could not be parsed
- if (!parsable) {
- setText(previousValue);
- }
- updateTextFieldEnabled();
- }
-
- /**
- * Update the text field contents from the date. See {@link #buildDate()}.
- *
- * @param forceValid
- * true to force the text field to be updated, false to only
- * update if the parsable flag is true.
- */
- protected void buildDate(boolean forceValid) {
- if (forceValid) {
- parsable = true;
- }
- buildDate();
- }
-
- /*
- * (non-Javadoc)
- *
- * @see com.vaadin.client.ui.VDateField#onBrowserEvent(com.google
- * .gwt.user.client.Event)
- */
- @Override
- public void onBrowserEvent(com.google.gwt.user.client.Event event) {
- super.onBrowserEvent(event);
- if (DOM.eventGetType(event) == Event.ONKEYDOWN
- && event.getKeyCode() == getOpenCalenderPanelKey()) {
- openCalendarPanel();
- event.preventDefault();
- }
- }
-
- /**
- * Get the key code that opens the calendar panel. By default it is the down
- * key but you can override this to be whatever you like
- *
- * @return
- */
- protected int getOpenCalenderPanelKey() {
- return KeyCodes.KEY_DOWN;
- }
-
- /**
- * Closes the open popup panel.
- */
- public void closeCalendarPanel() {
- if (open) {
- toggleButtonClosesWithGuarantee = true;
- popup.hide(true);
- }
- }
-
- @Override
- public com.google.gwt.user.client.Element getSubPartElement(
- String subPart) {
- if (subPart.equals(CALENDAR_TOGGLE_ID)) {
- return calendarToggle.getElement();
- }
-
- return super.getSubPartElement(subPart);
- }
-
- @Override
- public String getSubPartName(
- com.google.gwt.user.client.Element subElement) {
- if (calendarToggle.getElement().isOrHasChild(subElement)) {
- return CALENDAR_TOGGLE_ID;
- }
-
- return super.getSubPartName(subElement);
- }
-
- /**
- * Set a description that explains the usage of the Widget for users of
- * assistive devices.
- *
- * @param descriptionForAssistiveDevices
- * String with the description
- */
- public void setDescriptionForAssistiveDevices(
- String descriptionForAssistiveDevices) {
- descriptionForAssistiveDevicesElement
- .setInnerText(descriptionForAssistiveDevices);
- }
-
- /**
- * Get the description that explains the usage of the Widget for users of
- * assistive devices.
- *
- * @return String with the description
- */
- public String getDescriptionForAssistiveDevices() {
- return descriptionForAssistiveDevicesElement.getInnerText();
- }
-
- /**
- * Sets the start range for this component. The start range is inclusive,
- * and it depends on the current resolution, what is considered inside the
- * range.
- *
- * @param rangeStart
- * - the allowed range's start date
- */
- public void setRangeStart(Date rangeStart) {
- calendar.setRangeStart(rangeStart);
- }
-
- /**
- * Sets the end range for this component. The end range is inclusive, and it
- * depends on the current resolution, what is considered inside the range.
- *
- * @param rangeEnd
- * - the allowed range's end date
- */
- public void setRangeEnd(Date rangeEnd) {
- calendar.setRangeEnd(rangeEnd);
- }
-
- private class PopupPositionCallback implements PositionCallback {
-
- @Override
- public void setPosition(int offsetWidth, int offsetHeight) {
- final int width = offsetWidth;
- final int height = offsetHeight;
- final int browserWindowWidth = Window.getClientWidth()
- + Window.getScrollLeft();
- final int windowHeight = Window.getClientHeight()
- + Window.getScrollTop();
- int left = calendarToggle.getAbsoluteLeft();
-
- // Add a little extra space to the right to avoid
- // problems with IE7 scrollbars and to make it look
- // nicer.
- int extraSpace = 30;
-
- boolean overflow = left + width + extraSpace > browserWindowWidth;
- if (overflow) {
- // Part of the popup is outside the browser window
- // (to the right)
- left = browserWindowWidth - width - extraSpace;
- }
-
- int top = calendarToggle.getAbsoluteTop();
- int extraHeight = 2;
- boolean verticallyRepositioned = false;
- ComputedStyle style = new ComputedStyle(popup.getElement());
- int[] margins = style.getMargin();
- int desiredPopupBottom = top + height
- + calendarToggle.getOffsetHeight() + margins[0]
- + margins[2];
-
- if (desiredPopupBottom > windowHeight) {
- int updatedLeft = left;
- left = getLeftPosition(left, width, style, overflow);
-
- // if position has not been changed then it means there is no
- // space to make popup fully visible
- if (updatedLeft == left) {
- // let's try to show popup on the top of the field
- int updatedTop = top - extraHeight - height - margins[0]
- - margins[2];
- verticallyRepositioned = updatedTop >= 0;
- if (verticallyRepositioned) {
- top = updatedTop;
- }
- }
- // Part of the popup is outside the browser window
- // (below)
- if (!verticallyRepositioned) {
- verticallyRepositioned = true;
- top = windowHeight - height - extraSpace + extraHeight;
- }
- }
- if (verticallyRepositioned) {
- popup.setPopupPosition(left, top);
- } else {
- popup.setPopupPosition(left,
- top + calendarToggle.getOffsetHeight() + extraHeight);
- }
- doSetFocus();
- }
-
- private int getLeftPosition(int left, int width, ComputedStyle style,
- boolean overflow) {
- if (positionRightSide()) {
- // Show to the right of the popup button unless we
- // are in the lower right corner of the screen
- if (overflow) {
- return left;
- } else {
- return left + calendarToggle.getOffsetWidth();
- }
- } else {
- int[] margins = style.getMargin();
- int desiredLeftPosition = calendarToggle.getAbsoluteLeft()
- - width - margins[1] - margins[3];
- if (desiredLeftPosition >= 0) {
- return desiredLeftPosition;
- } else {
- return left;
- }
- }
- }
-
- private boolean positionRightSide() {
- int buttonRightSide = calendarToggle.getAbsoluteLeft()
- + calendarToggle.getOffsetWidth();
- int textRightSide = text.getAbsoluteLeft() + text.getOffsetWidth();
- return buttonRightSide >= textRightSide;
- }
-
- private void doSetFocus() {
- /*
- * We have to wait a while before focusing since the popup needs to
- * be opened before we can focus
- */
- Timer focusTimer = new Timer() {
- @Override
- public void run() {
- setFocus(true);
- }
- };
-
- focusTimer.schedule(100);
- }
- }
-
- }
|