You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

VPopupCalendar.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client.ui;
  17. import java.util.Date;
  18. import com.google.gwt.aria.client.Id;
  19. import com.google.gwt.aria.client.LiveValue;
  20. import com.google.gwt.aria.client.Roles;
  21. import com.google.gwt.core.client.GWT;
  22. import com.google.gwt.dom.client.Element;
  23. import com.google.gwt.event.dom.client.ClickEvent;
  24. import com.google.gwt.event.dom.client.ClickHandler;
  25. import com.google.gwt.event.dom.client.DomEvent;
  26. import com.google.gwt.event.dom.client.KeyCodes;
  27. import com.google.gwt.event.logical.shared.CloseEvent;
  28. import com.google.gwt.event.logical.shared.CloseHandler;
  29. import com.google.gwt.i18n.client.DateTimeFormat;
  30. import com.google.gwt.user.client.DOM;
  31. import com.google.gwt.user.client.Event;
  32. import com.google.gwt.user.client.Timer;
  33. import com.google.gwt.user.client.Window;
  34. import com.google.gwt.user.client.ui.Button;
  35. import com.google.gwt.user.client.ui.FlowPanel;
  36. import com.google.gwt.user.client.ui.Label;
  37. import com.google.gwt.user.client.ui.PopupPanel;
  38. import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
  39. import com.google.gwt.user.client.ui.RootPanel;
  40. import com.google.gwt.user.client.ui.Widget;
  41. import com.vaadin.client.BrowserInfo;
  42. import com.vaadin.client.VConsole;
  43. import com.vaadin.client.ui.VCalendarPanel.FocusOutListener;
  44. import com.vaadin.client.ui.VCalendarPanel.SubmitListener;
  45. import com.vaadin.client.ui.aria.AriaHelper;
  46. import com.vaadin.shared.ui.datefield.PopupDateFieldState;
  47. import com.vaadin.shared.ui.datefield.Resolution;
  48. /**
  49. * Represents a date selection component with a text field and a popup date
  50. * selector.
  51. *
  52. * <b>Note:</b> To change the keyboard assignments used in the popup dialog you
  53. * should extend <code>com.vaadin.client.ui.VCalendarPanel</code> and then pass
  54. * set it by calling the <code>setCalendarPanel(VCalendarPanel panel)</code>
  55. * method.
  56. *
  57. */
  58. public class VPopupCalendar extends VTextualDate implements Field,
  59. ClickHandler, CloseHandler<PopupPanel>, SubPartAware {
  60. /** For internal use only. May be removed or replaced in the future. */
  61. public final Button calendarToggle = new Button();
  62. /** For internal use only. May be removed or replaced in the future. */
  63. public VCalendarPanel calendar;
  64. /** For internal use only. May be removed or replaced in the future. */
  65. public final VOverlay popup;
  66. /** For internal use only. May be removed or replaced in the future. */
  67. public boolean parsable = true;
  68. private boolean open = false;
  69. private boolean textFieldEnabled = true;
  70. private String captionId;
  71. private Label selectedDate;
  72. private Element descriptionForAssisitveDevicesElement;
  73. public VPopupCalendar() {
  74. super();
  75. calendarToggle.setText("");
  76. calendarToggle.addClickHandler(this);
  77. // -2 instead of -1 to avoid FocusWidget.onAttach to reset it
  78. calendarToggle.getElement().setTabIndex(-2);
  79. Roles.getButtonRole().set(calendarToggle.getElement());
  80. Roles.getButtonRole().setAriaHiddenState(calendarToggle.getElement(),
  81. true);
  82. add(calendarToggle);
  83. // Description of the usage of the widget for assisitve device users
  84. descriptionForAssisitveDevicesElement = DOM.createDiv();
  85. descriptionForAssisitveDevicesElement
  86. .setInnerText(PopupDateFieldState.DESCRIPTION_FOR_ASSISTIVE_DEVICES);
  87. AriaHelper.ensureHasId(descriptionForAssisitveDevicesElement);
  88. Roles.getTextboxRole().setAriaDescribedbyProperty(text.getElement(),
  89. Id.of(descriptionForAssisitveDevicesElement));
  90. AriaHelper.setVisibleForAssistiveDevicesOnly(
  91. descriptionForAssisitveDevicesElement, true);
  92. calendar = GWT.create(VCalendarPanel.class);
  93. calendar.setParentField(this);
  94. calendar.setFocusOutListener(new FocusOutListener() {
  95. @Override
  96. public boolean onFocusOut(DomEvent<?> event) {
  97. event.preventDefault();
  98. closeCalendarPanel();
  99. return true;
  100. }
  101. });
  102. // FIXME: Problem is, that the element with the provided id does not
  103. // exist yet in html. This is the same problem as with the context menu.
  104. // Apply here the same fix (#11795)
  105. Roles.getTextboxRole().setAriaControlsProperty(text.getElement(),
  106. Id.of(calendar.getElement()));
  107. Roles.getButtonRole().setAriaControlsProperty(
  108. calendarToggle.getElement(), Id.of(calendar.getElement()));
  109. calendar.setSubmitListener(new SubmitListener() {
  110. @Override
  111. public void onSubmit() {
  112. // Update internal value and send valuechange event if immediate
  113. updateValue(calendar.getDate());
  114. // Update text field (a must when not immediate).
  115. buildDate(true);
  116. closeCalendarPanel();
  117. }
  118. @Override
  119. public void onCancel() {
  120. closeCalendarPanel();
  121. }
  122. });
  123. popup = new VOverlay(true, false, true);
  124. popup.setOwner(this);
  125. FlowPanel wrapper = new FlowPanel();
  126. selectedDate = new Label();
  127. selectedDate.setStyleName(getStylePrimaryName() + "-selecteddate");
  128. AriaHelper.setVisibleForAssistiveDevicesOnly(selectedDate.getElement(),
  129. true);
  130. Roles.getTextboxRole().setAriaLiveProperty(selectedDate.getElement(),
  131. LiveValue.ASSERTIVE);
  132. Roles.getTextboxRole().setAriaAtomicProperty(selectedDate.getElement(),
  133. true);
  134. wrapper.add(selectedDate);
  135. wrapper.add(calendar);
  136. popup.setWidget(wrapper);
  137. popup.addCloseHandler(this);
  138. DOM.setElementProperty(calendar.getElement(), "id",
  139. "PID_VAADIN_POPUPCAL");
  140. sinkEvents(Event.ONKEYDOWN);
  141. updateStyleNames();
  142. }
  143. @Override
  144. protected void onAttach() {
  145. super.onAttach();
  146. DOM.appendChild(RootPanel.get().getElement(),
  147. descriptionForAssisitveDevicesElement);
  148. }
  149. @Override
  150. protected void onDetach() {
  151. super.onDetach();
  152. descriptionForAssisitveDevicesElement.removeFromParent();
  153. }
  154. @SuppressWarnings("deprecation")
  155. public void updateValue(Date newDate) {
  156. Date currentDate = getCurrentDate();
  157. if (currentDate == null || newDate.getTime() != currentDate.getTime()) {
  158. setCurrentDate((Date) newDate.clone());
  159. getClient().updateVariable(getId(), "year",
  160. newDate.getYear() + 1900, false);
  161. if (getCurrentResolution().getCalendarField() > Resolution.YEAR
  162. .getCalendarField()) {
  163. getClient().updateVariable(getId(), "month",
  164. newDate.getMonth() + 1, false);
  165. if (getCurrentResolution().getCalendarField() > Resolution.MONTH
  166. .getCalendarField()) {
  167. getClient().updateVariable(getId(), "day",
  168. newDate.getDate(), false);
  169. if (getCurrentResolution().getCalendarField() > Resolution.DAY
  170. .getCalendarField()) {
  171. getClient().updateVariable(getId(), "hour",
  172. newDate.getHours(), false);
  173. if (getCurrentResolution().getCalendarField() > Resolution.HOUR
  174. .getCalendarField()) {
  175. getClient().updateVariable(getId(), "min",
  176. newDate.getMinutes(), false);
  177. if (getCurrentResolution().getCalendarField() > Resolution.MINUTE
  178. .getCalendarField()) {
  179. getClient().updateVariable(getId(), "sec",
  180. newDate.getSeconds(), false);
  181. }
  182. }
  183. }
  184. }
  185. }
  186. }
  187. }
  188. /**
  189. * Checks whether the text field is enabled.
  190. *
  191. * @see VPopupCalendar#setTextFieldEnabled(boolean)
  192. * @return The current state of the text field.
  193. */
  194. public boolean isTextFieldEnabled() {
  195. return textFieldEnabled;
  196. }
  197. /**
  198. * Sets the state of the text field of this component. By default the text
  199. * field is enabled. Disabling it causes only the button for date selection
  200. * to be active, thus preventing the user from entering invalid dates. See
  201. * {@link http://dev.vaadin.com/ticket/6790}.
  202. *
  203. * @param state
  204. */
  205. public void setTextFieldEnabled(boolean textFieldEnabled) {
  206. this.textFieldEnabled = textFieldEnabled;
  207. text.setEnabled(textFieldEnabled);
  208. if (textFieldEnabled) {
  209. calendarToggle.setTabIndex(-1);
  210. Roles.getButtonRole().setAriaHiddenState(
  211. calendarToggle.getElement(), true);
  212. } else {
  213. calendarToggle.setTabIndex(0);
  214. Roles.getButtonRole().setAriaHiddenState(
  215. calendarToggle.getElement(), false);
  216. }
  217. handleAriaAttributes();
  218. }
  219. @Override
  220. public void bindAriaCaption(
  221. com.google.gwt.user.client.Element captionElement) {
  222. if (captionElement == null) {
  223. captionId = null;
  224. } else {
  225. captionId = captionElement.getId();
  226. }
  227. if (isTextFieldEnabled()) {
  228. super.bindAriaCaption(captionElement);
  229. } else {
  230. AriaHelper.bindCaption(calendarToggle, captionElement);
  231. }
  232. handleAriaAttributes();
  233. }
  234. private void handleAriaAttributes() {
  235. Widget removeFromWidget;
  236. Widget setForWidget;
  237. if (isTextFieldEnabled()) {
  238. setForWidget = text;
  239. removeFromWidget = calendarToggle;
  240. } else {
  241. setForWidget = calendarToggle;
  242. removeFromWidget = text;
  243. }
  244. Roles.getFormRole().removeAriaLabelledbyProperty(
  245. removeFromWidget.getElement());
  246. if (captionId == null) {
  247. Roles.getFormRole().removeAriaLabelledbyProperty(
  248. setForWidget.getElement());
  249. } else {
  250. Roles.getFormRole().setAriaLabelledbyProperty(
  251. setForWidget.getElement(), Id.of(captionId));
  252. }
  253. }
  254. /*
  255. * (non-Javadoc)
  256. *
  257. * @see
  258. * com.google.gwt.user.client.ui.UIObject#setStyleName(java.lang.String)
  259. */
  260. @Override
  261. public void setStyleName(String style) {
  262. super.setStyleName(style);
  263. updateStyleNames();
  264. }
  265. @Override
  266. public void setStylePrimaryName(String style) {
  267. removeStyleName(getStylePrimaryName() + "-popupcalendar");
  268. super.setStylePrimaryName(style);
  269. updateStyleNames();
  270. }
  271. @Override
  272. protected void updateStyleNames() {
  273. super.updateStyleNames();
  274. if (getStylePrimaryName() != null && calendarToggle != null) {
  275. addStyleName(getStylePrimaryName() + "-popupcalendar");
  276. calendarToggle.setStyleName(getStylePrimaryName() + "-button");
  277. popup.setStyleName(getStylePrimaryName() + "-popup");
  278. calendar.setStyleName(getStylePrimaryName() + "-calendarpanel");
  279. }
  280. }
  281. /**
  282. * Opens the calendar panel popup
  283. */
  284. public void openCalendarPanel() {
  285. if (!open && !readonly && isEnabled()) {
  286. open = true;
  287. if (getCurrentDate() != null) {
  288. calendar.setDate((Date) getCurrentDate().clone());
  289. } else {
  290. calendar.setDate(new Date());
  291. }
  292. // clear previous values
  293. popup.setWidth("");
  294. popup.setHeight("");
  295. popup.setPopupPositionAndShow(new PositionCallback() {
  296. @Override
  297. public void setPosition(int offsetWidth, int offsetHeight) {
  298. final int w = offsetWidth;
  299. final int h = offsetHeight;
  300. final int browserWindowWidth = Window.getClientWidth()
  301. + Window.getScrollLeft();
  302. final int browserWindowHeight = Window.getClientHeight()
  303. + Window.getScrollTop();
  304. int t = calendarToggle.getAbsoluteTop();
  305. int l = calendarToggle.getAbsoluteLeft();
  306. // Add a little extra space to the right to avoid
  307. // problems with IE7 scrollbars and to make it look
  308. // nicer.
  309. int extraSpace = 30;
  310. boolean overflowRight = false;
  311. if (l + +w + extraSpace > browserWindowWidth) {
  312. overflowRight = true;
  313. // Part of the popup is outside the browser window
  314. // (to the right)
  315. l = browserWindowWidth - w - extraSpace;
  316. }
  317. if (t + h + calendarToggle.getOffsetHeight() + 30 > browserWindowHeight) {
  318. // Part of the popup is outside the browser window
  319. // (below)
  320. t = browserWindowHeight - h
  321. - calendarToggle.getOffsetHeight() - 30;
  322. if (!overflowRight) {
  323. // Show to the right of the popup button unless we
  324. // are in the lower right corner of the screen
  325. l += calendarToggle.getOffsetWidth();
  326. }
  327. }
  328. popup.setPopupPosition(l,
  329. t + calendarToggle.getOffsetHeight() + 2);
  330. /*
  331. * We have to wait a while before focusing since the popup
  332. * needs to be opened before we can focus
  333. */
  334. Timer focusTimer = new Timer() {
  335. @Override
  336. public void run() {
  337. setFocus(true);
  338. }
  339. };
  340. focusTimer.schedule(100);
  341. }
  342. });
  343. } else {
  344. VConsole.error("Cannot reopen popup, it is already open!");
  345. }
  346. }
  347. /*
  348. * (non-Javadoc)
  349. *
  350. * @see
  351. * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt.event
  352. * .dom.client.ClickEvent)
  353. */
  354. @Override
  355. public void onClick(ClickEvent event) {
  356. if (event.getSource() == calendarToggle && isEnabled()) {
  357. openCalendarPanel();
  358. }
  359. }
  360. /*
  361. * (non-Javadoc)
  362. *
  363. * @see
  364. * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google.gwt
  365. * .event.logical.shared.CloseEvent)
  366. */
  367. @Override
  368. public void onClose(CloseEvent<PopupPanel> event) {
  369. if (event.getSource() == popup) {
  370. buildDate();
  371. if (!BrowserInfo.get().isTouchDevice()) {
  372. /*
  373. * Move focus to textbox, unless on touch device (avoids opening
  374. * virtual keyboard).
  375. */
  376. focus();
  377. }
  378. // TODO resolve what the "Sigh." is all about and document it here
  379. // Sigh.
  380. Timer t = new Timer() {
  381. @Override
  382. public void run() {
  383. open = false;
  384. }
  385. };
  386. t.schedule(100);
  387. }
  388. }
  389. /**
  390. * Sets focus to Calendar panel.
  391. *
  392. * @param focus
  393. */
  394. public void setFocus(boolean focus) {
  395. calendar.setFocus(focus);
  396. }
  397. @Override
  398. public void setEnabled(boolean enabled) {
  399. super.setEnabled(enabled);
  400. calendarToggle.setEnabled(enabled);
  401. Roles.getButtonRole().setAriaDisabledState(calendarToggle.getElement(),
  402. !enabled);
  403. }
  404. /**
  405. * Sets the content of a special field for assistive devices, so that they
  406. * can recognize the change and inform the user (reading out in case of
  407. * screen reader)
  408. *
  409. * @param selectedDate
  410. * Date that is currently selected
  411. */
  412. public void setFocusedDate(Date selectedDate) {
  413. this.selectedDate.setText(DateTimeFormat.getFormat("dd, MMMM, yyyy")
  414. .format(selectedDate));
  415. }
  416. /**
  417. * For internal use only. May be removed or replaced in the future.
  418. *
  419. * @see com.vaadin.client.ui.VTextualDate#buildDate()
  420. */
  421. @Override
  422. public void buildDate() {
  423. // Save previous value
  424. String previousValue = getText();
  425. super.buildDate();
  426. // Restore previous value if the input could not be parsed
  427. if (!parsable) {
  428. setText(previousValue);
  429. }
  430. // superclass sets the text field independently when building date
  431. text.setEnabled(isEnabled() && isTextFieldEnabled());
  432. }
  433. /**
  434. * Update the text field contents from the date. See {@link #buildDate()}.
  435. *
  436. * @param forceValid
  437. * true to force the text field to be updated, false to only
  438. * update if the parsable flag is true.
  439. */
  440. protected void buildDate(boolean forceValid) {
  441. if (forceValid) {
  442. parsable = true;
  443. }
  444. buildDate();
  445. }
  446. /*
  447. * (non-Javadoc)
  448. *
  449. * @see com.vaadin.client.ui.VDateField#onBrowserEvent(com.google
  450. * .gwt.user.client.Event)
  451. */
  452. @Override
  453. public void onBrowserEvent(com.google.gwt.user.client.Event event) {
  454. super.onBrowserEvent(event);
  455. if (DOM.eventGetType(event) == Event.ONKEYDOWN
  456. && event.getKeyCode() == getOpenCalenderPanelKey()) {
  457. openCalendarPanel();
  458. event.preventDefault();
  459. }
  460. }
  461. /**
  462. * Get the key code that opens the calendar panel. By default it is the down
  463. * key but you can override this to be whatever you like
  464. *
  465. * @return
  466. */
  467. protected int getOpenCalenderPanelKey() {
  468. return KeyCodes.KEY_DOWN;
  469. }
  470. /**
  471. * Closes the open popup panel
  472. */
  473. public void closeCalendarPanel() {
  474. if (open) {
  475. popup.hide(true);
  476. }
  477. }
  478. private final String CALENDAR_TOGGLE_ID = "popupButton";
  479. @Override
  480. public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
  481. if (subPart.equals(CALENDAR_TOGGLE_ID)) {
  482. return calendarToggle.getElement();
  483. }
  484. return super.getSubPartElement(subPart);
  485. }
  486. @Override
  487. public String getSubPartName(com.google.gwt.user.client.Element subElement) {
  488. if (calendarToggle.getElement().isOrHasChild(subElement)) {
  489. return CALENDAR_TOGGLE_ID;
  490. }
  491. return super.getSubPartName(subElement);
  492. }
  493. /**
  494. * Set a description that explains the usage of the Widget for users of
  495. * assistive devices.
  496. *
  497. * @param descriptionForAssistiveDevices
  498. * String with the description
  499. */
  500. public void setDescriptionForAssistiveDevices(
  501. String descriptionForAssistiveDevices) {
  502. descriptionForAssisitveDevicesElement
  503. .setInnerText(descriptionForAssistiveDevices);
  504. }
  505. /**
  506. * Get the description that explains the usage of the Widget for users of
  507. * assistive devices.
  508. *
  509. * @return String with the description
  510. */
  511. public String getDescriptionForAssistiveDevices() {
  512. return descriptionForAssisitveDevicesElement.getInnerText();
  513. }
  514. /**
  515. * Sets the start range for this component. The start range is inclusive,
  516. * and it depends on the current resolution, what is considered inside the
  517. * range.
  518. *
  519. * @param startDate
  520. * - the allowed range's start date
  521. */
  522. public void setRangeStart(Date rangeStart) {
  523. calendar.setRangeStart(rangeStart);
  524. }
  525. /**
  526. * Sets the end range for this component. The end range is inclusive, and it
  527. * depends on the current resolution, what is considered inside the range.
  528. *
  529. * @param endDate
  530. * - the allowed range's end date
  531. */
  532. public void setRangeEnd(Date rangeEnd) {
  533. calendar.setRangeEnd(rangeEnd);
  534. }
  535. }