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.

VAbstractCalendarPanel.java 65KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022
  1. /*
  2. * Copyright 2000-2016 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.Iterator;
  19. import java.util.List;
  20. import java.util.function.Predicate;
  21. import java.util.stream.Collectors;
  22. import java.util.stream.Stream;
  23. import com.google.gwt.aria.client.Roles;
  24. import com.google.gwt.aria.client.SelectedValue;
  25. import com.google.gwt.dom.client.Element;
  26. import com.google.gwt.dom.client.NativeEvent;
  27. import com.google.gwt.event.dom.client.BlurEvent;
  28. import com.google.gwt.event.dom.client.BlurHandler;
  29. import com.google.gwt.event.dom.client.ClickEvent;
  30. import com.google.gwt.event.dom.client.ClickHandler;
  31. import com.google.gwt.event.dom.client.DomEvent;
  32. import com.google.gwt.event.dom.client.FocusEvent;
  33. import com.google.gwt.event.dom.client.FocusHandler;
  34. import com.google.gwt.event.dom.client.KeyCodes;
  35. import com.google.gwt.event.dom.client.KeyDownEvent;
  36. import com.google.gwt.event.dom.client.KeyDownHandler;
  37. import com.google.gwt.event.dom.client.KeyPressEvent;
  38. import com.google.gwt.event.dom.client.KeyPressHandler;
  39. import com.google.gwt.event.dom.client.MouseDownEvent;
  40. import com.google.gwt.event.dom.client.MouseDownHandler;
  41. import com.google.gwt.event.dom.client.MouseOutEvent;
  42. import com.google.gwt.event.dom.client.MouseOutHandler;
  43. import com.google.gwt.event.dom.client.MouseUpEvent;
  44. import com.google.gwt.event.dom.client.MouseUpHandler;
  45. import com.google.gwt.user.client.DOM;
  46. import com.google.gwt.user.client.Timer;
  47. import com.google.gwt.user.client.ui.Button;
  48. import com.google.gwt.user.client.ui.FlexTable;
  49. import com.google.gwt.user.client.ui.InlineHTML;
  50. import com.google.gwt.user.client.ui.Widget;
  51. import com.vaadin.client.BrowserInfo;
  52. import com.vaadin.client.DateTimeService;
  53. import com.vaadin.client.VConsole;
  54. import com.vaadin.client.WidgetUtil;
  55. import com.vaadin.shared.util.SharedUtil;
  56. /**
  57. * Abstract calendar panel to show and select a date using a resolution. The
  58. * class is parameterized by the date resolution enumeration type.
  59. *
  60. * @author Vaadin Ltd
  61. *
  62. * @param <R>
  63. * the resolution type which this field is based on (day, month, ...)
  64. */
  65. @SuppressWarnings("deprecation")
  66. public abstract class VAbstractCalendarPanel<R extends Enum<R>>
  67. extends FocusableFlexTable implements KeyDownHandler, KeyPressHandler,
  68. MouseOutHandler, MouseDownHandler, MouseUpHandler, BlurHandler,
  69. FocusHandler, SubPartAware {
  70. public interface SubmitListener {
  71. /**
  72. * Called when calendar user triggers a submitting operation in calendar
  73. * panel. Eg. clicking on day or hitting enter.
  74. */
  75. void onSubmit();
  76. /**
  77. * On eg. ESC key.
  78. */
  79. void onCancel();
  80. }
  81. /**
  82. * Blur listener that listens to blur event from the panel
  83. */
  84. public interface FocusOutListener {
  85. /**
  86. * @return true if the calendar panel is not used after focus moves out
  87. */
  88. boolean onFocusOut(DomEvent<?> event);
  89. }
  90. /**
  91. * FocusChangeListener is notified when the panel changes its _focused_
  92. * value.
  93. */
  94. public interface FocusChangeListener {
  95. void focusChanged(Date focusedDate);
  96. }
  97. /**
  98. * Represents a Date button in the calendar
  99. */
  100. private class VEventButton extends Button {
  101. public VEventButton() {
  102. addMouseDownHandler(VAbstractCalendarPanel.this);
  103. addMouseOutHandler(VAbstractCalendarPanel.this);
  104. addMouseUpHandler(VAbstractCalendarPanel.this);
  105. }
  106. }
  107. private static final String CN_FOCUSED = "focused";
  108. private static final String CN_TODAY = "today";
  109. private static final String CN_SELECTED = "selected";
  110. private static final String CN_OFFMONTH = "offmonth";
  111. private static final String CN_OUTSIDE_RANGE = "outside-range";
  112. /**
  113. * Represents a click handler for when a user selects a value by using the
  114. * mouse
  115. */
  116. private ClickHandler dayClickHandler = new ClickHandler() {
  117. /*
  118. * (non-Javadoc)
  119. *
  120. * @see
  121. * com.google.gwt.event.dom.client.ClickHandler#onClick(com.google.gwt
  122. * .event.dom.client.ClickEvent)
  123. */
  124. @Override
  125. public void onClick(ClickEvent event) {
  126. if (!isEnabled() || isReadonly()) {
  127. return;
  128. }
  129. Date newDate = ((Day) event.getSource()).getDate();
  130. if (!isDateInsideRange(newDate,
  131. getResolution(VAbstractCalendarPanel.this::isDay))) {
  132. return;
  133. }
  134. if (newDate.getMonth() != displayedMonth.getMonth()
  135. || newDate.getYear() != displayedMonth.getYear()) {
  136. // If an off-month date was clicked, we must change the
  137. // displayed month and re-render the calendar (#8931)
  138. displayedMonth.setMonth(newDate.getMonth());
  139. displayedMonth.setYear(newDate.getYear());
  140. renderCalendar();
  141. }
  142. focusDay(newDate);
  143. selectFocused();
  144. onSubmit();
  145. }
  146. };
  147. private VEventButton prevYear;
  148. private VEventButton nextYear;
  149. private VEventButton prevMonth;
  150. private VEventButton nextMonth;
  151. private FlexTable days = new FlexTable();
  152. private R resolution;
  153. private Timer mouseTimer;
  154. private Date value;
  155. private DateTimeService dateTimeService;
  156. private boolean showISOWeekNumbers;
  157. private FocusedDate displayedMonth;
  158. private FocusedDate focusedDate;
  159. private Day selectedDay;
  160. private Day focusedDay;
  161. private FocusOutListener focusOutListener;
  162. private SubmitListener submitListener;
  163. private FocusChangeListener focusChangeListener;
  164. private boolean hasFocus = false;
  165. private VDateField<R> parent;
  166. private boolean initialRenderDone = false;
  167. public VAbstractCalendarPanel() {
  168. getElement().setId(DOM.createUniqueId());
  169. setStyleName(VDateField.CLASSNAME + "-calendarpanel");
  170. Roles.getGridRole().set(getElement());
  171. /*
  172. * Firefox auto-repeat works correctly only if we use a key press
  173. * handler, other browsers handle it correctly when using a key down
  174. * handler
  175. */
  176. if (BrowserInfo.get().isGecko()) {
  177. addKeyPressHandler(this);
  178. } else {
  179. addKeyDownHandler(this);
  180. }
  181. addFocusHandler(this);
  182. addBlurHandler(this);
  183. }
  184. public void setParentField(VDateField<R> parent) {
  185. this.parent = parent;
  186. }
  187. /**
  188. * Sets the focus to given date in the current view. Used when moving in the
  189. * calendar with the keyboard.
  190. *
  191. * @param date
  192. * A Date representing the day of month to be focused. Must be
  193. * one of the days currently visible.
  194. */
  195. private void focusDay(Date date) {
  196. // Only used when calender body is present
  197. if (acceptDayFocus()) {
  198. if (focusedDay != null) {
  199. focusedDay.removeStyleDependentName(CN_FOCUSED);
  200. }
  201. if (date != null && focusedDate != null) {
  202. focusedDate.setTime(date.getTime());
  203. int rowCount = days.getRowCount();
  204. for (int i = 0; i < rowCount; i++) {
  205. int cellCount = days.getCellCount(i);
  206. for (int j = 0; j < cellCount; j++) {
  207. Widget widget = days.getWidget(i, j);
  208. if (widget != null
  209. && widget instanceof VAbstractCalendarPanel.Day) {
  210. Day curday = (Day) widget;
  211. if (curday.getDate().equals(date)) {
  212. curday.addStyleDependentName(CN_FOCUSED);
  213. focusedDay = curday;
  214. return;
  215. }
  216. }
  217. }
  218. }
  219. }
  220. }
  221. }
  222. /**
  223. * Returns {@code true} if current resolution assumes handling focus event
  224. * for day UI component.
  225. *
  226. * @return {@code true} if day focus events should be handled, {@code false}
  227. * otherwise
  228. */
  229. protected abstract boolean acceptDayFocus();
  230. /**
  231. * Returns {@code true} if the provided {@code resolution} represents a day.
  232. *
  233. * @param resolution
  234. * the given resolution
  235. * @return {@code true} if the {@code resolution} represents a day
  236. */
  237. protected abstract boolean isDay(R resolution);
  238. /**
  239. * Returns {@code true} if the provided {@code resolution} represents a
  240. * month.
  241. *
  242. * @param resolution
  243. * the given resolution
  244. * @return {@code true} if the {@code resolution} represents a month
  245. */
  246. protected abstract boolean isMonth(R resolution);
  247. /**
  248. * Returns {@code true} if the provided {@code resolution} represents an
  249. * year.
  250. *
  251. * @param resolution
  252. * the given resolution
  253. * @return {@code true} if the {@code resolution} represents a year
  254. */
  255. protected boolean isYear(R resolution) {
  256. return parent.isYear(resolution);
  257. }
  258. /**
  259. * Returns {@code true} if the {@code resolution} representation is strictly
  260. * below month (day, hour, etc..).
  261. *
  262. * @param resolution
  263. * the given resolution
  264. * @return whether the {@code resolution} is below the month resolution
  265. */
  266. protected abstract boolean isBelowMonth(R resolution);
  267. /**
  268. * Returns all available resolutions for the widget.
  269. *
  270. * @return all available resolutions
  271. */
  272. protected Stream<R> getResolutions() {
  273. return parent.getResolutions();
  274. }
  275. /**
  276. * Finds the resolution by the {@code filter}.
  277. *
  278. * @param filter
  279. * predicate to filter resolutions
  280. * @return the resolution accepted by the {@code filter}
  281. */
  282. protected R getResolution(Predicate<R> filter) {
  283. List<R> resolutions = getResolutions().filter(filter)
  284. .collect(Collectors.toList());
  285. assert resolutions
  286. .size() == 1 : "The result of filtering by the predicate "
  287. + "contains unexpected number of resolution items :"
  288. + resolutions.size();
  289. return resolutions.get(0);
  290. }
  291. /**
  292. * Sets the selection highlight to a given day in the current view
  293. *
  294. * @param date
  295. * A Date representing the day of month to be selected. Must be
  296. * one of the days currently visible.
  297. *
  298. */
  299. private void selectDate(Date date) {
  300. if (selectedDay != null) {
  301. selectedDay.removeStyleDependentName(CN_SELECTED);
  302. Roles.getGridcellRole()
  303. .removeAriaSelectedState(selectedDay.getElement());
  304. }
  305. int rowCount = days.getRowCount();
  306. for (int i = 0; i < rowCount; i++) {
  307. int cellCount = days.getCellCount(i);
  308. for (int j = 0; j < cellCount; j++) {
  309. Widget widget = days.getWidget(i, j);
  310. if (widget != null
  311. && widget instanceof VAbstractCalendarPanel.Day) {
  312. Day curday = (Day) widget;
  313. if (curday.getDate().equals(date)) {
  314. curday.addStyleDependentName(CN_SELECTED);
  315. selectedDay = curday;
  316. Roles.getGridcellRole().setAriaSelectedState(
  317. selectedDay.getElement(), SelectedValue.TRUE);
  318. return;
  319. }
  320. }
  321. }
  322. }
  323. }
  324. /**
  325. * Updates year, month, day from focusedDate to value
  326. */
  327. private void selectFocused() {
  328. if (focusedDate != null
  329. && isDateInsideRange(focusedDate, getResolution())) {
  330. if (value == null) {
  331. // No previously selected value (set to null on server side).
  332. // Create a new date using current date and time
  333. value = new Date();
  334. }
  335. /*
  336. * #5594 set Date (day) to 1 in order to prevent any kind of
  337. * wrapping of months when later setting the month. (e.g. 31 ->
  338. * month with 30 days -> wraps to the 1st of the following month,
  339. * e.g. 31st of May -> 31st of April = 1st of May)
  340. */
  341. value.setDate(1);
  342. if (value.getYear() != focusedDate.getYear()) {
  343. value.setYear(focusedDate.getYear());
  344. }
  345. if (value.getMonth() != focusedDate.getMonth()) {
  346. value.setMonth(focusedDate.getMonth());
  347. }
  348. if (value.getDate() != focusedDate.getDate()) {
  349. }
  350. // We always need to set the date, even if it hasn't changed, since
  351. // it was forced to 1 above.
  352. value.setDate(focusedDate.getDate());
  353. selectDate(focusedDate);
  354. } else {
  355. VConsole.log("Trying to select a the focused date which is NULL!");
  356. }
  357. }
  358. protected boolean onValueChange() {
  359. return false;
  360. }
  361. public R getResolution() {
  362. return resolution;
  363. }
  364. public void setResolution(R resolution) {
  365. this.resolution = resolution;
  366. }
  367. private boolean isReadonly() {
  368. return parent.isReadonly();
  369. }
  370. private boolean isEnabled() {
  371. return parent.isEnabled();
  372. }
  373. @Override
  374. public void setStyleName(String style) {
  375. super.setStyleName(style);
  376. if (initialRenderDone) {
  377. // Dynamic updates to the stylename needs to render the calendar to
  378. // update the inner element stylenames
  379. renderCalendar();
  380. }
  381. }
  382. @Override
  383. public void setStylePrimaryName(String style) {
  384. super.setStylePrimaryName(style);
  385. if (initialRenderDone) {
  386. // Dynamic updates to the stylename needs to render the calendar to
  387. // update the inner element stylenames
  388. renderCalendar();
  389. }
  390. }
  391. private void clearCalendarBody(boolean remove) {
  392. if (!remove) {
  393. // Leave the cells in place but clear their contents
  394. // This has the side effect of ensuring that the calendar always
  395. // contain 7 rows.
  396. for (int row = 1; row < 7; row++) {
  397. for (int col = 0; col < 8; col++) {
  398. days.setHTML(row, col, "&nbsp;");
  399. }
  400. }
  401. } else if (getRowCount() > 1) {
  402. removeRow(1);
  403. days.clear();
  404. }
  405. }
  406. /**
  407. * Builds the top buttons and current month and year header.
  408. *
  409. * @param needsMonth
  410. * Should the month buttons be visible?
  411. */
  412. private void buildCalendarHeader(boolean needsMonth) {
  413. getRowFormatter().addStyleName(0,
  414. parent.getStylePrimaryName() + "-calendarpanel-header");
  415. if (prevMonth == null && needsMonth) {
  416. prevMonth = new VEventButton();
  417. prevMonth.setHTML("&lsaquo;");
  418. prevMonth.setStyleName("v-button-prevmonth");
  419. prevMonth.setTabIndex(-1);
  420. nextMonth = new VEventButton();
  421. nextMonth.setHTML("&rsaquo;");
  422. nextMonth.setStyleName("v-button-nextmonth");
  423. nextMonth.setTabIndex(-1);
  424. setWidget(0, 3, nextMonth);
  425. setWidget(0, 1, prevMonth);
  426. } else if (prevMonth != null && !needsMonth) {
  427. // Remove month traverse buttons
  428. remove(prevMonth);
  429. remove(nextMonth);
  430. prevMonth = null;
  431. nextMonth = null;
  432. }
  433. if (prevYear == null) {
  434. prevYear = new VEventButton();
  435. prevYear.setHTML("&laquo;");
  436. prevYear.setStyleName("v-button-prevyear");
  437. prevYear.setTabIndex(-1);
  438. nextYear = new VEventButton();
  439. nextYear.setHTML("&raquo;");
  440. nextYear.setStyleName("v-button-nextyear");
  441. nextYear.setTabIndex(-1);
  442. setWidget(0, 0, prevYear);
  443. setWidget(0, 4, nextYear);
  444. }
  445. updateControlButtonRangeStyles(needsMonth);
  446. final String monthName = needsMonth
  447. ? getDateTimeService().getMonth(displayedMonth.getMonth()) : "";
  448. final int year = displayedMonth.getYear() + 1900;
  449. getFlexCellFormatter().setStyleName(0, 2,
  450. parent.getStylePrimaryName() + "-calendarpanel-month");
  451. getFlexCellFormatter().setStyleName(0, 0,
  452. parent.getStylePrimaryName() + "-calendarpanel-prevyear");
  453. getFlexCellFormatter().setStyleName(0, 4,
  454. parent.getStylePrimaryName() + "-calendarpanel-nextyear");
  455. getFlexCellFormatter().setStyleName(0, 3,
  456. parent.getStylePrimaryName() + "-calendarpanel-nextmonth");
  457. getFlexCellFormatter().setStyleName(0, 1,
  458. parent.getStylePrimaryName() + "-calendarpanel-prevmonth");
  459. setHTML(0, 2,
  460. "<span class=\"" + parent.getStylePrimaryName()
  461. + "-calendarpanel-month\">" + monthName + " " + year
  462. + "</span>");
  463. }
  464. private void updateControlButtonRangeStyles(boolean needsMonth) {
  465. if (focusedDate == null) {
  466. return;
  467. }
  468. if (needsMonth) {
  469. Date prevMonthDate = (Date) focusedDate.clone();
  470. removeOneMonth(prevMonthDate);
  471. R month = getResolution(VAbstractCalendarPanel.this::isMonth);
  472. if (!isDateInsideRange(prevMonthDate, month)) {
  473. prevMonth.addStyleName(CN_OUTSIDE_RANGE);
  474. } else {
  475. prevMonth.removeStyleName(CN_OUTSIDE_RANGE);
  476. }
  477. Date nextMonthDate = (Date) focusedDate.clone();
  478. addOneMonth(nextMonthDate);
  479. if (!isDateInsideRange(nextMonthDate, month)) {
  480. nextMonth.addStyleName(CN_OUTSIDE_RANGE);
  481. } else {
  482. nextMonth.removeStyleName(CN_OUTSIDE_RANGE);
  483. }
  484. }
  485. Date prevYearDate = (Date) focusedDate.clone();
  486. prevYearDate.setYear(prevYearDate.getYear() - 1);
  487. R year = getResolution(VAbstractCalendarPanel.this::isYear);
  488. if (!isDateInsideRange(prevYearDate, year)) {
  489. prevYear.addStyleName(CN_OUTSIDE_RANGE);
  490. } else {
  491. prevYear.removeStyleName(CN_OUTSIDE_RANGE);
  492. }
  493. Date nextYearDate = (Date) focusedDate.clone();
  494. nextYearDate.setYear(nextYearDate.getYear() + 1);
  495. if (!isDateInsideRange(nextYearDate, year)) {
  496. nextYear.addStyleName(CN_OUTSIDE_RANGE);
  497. } else {
  498. nextYear.removeStyleName(CN_OUTSIDE_RANGE);
  499. }
  500. }
  501. private DateTimeService getDateTimeService() {
  502. return dateTimeService;
  503. }
  504. public void setDateTimeService(DateTimeService dateTimeService) {
  505. this.dateTimeService = dateTimeService;
  506. }
  507. /**
  508. * Returns whether ISO 8601 week numbers should be shown in the value
  509. * selector or not. ISO 8601 defines that a week always starts with a Monday
  510. * so the week numbers are only shown if this is the case.
  511. *
  512. * @return true if week number should be shown, false otherwise
  513. */
  514. public boolean isShowISOWeekNumbers() {
  515. return showISOWeekNumbers;
  516. }
  517. public void setShowISOWeekNumbers(boolean showISOWeekNumbers) {
  518. this.showISOWeekNumbers = showISOWeekNumbers;
  519. }
  520. /**
  521. * Checks inclusively whether a date is inside a range of dates or not.
  522. *
  523. * @param date
  524. * @return
  525. */
  526. private boolean isDateInsideRange(Date date, R minResolution) {
  527. assert (date != null);
  528. return isAcceptedByRangeEnd(date, minResolution)
  529. && isAcceptedByRangeStart(date, minResolution);
  530. }
  531. /**
  532. * Accepts dates greater than or equal to rangeStart, depending on the
  533. * resolution. If the resolution is set to DAY, the range will compare on a
  534. * day-basis. If the resolution is set to YEAR, only years are compared. So
  535. * even if the range is set to one millisecond in next year, also next year
  536. * will be included.
  537. *
  538. * @param date
  539. * @param minResolution
  540. * @return
  541. */
  542. private boolean isAcceptedByRangeStart(Date date, R minResolution) {
  543. assert (date != null);
  544. // rangeStart == null means that we accept all values below rangeEnd
  545. if (rangeStart == null) {
  546. return true;
  547. }
  548. Date valueDuplicate = (Date) date.clone();
  549. Date rangeStartDuplicate = (Date) rangeStart.clone();
  550. if (isYear(minResolution)) {
  551. return valueDuplicate.getYear() >= rangeStartDuplicate.getYear();
  552. }
  553. if (isMonth(minResolution)) {
  554. valueDuplicate = clearDateBelowMonth(valueDuplicate);
  555. rangeStartDuplicate = clearDateBelowMonth(rangeStartDuplicate);
  556. } else {
  557. valueDuplicate = clearDateBelowDay(valueDuplicate);
  558. rangeStartDuplicate = clearDateBelowDay(rangeStartDuplicate);
  559. }
  560. return !rangeStartDuplicate.after(valueDuplicate);
  561. }
  562. /**
  563. * Accepts dates earlier than or equal to rangeStart, depending on the
  564. * resolution. If the resolution is set to DAY, the range will compare on a
  565. * day-basis. If the resolution is set to YEAR, only years are compared. So
  566. * even if the range is set to one millisecond in next year, also next year
  567. * will be included.
  568. *
  569. * @param date
  570. * @param minResolution
  571. * @return
  572. */
  573. private boolean isAcceptedByRangeEnd(Date date, R minResolution) {
  574. assert (date != null);
  575. // rangeEnd == null means that we accept all values above rangeStart
  576. if (rangeEnd == null) {
  577. return true;
  578. }
  579. Date valueDuplicate = (Date) date.clone();
  580. Date rangeEndDuplicate = (Date) rangeEnd.clone();
  581. if (isYear(minResolution)) {
  582. return valueDuplicate.getYear() <= rangeEndDuplicate.getYear();
  583. }
  584. if (isMonth(minResolution)) {
  585. valueDuplicate = clearDateBelowMonth(valueDuplicate);
  586. rangeEndDuplicate = clearDateBelowMonth(rangeEndDuplicate);
  587. } else {
  588. valueDuplicate = clearDateBelowDay(valueDuplicate);
  589. rangeEndDuplicate = clearDateBelowDay(rangeEndDuplicate);
  590. }
  591. return !rangeEndDuplicate.before(valueDuplicate);
  592. }
  593. private static Date clearDateBelowMonth(Date date) {
  594. date.setDate(1);
  595. return clearDateBelowDay(date);
  596. }
  597. private static Date clearDateBelowDay(Date date) {
  598. date.setHours(0);
  599. date.setMinutes(0);
  600. date.setSeconds(0);
  601. // Clearing milliseconds
  602. long time = date.getTime() / 1000;
  603. date = new Date(time * 1000);
  604. return date;
  605. }
  606. /**
  607. * Builds the day and time selectors of the calendar.
  608. */
  609. private void buildCalendarBody() {
  610. final int weekColumn = 0;
  611. final int firstWeekdayColumn = 1;
  612. final int headerRow = 0;
  613. setWidget(1, 0, days);
  614. setCellPadding(0);
  615. setCellSpacing(0);
  616. getFlexCellFormatter().setColSpan(1, 0, 5);
  617. getFlexCellFormatter().setStyleName(1, 0,
  618. parent.getStylePrimaryName() + "-calendarpanel-body");
  619. days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
  620. "v-week");
  621. days.setHTML(headerRow, weekColumn, "<strong></strong>");
  622. // Hide the week column if week numbers are not to be displayed.
  623. days.getFlexCellFormatter().setVisible(headerRow, weekColumn,
  624. isShowISOWeekNumbers());
  625. days.getRowFormatter().setStyleName(headerRow,
  626. parent.getStylePrimaryName() + "-calendarpanel-weekdays");
  627. if (isShowISOWeekNumbers()) {
  628. days.getFlexCellFormatter().setStyleName(headerRow, weekColumn,
  629. "v-first");
  630. days.getFlexCellFormatter().setStyleName(headerRow,
  631. firstWeekdayColumn, "");
  632. days.getRowFormatter().addStyleName(headerRow,
  633. parent.getStylePrimaryName()
  634. + "-calendarpanel-weeknumbers");
  635. } else {
  636. days.getFlexCellFormatter().setStyleName(headerRow, weekColumn, "");
  637. days.getFlexCellFormatter().setStyleName(headerRow,
  638. firstWeekdayColumn, "v-first");
  639. }
  640. days.getFlexCellFormatter().setStyleName(headerRow,
  641. firstWeekdayColumn + 6, "v-last");
  642. // Print weekday names
  643. final int firstDay = getDateTimeService().getFirstDayOfWeek();
  644. for (int i = 0; i < 7; i++) {
  645. int day = i + firstDay;
  646. if (day > 6) {
  647. day = 0;
  648. }
  649. if (isBelowMonth(getResolution())) {
  650. days.setHTML(headerRow, firstWeekdayColumn + i, "<strong>"
  651. + getDateTimeService().getShortDay(day) + "</strong>");
  652. } else {
  653. days.setHTML(headerRow, firstWeekdayColumn + i, "");
  654. }
  655. Roles.getColumnheaderRole().set(days.getCellFormatter()
  656. .getElement(headerRow, firstWeekdayColumn + i));
  657. }
  658. // Zero out hours, minutes, seconds, and milliseconds to compare dates
  659. // without time part
  660. final Date tmp = new Date();
  661. final Date today = new Date(tmp.getYear(), tmp.getMonth(),
  662. tmp.getDate());
  663. final Date selectedDate = value == null ? null
  664. : new Date(value.getYear(), value.getMonth(), value.getDate());
  665. final int startWeekDay = getDateTimeService()
  666. .getStartWeekDay(displayedMonth);
  667. final Date curr = (Date) displayedMonth.clone();
  668. // Start from the first day of the week that at least partially belongs
  669. // to the current month
  670. curr.setDate(1 - startWeekDay);
  671. // No month has more than 6 weeks so 6 is a safe maximum for rows.
  672. for (int weekOfMonth = 1; weekOfMonth < 7; weekOfMonth++) {
  673. for (int dayOfWeek = 0; dayOfWeek < 7; dayOfWeek++) {
  674. // Actually write the day of month
  675. Date dayDate = (Date) curr.clone();
  676. Day day = new Day(dayDate);
  677. day.setStyleName(
  678. parent.getStylePrimaryName() + "-calendarpanel-day");
  679. if (!isDateInsideRange(dayDate, getResolution(this::isDay))) {
  680. day.addStyleDependentName(CN_OUTSIDE_RANGE);
  681. }
  682. if (curr.equals(selectedDate)) {
  683. day.addStyleDependentName(CN_SELECTED);
  684. Roles.getGridcellRole().setAriaSelectedState(
  685. day.getElement(), SelectedValue.TRUE);
  686. selectedDay = day;
  687. }
  688. if (curr.equals(today)) {
  689. day.addStyleDependentName(CN_TODAY);
  690. }
  691. if (curr.equals(focusedDate)) {
  692. focusedDay = day;
  693. if (hasFocus) {
  694. day.addStyleDependentName(CN_FOCUSED);
  695. }
  696. }
  697. if (curr.getMonth() != displayedMonth.getMonth()) {
  698. day.addStyleDependentName(CN_OFFMONTH);
  699. }
  700. days.setWidget(weekOfMonth, firstWeekdayColumn + dayOfWeek,
  701. day);
  702. Roles.getGridcellRole().set(days.getCellFormatter().getElement(
  703. weekOfMonth, firstWeekdayColumn + dayOfWeek));
  704. // ISO week numbers if requested
  705. days.getCellFormatter().setVisible(weekOfMonth, weekColumn,
  706. isShowISOWeekNumbers());
  707. if (isShowISOWeekNumbers()) {
  708. final String baseCssClass = parent.getStylePrimaryName()
  709. + "-calendarpanel-weeknumber";
  710. String weekCssClass = baseCssClass;
  711. int weekNumber = DateTimeService.getISOWeekNumber(curr);
  712. days.setHTML(weekOfMonth, 0, "<span class=\"" + weekCssClass
  713. + "\"" + ">" + weekNumber + "</span>");
  714. }
  715. curr.setDate(curr.getDate() + 1);
  716. }
  717. }
  718. }
  719. /**
  720. * Updates the calendar and text field with the selected dates.
  721. */
  722. public void renderCalendar() {
  723. renderCalendar(true);
  724. }
  725. /**
  726. * For internal use only. May be removed or replaced in the future.
  727. *
  728. * Updates the calendar and text field with the selected dates.
  729. *
  730. * @param updateDate
  731. * The value false prevents setting the selected date of the
  732. * calendar based on focusedDate. That can be used when only the
  733. * resolution of the calendar is changed and no date has been
  734. * selected.
  735. */
  736. public void renderCalendar(boolean updateDate) {
  737. doRenderCalendar(updateDate);
  738. initialRenderDone = true;
  739. }
  740. /**
  741. * Performs the rendering required by the {@link #renderCalendar(boolean)}.
  742. * Subclasses may override this method to provide a custom implementation
  743. * avoiding {@link #renderCalendar(boolean)} override. The latter method
  744. * contains a common logic which should not be overriden.
  745. *
  746. * @param updateDate
  747. * The value false prevents setting the selected date of the
  748. * calendar based on focusedDate. That can be used when only the
  749. * resolution of the calendar is changed and no date has been
  750. * selected.
  751. */
  752. protected void doRenderCalendar(boolean updateDate) {
  753. super.setStylePrimaryName(
  754. parent.getStylePrimaryName() + "-calendarpanel");
  755. if (focusedDate == null) {
  756. Date now = new Date();
  757. // focusedDate must have zero hours, mins, secs, millisecs
  758. focusedDate = new FocusedDate(now.getYear(), now.getMonth(),
  759. now.getDate());
  760. displayedMonth = new FocusedDate(now.getYear(), now.getMonth(), 1);
  761. }
  762. if (updateDate && !isDay(getResolution())
  763. && focusChangeListener != null) {
  764. focusChangeListener.focusChanged(new Date(focusedDate.getTime()));
  765. }
  766. final boolean needsMonth = !isYear(getResolution());
  767. boolean needsBody = isDay(getResolution());
  768. buildCalendarHeader(needsMonth);
  769. clearCalendarBody(!needsBody);
  770. if (needsBody) {
  771. buildCalendarBody();
  772. }
  773. }
  774. /**
  775. * Moves the focus forward the given number of days.
  776. */
  777. private void focusNextDay(int days) {
  778. if (focusedDate == null) {
  779. return;
  780. }
  781. Date focusCopy = ((Date) focusedDate.clone());
  782. focusCopy.setDate(focusedDate.getDate() + days);
  783. if (!isDateInsideRange(focusCopy, getResolution())) {
  784. // If not inside allowed range, then do not move anything
  785. return;
  786. }
  787. int oldMonth = focusedDate.getMonth();
  788. int oldYear = focusedDate.getYear();
  789. focusedDate.setDate(focusedDate.getDate() + days);
  790. if (focusedDate.getMonth() == oldMonth
  791. && focusedDate.getYear() == oldYear) {
  792. // Month did not change, only move the selection
  793. focusDay(focusedDate);
  794. } else {
  795. // If the month changed we need to re-render the calendar
  796. displayedMonth.setMonth(focusedDate.getMonth());
  797. displayedMonth.setYear(focusedDate.getYear());
  798. renderCalendar();
  799. }
  800. }
  801. /**
  802. * Moves the focus backward the given number of days.
  803. */
  804. private void focusPreviousDay(int days) {
  805. focusNextDay(-days);
  806. }
  807. /**
  808. * Selects the next month
  809. */
  810. private void focusNextMonth() {
  811. if (focusedDate == null) {
  812. return;
  813. }
  814. // Trying to request next month
  815. Date requestedNextMonthDate = (Date) focusedDate.clone();
  816. addOneMonth(requestedNextMonthDate);
  817. if (!isDateInsideRange(requestedNextMonthDate,
  818. getResolution(this::isMonth))) {
  819. return;
  820. }
  821. // Now also checking whether the day is inside the range or not. If not
  822. // inside,
  823. // correct it
  824. if (!isDateInsideRange(requestedNextMonthDate,
  825. getResolution(this::isDay))) {
  826. requestedNextMonthDate = adjustDateToFitInsideRange(
  827. requestedNextMonthDate);
  828. }
  829. focusedDate.setTime(requestedNextMonthDate.getTime());
  830. displayedMonth.setMonth(displayedMonth.getMonth() + 1);
  831. renderCalendar();
  832. }
  833. private static void addOneMonth(Date date) {
  834. int currentMonth = date.getMonth();
  835. int requestedMonth = (currentMonth + 1) % 12;
  836. date.setMonth(date.getMonth() + 1);
  837. /*
  838. * If the selected value was e.g. 31.3 the new value would be 31.4 but
  839. * this value is invalid so the new value will be 1.5. This is taken
  840. * care of by decreasing the value until we have the correct month.
  841. */
  842. while (date.getMonth() != requestedMonth) {
  843. date.setDate(date.getDate() - 1);
  844. }
  845. }
  846. private static void removeOneMonth(Date date) {
  847. int currentMonth = date.getMonth();
  848. date.setMonth(date.getMonth() - 1);
  849. /*
  850. * If the selected value was e.g. 31.12 the new value would be 31.11 but
  851. * this value is invalid so the new value will be 1.12. This is taken
  852. * care of by decreasing the value until we have the correct month.
  853. */
  854. while (date.getMonth() == currentMonth) {
  855. date.setDate(date.getDate() - 1);
  856. }
  857. }
  858. /**
  859. * Selects the previous month
  860. */
  861. private void focusPreviousMonth() {
  862. if (focusedDate == null) {
  863. return;
  864. }
  865. Date requestedPreviousMonthDate = (Date) focusedDate.clone();
  866. removeOneMonth(requestedPreviousMonthDate);
  867. if (!isDateInsideRange(requestedPreviousMonthDate,
  868. getResolution(this::isMonth))) {
  869. return;
  870. }
  871. if (!isDateInsideRange(requestedPreviousMonthDate,
  872. getResolution(this::isDay))) {
  873. requestedPreviousMonthDate = adjustDateToFitInsideRange(
  874. requestedPreviousMonthDate);
  875. }
  876. focusedDate.setTime(requestedPreviousMonthDate.getTime());
  877. displayedMonth.setMonth(displayedMonth.getMonth() - 1);
  878. renderCalendar();
  879. }
  880. /**
  881. * Selects the previous year
  882. */
  883. private void focusPreviousYear(int years) {
  884. if (focusedDate == null) {
  885. return;
  886. }
  887. Date previousYearDate = (Date) focusedDate.clone();
  888. previousYearDate.setYear(previousYearDate.getYear() - years);
  889. // Do not focus if not inside range
  890. if (!isDateInsideRange(previousYearDate, getResolution(this::isYear))) {
  891. return;
  892. }
  893. // If we remove one year, but have to roll back a bit, fit it
  894. // into the calendar. Also the months have to be changed
  895. if (!isDateInsideRange(previousYearDate, getResolution(this::isDay))) {
  896. previousYearDate = adjustDateToFitInsideRange(previousYearDate);
  897. focusedDate.setYear(previousYearDate.getYear());
  898. focusedDate.setMonth(previousYearDate.getMonth());
  899. focusedDate.setDate(previousYearDate.getDate());
  900. displayedMonth.setYear(previousYearDate.getYear());
  901. displayedMonth.setMonth(previousYearDate.getMonth());
  902. } else {
  903. int currentMonth = focusedDate.getMonth();
  904. focusedDate.setYear(focusedDate.getYear() - years);
  905. displayedMonth.setYear(displayedMonth.getYear() - years);
  906. /*
  907. * If the focused date was a leap day (Feb 29), the new date becomes
  908. * Mar 1 if the new year is not also a leap year. Set it to Feb 28
  909. * instead.
  910. */
  911. if (focusedDate.getMonth() != currentMonth) {
  912. focusedDate.setDate(0);
  913. }
  914. }
  915. renderCalendar();
  916. }
  917. /**
  918. * Selects the next year
  919. */
  920. private void focusNextYear(int years) {
  921. if (focusedDate == null) {
  922. return;
  923. }
  924. Date nextYearDate = (Date) focusedDate.clone();
  925. nextYearDate.setYear(nextYearDate.getYear() + years);
  926. // Do not focus if not inside range
  927. if (!isDateInsideRange(nextYearDate, getResolution(this::isYear))) {
  928. return;
  929. }
  930. // If we add one year, but have to roll back a bit, fit it
  931. // into the calendar. Also the months have to be changed
  932. if (!isDateInsideRange(nextYearDate, getResolution(this::isDay))) {
  933. nextYearDate = adjustDateToFitInsideRange(nextYearDate);
  934. focusedDate.setYear(nextYearDate.getYear());
  935. focusedDate.setMonth(nextYearDate.getMonth());
  936. focusedDate.setDate(nextYearDate.getDate());
  937. displayedMonth.setYear(nextYearDate.getYear());
  938. displayedMonth.setMonth(nextYearDate.getMonth());
  939. } else {
  940. int currentMonth = focusedDate.getMonth();
  941. focusedDate.setYear(focusedDate.getYear() + years);
  942. displayedMonth.setYear(displayedMonth.getYear() + years);
  943. /*
  944. * If the focused date was a leap day (Feb 29), the new date becomes
  945. * Mar 1 if the new year is not also a leap year. Set it to Feb 28
  946. * instead.
  947. */
  948. if (focusedDate.getMonth() != currentMonth) {
  949. focusedDate.setDate(0);
  950. }
  951. }
  952. renderCalendar();
  953. }
  954. /**
  955. * Handles a user click on the component
  956. *
  957. * @param sender
  958. * The component that was clicked
  959. * @param updateVariable
  960. * Should the value field be updated
  961. *
  962. */
  963. private void processClickEvent(Widget sender) {
  964. if (!isEnabled() || isReadonly()) {
  965. return;
  966. }
  967. if (sender == prevYear) {
  968. focusPreviousYear(1);
  969. } else if (sender == nextYear) {
  970. focusNextYear(1);
  971. } else if (sender == prevMonth) {
  972. focusPreviousMonth();
  973. } else if (sender == nextMonth) {
  974. focusNextMonth();
  975. }
  976. }
  977. /*
  978. * (non-Javadoc)
  979. *
  980. * @see
  981. * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
  982. * .event.dom.client.KeyDownEvent)
  983. */
  984. @Override
  985. public void onKeyDown(KeyDownEvent event) {
  986. handleKeyPress(event);
  987. }
  988. /*
  989. * (non-Javadoc)
  990. *
  991. * @see
  992. * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
  993. * .gwt.event.dom.client.KeyPressEvent)
  994. */
  995. @Override
  996. public void onKeyPress(KeyPressEvent event) {
  997. handleKeyPress(event);
  998. }
  999. /**
  1000. * Handles the keypress from both the onKeyPress event and the onKeyDown
  1001. * event
  1002. *
  1003. * @param event
  1004. * The keydown/keypress event
  1005. */
  1006. private void handleKeyPress(DomEvent<?> event) {
  1007. // Check tabs
  1008. int keycode = event.getNativeEvent().getKeyCode();
  1009. if (keycode == KeyCodes.KEY_TAB
  1010. && event.getNativeEvent().getShiftKey()) {
  1011. if (onTabOut(event)) {
  1012. return;
  1013. }
  1014. }
  1015. // Handle the navigation
  1016. if (handleNavigation(keycode,
  1017. event.getNativeEvent().getCtrlKey()
  1018. || event.getNativeEvent().getMetaKey(),
  1019. event.getNativeEvent().getShiftKey())) {
  1020. event.preventDefault();
  1021. }
  1022. }
  1023. /**
  1024. * Notifies submit-listeners of a submit event
  1025. */
  1026. private void onSubmit() {
  1027. if (getSubmitListener() != null) {
  1028. getSubmitListener().onSubmit();
  1029. }
  1030. }
  1031. /**
  1032. * Notifies submit-listeners of a cancel event
  1033. */
  1034. private void onCancel() {
  1035. if (getSubmitListener() != null) {
  1036. getSubmitListener().onCancel();
  1037. }
  1038. }
  1039. /**
  1040. * Handles the keyboard navigation when the resolution is set to years.
  1041. *
  1042. * @param keycode
  1043. * The keycode to process
  1044. * @param ctrl
  1045. * Is ctrl pressed?
  1046. * @param shift
  1047. * is shift pressed
  1048. * @return Returns true if the keycode was processed, else false
  1049. */
  1050. protected boolean handleNavigationYearMode(int keycode, boolean ctrl,
  1051. boolean shift) {
  1052. // Ctrl and Shift selection not supported
  1053. if (ctrl || shift) {
  1054. return false;
  1055. }
  1056. else if (keycode == getPreviousKey()) {
  1057. focusNextYear(10); // Add 10 years
  1058. return true;
  1059. }
  1060. else if (keycode == getForwardKey()) {
  1061. focusNextYear(1); // Add 1 year
  1062. return true;
  1063. }
  1064. else if (keycode == getNextKey()) {
  1065. focusPreviousYear(10); // Subtract 10 years
  1066. return true;
  1067. }
  1068. else if (keycode == getBackwardKey()) {
  1069. focusPreviousYear(1); // Subtract 1 year
  1070. return true;
  1071. } else if (keycode == getSelectKey()) {
  1072. value = (Date) focusedDate.clone();
  1073. onSubmit();
  1074. return true;
  1075. } else if (keycode == getResetKey()) {
  1076. // Restore showing value the selected value
  1077. focusedDate.setTime(value.getTime());
  1078. renderCalendar();
  1079. return true;
  1080. } else if (keycode == getCloseKey()) {
  1081. // TODO fire listener, on users responsibility??
  1082. onCancel();
  1083. return true;
  1084. }
  1085. return false;
  1086. }
  1087. /**
  1088. * Handle the keyboard navigation when the resolution is set to MONTH
  1089. *
  1090. * @param keycode
  1091. * The keycode to handle
  1092. * @param ctrl
  1093. * Was the ctrl key pressed?
  1094. * @param shift
  1095. * Was the shift key pressed?
  1096. * @return
  1097. */
  1098. protected boolean handleNavigationMonthMode(int keycode, boolean ctrl,
  1099. boolean shift) {
  1100. // Ctrl selection not supported
  1101. if (ctrl) {
  1102. return false;
  1103. } else if (keycode == getPreviousKey()) {
  1104. focusNextYear(1); // Add 1 year
  1105. return true;
  1106. } else if (keycode == getForwardKey()) {
  1107. focusNextMonth(); // Add 1 month
  1108. return true;
  1109. } else if (keycode == getNextKey()) {
  1110. focusPreviousYear(1); // Subtract 1 year
  1111. return true;
  1112. } else if (keycode == getBackwardKey()) {
  1113. focusPreviousMonth(); // Subtract 1 month
  1114. return true;
  1115. } else if (keycode == getSelectKey()) {
  1116. value = (Date) focusedDate.clone();
  1117. onSubmit();
  1118. return true;
  1119. } else if (keycode == getResetKey()) {
  1120. // Restore showing value the selected value
  1121. focusedDate.setTime(value.getTime());
  1122. renderCalendar();
  1123. return true;
  1124. } else if (keycode == getCloseKey() || keycode == KeyCodes.KEY_TAB) {
  1125. onCancel();
  1126. // TODO fire close event
  1127. return true;
  1128. }
  1129. return false;
  1130. }
  1131. /**
  1132. * Handle keyboard navigation what the resolution is set to DAY
  1133. *
  1134. * @param keycode
  1135. * The keycode to handle
  1136. * @param ctrl
  1137. * Was the ctrl key pressed?
  1138. * @param shift
  1139. * Was the shift key pressed?
  1140. * @return Return true if the key press was handled by the method, else
  1141. * return false.
  1142. */
  1143. protected boolean handleNavigationDayMode(int keycode, boolean ctrl,
  1144. boolean shift) {
  1145. // Ctrl key is not in use
  1146. if (ctrl) {
  1147. return false;
  1148. }
  1149. /*
  1150. * Jumps to the next day.
  1151. */
  1152. if (keycode == getForwardKey() && !shift) {
  1153. focusNextDay(1);
  1154. return true;
  1155. /*
  1156. * Jumps to the previous day
  1157. */
  1158. } else if (keycode == getBackwardKey() && !shift) {
  1159. focusPreviousDay(1);
  1160. return true;
  1161. /*
  1162. * Jumps one week forward in the calendar
  1163. */
  1164. } else if (keycode == getNextKey() && !shift) {
  1165. focusNextDay(7);
  1166. return true;
  1167. /*
  1168. * Jumps one week back in the calendar
  1169. */
  1170. } else if (keycode == getPreviousKey() && !shift) {
  1171. focusPreviousDay(7);
  1172. return true;
  1173. /*
  1174. * Selects the value that is chosen
  1175. */
  1176. } else if (keycode == getSelectKey() && !shift) {
  1177. selectFocused();
  1178. onSubmit(); // submit
  1179. return true;
  1180. } else if (keycode == getCloseKey()) {
  1181. onCancel();
  1182. // TODO close event
  1183. return true;
  1184. /*
  1185. * Jumps to the next month
  1186. */
  1187. } else if (shift && keycode == getForwardKey()) {
  1188. focusNextMonth();
  1189. return true;
  1190. /*
  1191. * Jumps to the previous month
  1192. */
  1193. } else if (shift && keycode == getBackwardKey()) {
  1194. focusPreviousMonth();
  1195. return true;
  1196. /*
  1197. * Jumps to the next year
  1198. */
  1199. } else if (shift && keycode == getPreviousKey()) {
  1200. focusNextYear(1);
  1201. return true;
  1202. /*
  1203. * Jumps to the previous year
  1204. */
  1205. } else if (shift && keycode == getNextKey()) {
  1206. focusPreviousYear(1);
  1207. return true;
  1208. /*
  1209. * Resets the selection
  1210. */
  1211. } else if (keycode == getResetKey() && !shift) {
  1212. // Restore showing value the selected value
  1213. focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
  1214. value.getDate());
  1215. displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
  1216. 1);
  1217. renderCalendar();
  1218. return true;
  1219. }
  1220. return false;
  1221. }
  1222. /**
  1223. * Handles the keyboard navigation
  1224. *
  1225. * @param keycode
  1226. * The key code that was pressed
  1227. * @param ctrl
  1228. * Was the ctrl key pressed
  1229. * @param shift
  1230. * Was the shift key pressed
  1231. * @return Return true if key press was handled by the component, else
  1232. * return false
  1233. */
  1234. protected boolean handleNavigation(int keycode, boolean ctrl,
  1235. boolean shift) {
  1236. if (!isEnabled() || isReadonly()) {
  1237. return false;
  1238. }
  1239. else if (isYear(getResolution())) {
  1240. return handleNavigationYearMode(keycode, ctrl, shift);
  1241. }
  1242. else if (isMonth(getResolution())) {
  1243. return handleNavigationMonthMode(keycode, ctrl, shift);
  1244. }
  1245. else if (isDay(getResolution())) {
  1246. return handleNavigationDayMode(keycode, ctrl, shift);
  1247. }
  1248. else {
  1249. return handleNavigationDayMode(keycode, ctrl, shift);
  1250. }
  1251. }
  1252. /**
  1253. * Returns the reset key which will reset the calendar to the previous
  1254. * selection. By default this is backspace but it can be overriden to change
  1255. * the key to whatever you want.
  1256. *
  1257. * @return
  1258. */
  1259. protected int getResetKey() {
  1260. return KeyCodes.KEY_BACKSPACE;
  1261. }
  1262. /**
  1263. * Returns the select key which selects the value. By default this is the
  1264. * enter key but it can be changed to whatever you like by overriding this
  1265. * method.
  1266. *
  1267. * @return
  1268. */
  1269. protected int getSelectKey() {
  1270. return KeyCodes.KEY_ENTER;
  1271. }
  1272. /**
  1273. * Returns the key that closes the popup window if this is a VPopopCalendar.
  1274. * Else this does nothing. By default this is the Escape key but you can
  1275. * change the key to whatever you want by overriding this method.
  1276. *
  1277. * @return
  1278. */
  1279. protected int getCloseKey() {
  1280. return KeyCodes.KEY_ESCAPE;
  1281. }
  1282. /**
  1283. * The key that selects the next day in the calendar. By default this is the
  1284. * right arrow key but by overriding this method it can be changed to
  1285. * whatever you like.
  1286. *
  1287. * @return
  1288. */
  1289. protected int getForwardKey() {
  1290. return KeyCodes.KEY_RIGHT;
  1291. }
  1292. /**
  1293. * The key that selects the previous day in the calendar. By default this is
  1294. * the left arrow key but by overriding this method it can be changed to
  1295. * whatever you like.
  1296. *
  1297. * @return
  1298. */
  1299. protected int getBackwardKey() {
  1300. return KeyCodes.KEY_LEFT;
  1301. }
  1302. /**
  1303. * The key that selects the next week in the calendar. By default this is
  1304. * the down arrow key but by overriding this method it can be changed to
  1305. * whatever you like.
  1306. *
  1307. * @return
  1308. */
  1309. protected int getNextKey() {
  1310. return KeyCodes.KEY_DOWN;
  1311. }
  1312. /**
  1313. * The key that selects the previous week in the calendar. By default this
  1314. * is the up arrow key but by overriding this method it can be changed to
  1315. * whatever you like.
  1316. *
  1317. * @return
  1318. */
  1319. protected int getPreviousKey() {
  1320. return KeyCodes.KEY_UP;
  1321. }
  1322. /*
  1323. * (non-Javadoc)
  1324. *
  1325. * @see
  1326. * com.google.gwt.event.dom.client.MouseOutHandler#onMouseOut(com.google
  1327. * .gwt.event.dom.client.MouseOutEvent)
  1328. */
  1329. @Override
  1330. public void onMouseOut(MouseOutEvent event) {
  1331. if (mouseTimer != null) {
  1332. mouseTimer.cancel();
  1333. }
  1334. }
  1335. /*
  1336. * (non-Javadoc)
  1337. *
  1338. * @see
  1339. * com.google.gwt.event.dom.client.MouseDownHandler#onMouseDown(com.google
  1340. * .gwt.event.dom.client.MouseDownEvent)
  1341. */
  1342. @Override
  1343. public void onMouseDown(MouseDownEvent event) {
  1344. // Click-n-hold the left mouse button for fast-forward or fast-rewind.
  1345. // Timer is first used for a 500ms delay after mousedown. After that has
  1346. // elapsed, another timer is triggered to go off every 150ms. Both
  1347. // timers are cancelled on mouseup or mouseout.
  1348. if (event.getNativeButton() == NativeEvent.BUTTON_LEFT && event
  1349. .getSource() instanceof VAbstractCalendarPanel.VEventButton) {
  1350. final VEventButton sender = (VEventButton) event.getSource();
  1351. processClickEvent(sender);
  1352. mouseTimer = new Timer() {
  1353. @Override
  1354. public void run() {
  1355. mouseTimer = new Timer() {
  1356. @Override
  1357. public void run() {
  1358. processClickEvent(sender);
  1359. }
  1360. };
  1361. mouseTimer.scheduleRepeating(150);
  1362. }
  1363. };
  1364. mouseTimer.schedule(500);
  1365. }
  1366. }
  1367. /*
  1368. * (non-Javadoc)
  1369. *
  1370. * @see
  1371. * com.google.gwt.event.dom.client.MouseUpHandler#onMouseUp(com.google.gwt
  1372. * .event.dom.client.MouseUpEvent)
  1373. */
  1374. @Override
  1375. public void onMouseUp(MouseUpEvent event) {
  1376. if (mouseTimer != null) {
  1377. mouseTimer.cancel();
  1378. }
  1379. }
  1380. /**
  1381. * Adjusts a date to fit inside the range, only if outside
  1382. *
  1383. * @param date
  1384. */
  1385. private Date adjustDateToFitInsideRange(Date date) {
  1386. if (rangeStart != null && rangeStart.after(date)) {
  1387. date = (Date) rangeStart.clone();
  1388. } else if (rangeEnd != null && rangeEnd.before(date)) {
  1389. date = (Date) rangeEnd.clone();
  1390. }
  1391. return date;
  1392. }
  1393. /**
  1394. * Sets the data of the Panel.
  1395. *
  1396. * @param currentDate
  1397. * The date to set
  1398. */
  1399. public void setDate(Date currentDate) {
  1400. doSetDate(currentDate, false, () -> {
  1401. });
  1402. }
  1403. /**
  1404. * The actual implementation of the logic which sets the data of the Panel.
  1405. * The method {@link #setDate(Date)} just delegate a call to this method
  1406. * providing additional config parameters.
  1407. *
  1408. * @param currentDate
  1409. * currentDate The date to set
  1410. * @param needRerender
  1411. * if {@code true} then calendar will be rerendered regardless of
  1412. * internal logic, otherwise the decision will be made on the
  1413. * internal state inside the method
  1414. * @param focusAction
  1415. * an additional action which will be executed in case
  1416. * rerendering is not required
  1417. */
  1418. protected void doSetDate(Date currentDate, boolean needRerender,
  1419. Runnable focusAction) {
  1420. // Check that we are not re-rendering an already active date
  1421. if (currentDate == value && currentDate != null) {
  1422. return;
  1423. }
  1424. boolean currentDateWasAdjusted = false;
  1425. // Check that selected date is inside the allowed range
  1426. if (currentDate != null
  1427. && !isDateInsideRange(currentDate, getResolution())) {
  1428. currentDate = adjustDateToFitInsideRange(currentDate);
  1429. currentDateWasAdjusted = true;
  1430. }
  1431. Date oldDisplayedMonth = displayedMonth;
  1432. value = currentDate;
  1433. // If current date was adjusted, we will not select any date,
  1434. // since that will look like a date is selected. Instead we
  1435. // only focus on the adjusted value
  1436. if (value == null || currentDateWasAdjusted) {
  1437. // If ranges enabled, we may need to focus on a different view to
  1438. // potentially not get stuck
  1439. if (rangeStart != null || rangeEnd != null) {
  1440. Date dateThatFitsInsideRange = adjustDateToFitInsideRange(
  1441. new Date());
  1442. focusedDate = new FocusedDate(dateThatFitsInsideRange.getYear(),
  1443. dateThatFitsInsideRange.getMonth(),
  1444. dateThatFitsInsideRange.getDate());
  1445. displayedMonth = new FocusedDate(
  1446. dateThatFitsInsideRange.getYear(),
  1447. dateThatFitsInsideRange.getMonth(), 1);
  1448. // value was adjusted. Set selected to null to not cause
  1449. // confusion, but this is only needed (and allowed) when we have
  1450. // a day
  1451. // resolution
  1452. if (isDay(getResolution())) {
  1453. value = null;
  1454. }
  1455. } else {
  1456. focusedDate = displayedMonth = null;
  1457. }
  1458. } else {
  1459. focusedDate = new FocusedDate(value.getYear(), value.getMonth(),
  1460. value.getDate());
  1461. displayedMonth = new FocusedDate(value.getYear(), value.getMonth(),
  1462. 1);
  1463. }
  1464. // Re-render calendar if the displayed month is changed.
  1465. if (needRerender || oldDisplayedMonth == null || value == null
  1466. || oldDisplayedMonth.getYear() != value.getYear()
  1467. || oldDisplayedMonth.getMonth() != value.getMonth()) {
  1468. renderCalendar();
  1469. } else {
  1470. focusDay(focusedDate);
  1471. selectFocused();
  1472. focusAction.run();
  1473. }
  1474. if (!hasFocus) {
  1475. focusDay(null);
  1476. }
  1477. }
  1478. /**
  1479. * A widget representing a single day in the calendar panel.
  1480. */
  1481. private class Day extends InlineHTML {
  1482. private final Date date;
  1483. Day(Date date) {
  1484. super("" + date.getDate());
  1485. this.date = date;
  1486. addClickHandler(dayClickHandler);
  1487. }
  1488. public Date getDate() {
  1489. return date;
  1490. }
  1491. }
  1492. public Date getDate() {
  1493. return value;
  1494. }
  1495. /**
  1496. * If true should be returned if the panel will not be used after this
  1497. * event.
  1498. *
  1499. * @param event
  1500. * @return
  1501. */
  1502. protected boolean onTabOut(DomEvent<?> event) {
  1503. if (focusOutListener != null) {
  1504. return focusOutListener.onFocusOut(event);
  1505. }
  1506. return false;
  1507. }
  1508. /**
  1509. * A focus out listener is triggered when the panel loosed focus. This can
  1510. * happen either after a user clicks outside the panel or tabs out.
  1511. *
  1512. * @param listener
  1513. * The listener to trigger
  1514. */
  1515. public void setFocusOutListener(FocusOutListener listener) {
  1516. focusOutListener = listener;
  1517. }
  1518. /**
  1519. * The submit listener is called when the user selects a value from the
  1520. * calender either by clicking the day or selects it by keyboard.
  1521. *
  1522. * @param submitListener
  1523. * The listener to trigger
  1524. */
  1525. public void setSubmitListener(SubmitListener submitListener) {
  1526. this.submitListener = submitListener;
  1527. }
  1528. /**
  1529. * The given FocusChangeListener is notified when the focused date changes
  1530. * by user either clicking on a new date or by using the keyboard.
  1531. *
  1532. * @param listener
  1533. * The FocusChangeListener to be notified
  1534. */
  1535. public void setFocusChangeListener(FocusChangeListener listener) {
  1536. focusChangeListener = listener;
  1537. }
  1538. /**
  1539. * Returns the submit listener that listens to selection made from the panel
  1540. *
  1541. * @return The listener or NULL if no listener has been set
  1542. */
  1543. public SubmitListener getSubmitListener() {
  1544. return submitListener;
  1545. }
  1546. /*
  1547. * (non-Javadoc)
  1548. *
  1549. * @see
  1550. * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
  1551. * .dom.client.BlurEvent)
  1552. */
  1553. @Override
  1554. public void onBlur(final BlurEvent event) {
  1555. if (event.getSource() instanceof VAbstractCalendarPanel) {
  1556. hasFocus = false;
  1557. focusDay(null);
  1558. }
  1559. }
  1560. /*
  1561. * (non-Javadoc)
  1562. *
  1563. * @see
  1564. * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
  1565. * .dom.client.FocusEvent)
  1566. */
  1567. @Override
  1568. public void onFocus(FocusEvent event) {
  1569. if (event.getSource() instanceof VAbstractCalendarPanel) {
  1570. hasFocus = true;
  1571. // Focuses the current day if the calendar shows the days
  1572. if (focusedDay != null) {
  1573. focusDay(focusedDate);
  1574. }
  1575. }
  1576. }
  1577. private static final String SUBPART_NEXT_MONTH = "nextmon";
  1578. private static final String SUBPART_PREV_MONTH = "prevmon";
  1579. private static final String SUBPART_NEXT_YEAR = "nexty";
  1580. private static final String SUBPART_PREV_YEAR = "prevy";
  1581. private static final String SUBPART_HOUR_SELECT = "h";
  1582. private static final String SUBPART_MINUTE_SELECT = "m";
  1583. private static final String SUBPART_SECS_SELECT = "s";
  1584. private static final String SUBPART_AMPM_SELECT = "ampm";
  1585. private static final String SUBPART_DAY = "day";
  1586. private static final String SUBPART_MONTH_YEAR_HEADER = "header";
  1587. private Date rangeStart;
  1588. private Date rangeEnd;
  1589. @Override
  1590. public String getSubPartName(
  1591. com.google.gwt.user.client.Element subElement) {
  1592. if (contains(nextMonth, subElement)) {
  1593. return SUBPART_NEXT_MONTH;
  1594. } else if (contains(prevMonth, subElement)) {
  1595. return SUBPART_PREV_MONTH;
  1596. } else if (contains(nextYear, subElement)) {
  1597. return SUBPART_NEXT_YEAR;
  1598. } else if (contains(prevYear, subElement)) {
  1599. return SUBPART_PREV_YEAR;
  1600. } else if (contains(days, subElement)) {
  1601. // Day, find out which dayOfMonth and use that as the identifier
  1602. Day day = WidgetUtil.findWidget(subElement, Day.class);
  1603. if (day != null) {
  1604. Date date = day.getDate();
  1605. int id = date.getDate();
  1606. // Zero or negative ids map to days of the preceding month,
  1607. // past-the-end-of-month ids to days of the following month
  1608. if (date.getMonth() < displayedMonth.getMonth()) {
  1609. id -= DateTimeService.getNumberOfDaysInMonth(date);
  1610. } else if (date.getMonth() > displayedMonth.getMonth()) {
  1611. id += DateTimeService
  1612. .getNumberOfDaysInMonth(displayedMonth);
  1613. }
  1614. return SUBPART_DAY + id;
  1615. }
  1616. } else if (getCellFormatter().getElement(0, 2)
  1617. .isOrHasChild(subElement)) {
  1618. return SUBPART_MONTH_YEAR_HEADER;
  1619. }
  1620. return null;
  1621. }
  1622. /**
  1623. * Checks if subElement is inside the widget DOM hierarchy.
  1624. *
  1625. * @param w
  1626. * @param subElement
  1627. * @return true if {@code w} is a parent of subElement, false otherwise.
  1628. */
  1629. protected boolean contains(Widget w, Element subElement) {
  1630. if (w == null || w.getElement() == null) {
  1631. return false;
  1632. }
  1633. return w.getElement().isOrHasChild(subElement);
  1634. }
  1635. @Override
  1636. public com.google.gwt.user.client.Element getSubPartElement(
  1637. String subPart) {
  1638. if (SUBPART_NEXT_MONTH.equals(subPart)) {
  1639. return nextMonth.getElement();
  1640. }
  1641. if (SUBPART_PREV_MONTH.equals(subPart)) {
  1642. return prevMonth.getElement();
  1643. }
  1644. if (SUBPART_NEXT_YEAR.equals(subPart)) {
  1645. return nextYear.getElement();
  1646. }
  1647. if (SUBPART_PREV_YEAR.equals(subPart)) {
  1648. return prevYear.getElement();
  1649. }
  1650. if (subPart.startsWith(SUBPART_DAY)) {
  1651. // Zero or negative ids map to days in the preceding month,
  1652. // past-the-end-of-month ids to days in the following month
  1653. int dayOfMonth = Integer
  1654. .parseInt(subPart.substring(SUBPART_DAY.length()));
  1655. Date date = new Date(displayedMonth.getYear(),
  1656. displayedMonth.getMonth(), dayOfMonth);
  1657. Iterator<Widget> iter = days.iterator();
  1658. while (iter.hasNext()) {
  1659. Widget w = iter.next();
  1660. if (w instanceof VAbstractCalendarPanel.Day) {
  1661. Day day = (Day) w;
  1662. if (day.getDate().equals(date)) {
  1663. return day.getElement();
  1664. }
  1665. }
  1666. }
  1667. }
  1668. if (SUBPART_MONTH_YEAR_HEADER.equals(subPart)) {
  1669. return DOM.asOld(
  1670. (Element) getCellFormatter().getElement(0, 2).getChild(0));
  1671. }
  1672. return null;
  1673. }
  1674. @Override
  1675. protected void onDetach() {
  1676. super.onDetach();
  1677. if (mouseTimer != null) {
  1678. mouseTimer.cancel();
  1679. }
  1680. }
  1681. /**
  1682. * Helper class to inform the screen reader that the user changed the
  1683. * selected date. It sets the value of a field that is outside the view, and
  1684. * is defined as a live area. That way the screen reader recognizes the
  1685. * change and reads it to the user.
  1686. */
  1687. public class FocusedDate extends Date {
  1688. public FocusedDate(int year, int month, int date) {
  1689. super(year, month, date);
  1690. }
  1691. @Override
  1692. public void setTime(long time) {
  1693. super.setTime(time);
  1694. setLabel();
  1695. }
  1696. @Override
  1697. @Deprecated
  1698. public void setDate(int date) {
  1699. super.setDate(date);
  1700. setLabel();
  1701. }
  1702. @Override
  1703. @Deprecated
  1704. public void setMonth(int month) {
  1705. super.setMonth(month);
  1706. setLabel();
  1707. }
  1708. @Override
  1709. @Deprecated
  1710. public void setYear(int year) {
  1711. super.setYear(year);
  1712. setLabel();
  1713. }
  1714. private void setLabel() {
  1715. if (parent instanceof VAbstractPopupCalendar) {
  1716. ((VAbstractPopupCalendar) parent).setFocusedDate(this);
  1717. }
  1718. }
  1719. }
  1720. /**
  1721. * Sets the start range for this component. The start range is inclusive,
  1722. * and it depends on the current resolution, what is considered inside the
  1723. * range.
  1724. *
  1725. * @param startDate
  1726. * - the allowed range's start date
  1727. */
  1728. public void setRangeStart(Date newRangeStart) {
  1729. if (!SharedUtil.equals(rangeStart, newRangeStart)) {
  1730. rangeStart = newRangeStart;
  1731. if (initialRenderDone) {
  1732. // Dynamic updates to the range needs to render the calendar to
  1733. // update the element stylenames
  1734. renderCalendar();
  1735. }
  1736. }
  1737. }
  1738. /**
  1739. * Sets the end range for this component. The end range is inclusive, and it
  1740. * depends on the current resolution, what is considered inside the range.
  1741. *
  1742. * @param endDate
  1743. * - the allowed range's end date
  1744. */
  1745. public void setRangeEnd(Date newRangeEnd) {
  1746. if (!SharedUtil.equals(rangeEnd, newRangeEnd)) {
  1747. rangeEnd = newRangeEnd;
  1748. if (initialRenderDone) {
  1749. // Dynamic updates to the range needs to render the calendar to
  1750. // update the element stylenames
  1751. renderCalendar();
  1752. }
  1753. }
  1754. }
  1755. }