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.

VAbstractTextualDate.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. /*
  2. * Copyright 2000-2018 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 java.util.logging.Level;
  19. import java.util.logging.Logger;
  20. import com.google.gwt.aria.client.Roles;
  21. import com.google.gwt.core.client.Scheduler;
  22. import com.google.gwt.dom.client.Element;
  23. import com.google.gwt.event.dom.client.ChangeEvent;
  24. import com.google.gwt.event.dom.client.ChangeHandler;
  25. import com.google.gwt.event.dom.client.DomEvent;
  26. import com.google.gwt.event.dom.client.KeyCodes;
  27. import com.google.gwt.event.dom.client.KeyDownEvent;
  28. import com.google.gwt.event.dom.client.KeyDownHandler;
  29. import com.google.gwt.i18n.client.DateTimeFormat;
  30. import com.google.gwt.i18n.client.TimeZone;
  31. import com.google.gwt.user.client.ui.TextBox;
  32. import com.vaadin.client.BrowserInfo;
  33. import com.vaadin.client.Focusable;
  34. import com.vaadin.client.LocaleNotLoadedException;
  35. import com.vaadin.client.LocaleService;
  36. import com.vaadin.client.WidgetUtil;
  37. import com.vaadin.client.ui.aria.AriaHelper;
  38. import com.vaadin.client.ui.aria.HandlesAriaCaption;
  39. import com.vaadin.client.ui.aria.HandlesAriaInvalid;
  40. import com.vaadin.client.ui.aria.HandlesAriaRequired;
  41. import com.vaadin.shared.EventId;
  42. /**
  43. * Abstract textual date field base implementation. Provides a text box as an
  44. * editor for a date. The class is parameterized by the date resolution
  45. * enumeration type.
  46. *
  47. * @author Vaadin Ltd
  48. *
  49. * @param <R>
  50. * the resolution type which this field is based on (day, month, ...)
  51. * @since 8.0
  52. */
  53. public abstract class VAbstractTextualDate<R extends Enum<R>>
  54. extends VDateField<R>
  55. implements ChangeHandler, Focusable, SubPartAware, HandlesAriaCaption,
  56. HandlesAriaInvalid, HandlesAriaRequired, KeyDownHandler {
  57. private static final String PARSE_ERROR_CLASSNAME = "-parseerror";
  58. private static final String ISO_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss";
  59. private static final String ISO_DATE_PATTERN = "yyyy-MM-dd";
  60. /** For internal use only. May be removed or replaced in the future. */
  61. public final TextBox text;
  62. /** For internal use only. May be removed or replaced in the future. */
  63. public boolean lenient;
  64. private static final String TEXTFIELD_ID = "field";
  65. /** For internal use only. May be removed or replaced in the future. */
  66. private String formatStr;
  67. /** For internal use only. May be removed or replaced in the future. */
  68. private TimeZone timeZone;
  69. /**
  70. * Specifies whether the group of components has focus or not.
  71. */
  72. private boolean groupFocus;
  73. public VAbstractTextualDate(R resoluton) {
  74. super(resoluton);
  75. text = new TextBox();
  76. text.addChangeHandler(this);
  77. text.addFocusHandler(event -> fireBlurFocusEvent(event, true));
  78. text.addBlurHandler(event -> fireBlurFocusEvent(event, false));
  79. if (BrowserInfo.get().isIE()) {
  80. addDomHandler(this, KeyDownEvent.getType());
  81. }
  82. // Stop the browser from showing its own suggestion popup.
  83. WidgetUtil.disableBrowserAutocomplete(text);
  84. add(text);
  85. publishJSHelpers(getElement());
  86. }
  87. /**
  88. * Updates style names for the widget (and its children).
  89. */
  90. protected void updateStyleNames() {
  91. if (text != null) {
  92. text.setStyleName(VTextField.CLASSNAME);
  93. text.addStyleName(getStylePrimaryName() + "-textfield");
  94. }
  95. }
  96. /**
  97. * Gets the date format string for the current locale.
  98. *
  99. * @return the format string
  100. */
  101. public String getFormatString() {
  102. if (formatStr == null) {
  103. setFormatString(createFormatString());
  104. }
  105. return formatStr;
  106. }
  107. /**
  108. * Create a format string suitable for the widget in its current state.
  109. *
  110. * @return a date format string to use when formatting and parsing the text
  111. * in the input field
  112. * @since 8.1
  113. */
  114. protected String createFormatString() {
  115. if (isYear(getCurrentResolution())) {
  116. return "yyyy"; // force full year
  117. }
  118. try {
  119. String frmString = LocaleService.getDateFormat(currentLocale);
  120. return cleanFormat(frmString);
  121. } catch (LocaleNotLoadedException e) {
  122. // TODO should die instead? Can the component survive
  123. // without format string?
  124. getLogger().log(Level.SEVERE,
  125. e.getMessage() == null ? "" : e.getMessage(), e);
  126. return null;
  127. }
  128. }
  129. /**
  130. * Sets the date format string to use for the text field.
  131. *
  132. * @param formatString
  133. * the format string to use, or {@code null} to force re-creating
  134. * the format string from the locale the next time it is needed
  135. * @since 8.1
  136. */
  137. public void setFormatString(String formatString) {
  138. this.formatStr = formatString;
  139. }
  140. @Override
  141. public void bindAriaCaption(
  142. com.google.gwt.user.client.Element captionElement) {
  143. AriaHelper.bindCaption(text, captionElement);
  144. }
  145. @Override
  146. public void setAriaRequired(boolean required) {
  147. AriaHelper.handleInputRequired(text, required);
  148. }
  149. @Override
  150. public void setAriaInvalid(boolean invalid) {
  151. AriaHelper.handleInputInvalid(text, invalid);
  152. }
  153. /**
  154. * Updates the text field according to the current date (provided by
  155. * {@link #getDate()}). Takes care of updating text, enabling and disabling
  156. * the field, setting/removing readonly status and updating readonly styles.
  157. * <p>
  158. * For internal use only. May be removed or replaced in the future.
  159. * <p>
  160. * TODO: Split part of this into a method that only updates the text as this
  161. * is what usually is needed except for updateFromUIDL.
  162. */
  163. public void buildDate() {
  164. removeStyleName(getStylePrimaryName() + PARSE_ERROR_CLASSNAME);
  165. // Create the initial text for the textfield
  166. String dateText;
  167. Date currentDate = getDate();
  168. // Always call this to ensure the format ends up in the element
  169. String formatString = getFormatString();
  170. if (currentDate != null) {
  171. dateText = getDateTimeService().formatDate(currentDate,
  172. formatString, timeZone);
  173. } else {
  174. dateText = "";
  175. }
  176. setText(dateText);
  177. text.setEnabled(enabled);
  178. text.setReadOnly(readonly);
  179. if (readonly) {
  180. text.addStyleName("v-readonly");
  181. Roles.getTextboxRole().setAriaReadonlyProperty(text.getElement(),
  182. true);
  183. } else {
  184. text.removeStyleName("v-readonly");
  185. Roles.getTextboxRole()
  186. .removeAriaReadonlyProperty(text.getElement());
  187. }
  188. }
  189. /**
  190. * Sets the time zone for the field.
  191. *
  192. * @param timeZone
  193. * the new time zone to use
  194. * @since 8.2
  195. */
  196. public void setTimeZone(TimeZone timeZone) {
  197. this.timeZone = timeZone;
  198. }
  199. @Override
  200. public void setEnabled(boolean enabled) {
  201. super.setEnabled(enabled);
  202. text.setEnabled(enabled);
  203. }
  204. @Override
  205. public void onChange(ChangeEvent event) {
  206. updateBufferedValues();
  207. sendBufferedValues();
  208. }
  209. @Override
  210. public void updateBufferedValues() {
  211. updateDate();
  212. bufferedDateString = text.getText();
  213. updateBufferedResolutions();
  214. }
  215. private void updateDate() {
  216. if (!text.getText().isEmpty()) {
  217. try {
  218. String enteredDate = text.getText();
  219. setDate(getDateTimeService().parseDate(enteredDate,
  220. getFormatString(), lenient));
  221. if (lenient) {
  222. // If date value was leniently parsed, normalize text
  223. // presentation.
  224. // FIXME: Add a description/example here of when this is
  225. // needed
  226. text.setValue(getDateTimeService().formatDate(getDate(),
  227. getFormatString(), timeZone), false);
  228. }
  229. // remove possibly added invalid value indication
  230. removeStyleName(getStylePrimaryName() + PARSE_ERROR_CLASSNAME);
  231. } catch (final Exception e) {
  232. getLogger().log(Level.INFO,
  233. e.getMessage() == null ? "" : e.getMessage(), e);
  234. addStyleName(getStylePrimaryName() + PARSE_ERROR_CLASSNAME);
  235. setDate(null);
  236. }
  237. } else {
  238. setDate(null);
  239. // remove possibly added invalid value indication
  240. removeStyleName(getStylePrimaryName() + PARSE_ERROR_CLASSNAME);
  241. }
  242. }
  243. /**
  244. * Updates the {@link VDateField#bufferedResolutions bufferedResolutions},
  245. * then {@link #sendBufferedValues() sends} the values to the server.
  246. *
  247. * @since 8.2
  248. * @deprecated Use {@link #updateBufferedResolutions()} and
  249. * {@link #sendBufferedValues()} instead.
  250. */
  251. @Deprecated
  252. protected final void updateAndSendBufferedValues() {
  253. updateBufferedResolutions();
  254. sendBufferedValues();
  255. }
  256. /**
  257. * Updates {@link VDateField#bufferedResolutions bufferedResolutions} before
  258. * sending a response to the server.
  259. * <p>
  260. * The method can be overridden by subclasses to provide a custom logic for
  261. * date variables to avoid overriding the {@link #onChange(ChangeEvent)}
  262. * method.
  263. *
  264. * <p>
  265. * Note that this method should not send the buffered values. For that, use
  266. * {@link #sendBufferedValues()}.
  267. *
  268. * @since 8.2
  269. */
  270. protected void updateBufferedResolutions() {
  271. Date currentDate = getDate();
  272. if (currentDate != null) {
  273. bufferedResolutions.put(
  274. getResolutions().filter(this::isYear).findFirst().get(),
  275. currentDate.getYear() + 1900);
  276. }
  277. }
  278. /**
  279. * Clean date format string to make it suitable for
  280. * {@link #getFormatString()}.
  281. *
  282. * @see #getFormatString()
  283. *
  284. * @param format
  285. * date format string
  286. * @return cleaned up string
  287. */
  288. protected String cleanFormat(String format) {
  289. // Remove unsupported patterns
  290. // TODO support for 'G', era designator (used at least in Japan)
  291. format = format.replaceAll("[GzZwWkK]", "");
  292. // Remove extra delimiters ('/' and '.')
  293. while (format.startsWith("/") || format.startsWith(".")
  294. || format.startsWith("-")) {
  295. format = format.substring(1);
  296. }
  297. while (format.endsWith("/") || format.endsWith(".")
  298. || format.endsWith("-")) {
  299. format = format.substring(0, format.length() - 1);
  300. }
  301. // Remove duplicate delimiters
  302. format = format.replaceAll("//", "/");
  303. format = format.replaceAll("\\.\\.", ".");
  304. format = format.replaceAll("--", "-");
  305. return format.trim();
  306. }
  307. @Override
  308. public void focus() {
  309. text.setFocus(true);
  310. }
  311. /**
  312. * Sets the placeholder for this textual date input.
  313. *
  314. * @param placeholder
  315. * the placeholder to set, or {@code null} to clear
  316. */
  317. public void setPlaceholder(String placeholder) {
  318. if (placeholder != null) {
  319. text.getElement().setAttribute("placeholder", placeholder);
  320. } else {
  321. text.getElement().removeAttribute("placeholder");
  322. }
  323. }
  324. /**
  325. * Gets the set placeholder this textual date input, or an empty string if
  326. * none is set.
  327. *
  328. * @return the placeholder or an empty string if none set
  329. */
  330. public String getPlaceHolder() {
  331. return text.getElement().getAttribute("placeholder");
  332. }
  333. protected String getText() {
  334. return text.getText();
  335. }
  336. protected void setText(String text) {
  337. this.text.setText(text);
  338. }
  339. @Override
  340. public com.google.gwt.user.client.Element getSubPartElement(
  341. String subPart) {
  342. if (subPart.equals(TEXTFIELD_ID)) {
  343. return text.getElement();
  344. }
  345. return null;
  346. }
  347. @Override
  348. public String getSubPartName(
  349. com.google.gwt.user.client.Element subElement) {
  350. if (text.getElement().isOrHasChild(subElement)) {
  351. return TEXTFIELD_ID;
  352. }
  353. return null;
  354. }
  355. @Override
  356. public void onKeyDown(KeyDownEvent event) {
  357. if (BrowserInfo.get().isIE()
  358. && event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
  359. // IE does not send change events when pressing enter in a text
  360. // input so we handle it using a key listener instead
  361. onChange(null);
  362. }
  363. }
  364. private void fireBlurFocusEvent(DomEvent<?> event, boolean focus) {
  365. String styleName = VTextField.CLASSNAME + "-"
  366. + VTextField.CLASSNAME_FOCUS;
  367. if (focus) {
  368. text.addStyleName(styleName);
  369. } else {
  370. text.removeStyleName(styleName);
  371. }
  372. Scheduler.get().scheduleDeferred(() -> checkGroupFocus(focus));
  373. // Needed for tooltip event handling
  374. fireEvent(event);
  375. }
  376. /**
  377. * Checks if the group focus has changed, and sends to the server if needed.
  378. *
  379. * @param textFocus
  380. * the focus of the {@link #text}
  381. * @since 8.3
  382. */
  383. protected void checkGroupFocus(boolean textFocus) {
  384. boolean newGroupFocus = textFocus | hasChildFocus();
  385. if (getClient() != null
  386. && connector.hasEventListener(
  387. textFocus ? EventId.FOCUS : EventId.BLUR)
  388. && groupFocus != newGroupFocus) {
  389. if (newGroupFocus) {
  390. rpc.focus();
  391. } else {
  392. rpc.blur();
  393. }
  394. sendBufferedValues();
  395. groupFocus = newGroupFocus;
  396. }
  397. }
  398. /**
  399. * Returns whether any of the child components has focus.
  400. *
  401. * @return {@code true} if any of the child component has focus,
  402. * {@code false} otherwise
  403. * @since 8.3
  404. */
  405. protected boolean hasChildFocus() {
  406. return false;
  407. }
  408. /**
  409. * Publish methods/properties on the element to be used from JavaScript.
  410. *
  411. * @since 8.1
  412. */
  413. private native void publishJSHelpers(Element root)
  414. /*-{
  415. var self = this;
  416. root.setISOValue = $entry(function (value) {
  417. self.@VAbstractTextualDate::setISODate(*)(value);
  418. });
  419. root.getISOValue = $entry(function () {
  420. return self.@VAbstractTextualDate::getISODate()();
  421. });
  422. }-*/;
  423. /**
  424. * Sets the value of the date field as a locale independent ISO date
  425. * (yyyy-MM-dd'T'HH:mm:ss or yyyy-MM-dd depending on whether this is a date
  426. * field or a date and time field).
  427. *
  428. * @param isoDate
  429. * the date to set in ISO8601 format, or null to clear the date
  430. * value
  431. * @since 8.1
  432. */
  433. public void setISODate(String isoDate) {
  434. Date date = null;
  435. if (isoDate != null) {
  436. date = getIsoFormatter().parse(isoDate);
  437. }
  438. setDate(date);
  439. updateBufferedResolutions();
  440. sendBufferedValues();
  441. }
  442. /**
  443. * Gets the value of the date field as a locale independent ISO date
  444. * (yyyy-MM-dd'T'HH:mm:ss or yyyy-MM-dd depending on whether this is a date
  445. * field or a date and time field).
  446. *
  447. * @return the current date in ISO8601 format, or null if no date is set
  448. *
  449. * @since 8.1
  450. */
  451. public String getISODate() {
  452. Date date = getDate();
  453. if (date == null) {
  454. return null;
  455. }
  456. return getIsoFormatter().format(date);
  457. }
  458. private DateTimeFormat getIsoFormatter() {
  459. if (supportsTime()) {
  460. return DateTimeFormat.getFormat(ISO_DATE_TIME_PATTERN);
  461. }
  462. return DateTimeFormat.getFormat(ISO_DATE_PATTERN);
  463. }
  464. private static Logger getLogger() {
  465. return Logger.getLogger(VAbstractTextualDate.class.getName());
  466. }
  467. }