/src/wt/src/Wt/WCalendar.C
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) 2008 Emweb bv, Herent, Belgium. |
3 | | * |
4 | | * See the LICENSE file for terms of use. |
5 | | */ |
6 | | |
7 | | #include "Wt/WApplication.h" |
8 | | #include "Wt/WEnvironment.h" |
9 | | #include "Wt/WCalendar.h" |
10 | | |
11 | | #include "Wt/WComboBox.h" |
12 | | #include "Wt/WInPlaceEdit.h" |
13 | | #include "Wt/WLineEdit.h" |
14 | | #include "Wt/WLogger.h" |
15 | | #include "Wt/WStringStream.h" |
16 | | #include "Wt/WTemplate.h" |
17 | | #include "Wt/WText.h" |
18 | | |
19 | | #include "WebUtils.h" |
20 | | |
21 | | namespace Wt { |
22 | | |
23 | | LOGGER("WCalendar"); |
24 | | |
25 | | // Because WDate returns days and weeks as WT_USTRING, we need this: |
26 | | #ifndef WT_TARGET_JAVA |
27 | | #define DATE_NAME_STR(e) e |
28 | | #else |
29 | | #define DATE_NAME_STR(e) WString::fromUTF8(e) |
30 | | #endif |
31 | | |
32 | | WCalendar::WCalendar() |
33 | 0 | { |
34 | 0 | create(); |
35 | 0 | impl_->addStyleClass("Wt-calendar"); |
36 | 0 | } |
37 | | |
38 | | void WCalendar::setSelectionMode(SelectionMode mode) |
39 | 0 | { |
40 | 0 | if (selectionMode_ != mode) { |
41 | 0 | if (mode != SelectionMode::Extended && selection_.size() > 1) { |
42 | 0 | selection_.clear(); |
43 | 0 | renderMonth(); |
44 | 0 | } |
45 | 0 | selectionMode_ = mode; |
46 | 0 | } |
47 | 0 | } |
48 | | |
49 | | void WCalendar::setSingleClickSelect(bool single) |
50 | 0 | { |
51 | 0 | singleClickSelect_ = single; |
52 | 0 | } |
53 | | |
54 | | void WCalendar::create() |
55 | 0 | { |
56 | 0 | selectionMode_ = SelectionMode::Single; |
57 | 0 | singleClickSelect_ = false; |
58 | 0 | horizontalHeaderFormat_ = CalendarHeaderFormat::ShortDayNames; |
59 | 0 | firstDayOfWeek_ = 1; |
60 | |
|
61 | 0 | WDate currentDay = WDate::currentDate(); |
62 | |
|
63 | 0 | currentYear_ = currentDay.year(); |
64 | 0 | currentMonth_ = currentDay.month(); |
65 | |
|
66 | 0 | WStringStream text; |
67 | |
|
68 | 0 | text << |
69 | 0 | "<table class=\"days ${table-class}\" cellspacing=\"0\" cellpadding=\"0\">" |
70 | 0 | """<tr>" |
71 | 0 | "" "<th class=\"caption\">${nav-prev}</th>" |
72 | 0 | "" "<th class=\"caption\"colspan=\"5\">${month} ${year}</th>" |
73 | 0 | "" "<th class=\"caption\">${nav-next}</th>" |
74 | 0 | """</tr>" |
75 | 0 | """<tr>"; |
76 | |
|
77 | 0 | for (int j = 0; j < 7; ++j) |
78 | 0 | text << |
79 | 0 | "<th title=\"${t" << j << "}\" scope=\"col\">${d" << j << "}</th>"; |
80 | |
|
81 | 0 | text << "</tr>"; |
82 | |
|
83 | 0 | for (int i = 0; i < 6; ++i) { |
84 | 0 | text << "<tr>"; |
85 | 0 | for (int j = 0; j < 7; ++j) |
86 | 0 | text << "<td>${c" << (i * 7 + j) << "}</td>"; |
87 | 0 | text << "</tr>"; |
88 | 0 | } |
89 | |
|
90 | 0 | text << "</table>"; |
91 | |
|
92 | 0 | std::unique_ptr<WTemplate> t(new WTemplate()); |
93 | 0 | impl_ = t.get(); |
94 | 0 | setImplementation(std::move(t)); |
95 | 0 | impl_->setTemplateText(WString::fromUTF8(text.str()), |
96 | 0 | TextFormat::UnsafeXHTML); |
97 | 0 | impl_->setStyleClass("Wt-cal"); |
98 | |
|
99 | 0 | setSelectable(false); |
100 | |
|
101 | 0 | std::unique_ptr<WText> prevMonth(new WText(tr("Wt.WCalendar.PrevMonth"))); |
102 | 0 | prevMonth->setStyleClass("Wt-cal-navbutton"); |
103 | 0 | prevMonth->clicked().connect(this, &WCalendar::browseToPreviousMonth); |
104 | |
|
105 | 0 | std::unique_ptr<WText> nextMonth(new WText(tr("Wt.WCalendar.NextMonth"))); |
106 | 0 | nextMonth->setStyleClass("Wt-cal-navbutton"); |
107 | 0 | nextMonth->clicked().connect(this, &WCalendar::browseToNextMonth); |
108 | |
|
109 | 0 | std::unique_ptr<WComboBox> monthEdit(new WComboBox()); |
110 | 0 | monthEdit_ = monthEdit.get(); |
111 | |
|
112 | 0 | monthEdit->setInline(true); |
113 | 0 | for (unsigned i = 0; i < 12; ++i) |
114 | 0 | monthEdit->addItem(WDate::longMonthName(i+1)); |
115 | 0 | monthEdit->activated().connect(this, &WCalendar::monthChanged); |
116 | 0 | monthEdit->setDisabled(!WApplication::instance()->environment().ajax()); |
117 | |
|
118 | 0 | std::unique_ptr<WInPlaceEdit> yearEdit(new WInPlaceEdit("")); |
119 | 0 | yearEdit_ = yearEdit.get(); |
120 | |
|
121 | 0 | yearEdit->setButtonsEnabled(false); |
122 | 0 | yearEdit->lineEdit()->setTextSize(4); |
123 | 0 | yearEdit->setStyleClass("Wt-cal-year"); |
124 | 0 | yearEdit->valueChanged().connect(this, &WCalendar::yearChanged); |
125 | |
|
126 | 0 | impl_->bindWidget("nav-prev", std::move(prevMonth)); |
127 | 0 | impl_->bindWidget("nav-next", std::move(nextMonth)); |
128 | 0 | impl_->bindWidget("month", std::move(monthEdit)); |
129 | 0 | impl_->bindWidget("year", std::move(yearEdit)); |
130 | |
|
131 | 0 | setHorizontalHeaderFormat(horizontalHeaderFormat_); |
132 | 0 | setFirstDayOfWeek(firstDayOfWeek_); |
133 | 0 | } |
134 | | |
135 | | void WCalendar::enableAjax() |
136 | 0 | { |
137 | 0 | WCompositeWidget::enableAjax(); |
138 | 0 | monthEdit_->enable(); |
139 | |
|
140 | 0 | } |
141 | | |
142 | | void WCalendar::load() |
143 | 0 | { |
144 | 0 | WCompositeWidget::load(); |
145 | 0 | if(WApplication::instance()->environment().ajax()){ |
146 | 0 | monthEdit_->enable(); |
147 | 0 | } |
148 | 0 | } |
149 | | |
150 | | void WCalendar::setFirstDayOfWeek(int dayOfWeek) |
151 | 0 | { |
152 | 0 | firstDayOfWeek_ = dayOfWeek; |
153 | |
|
154 | 0 | for (unsigned i = 0; i < 7; ++i) { |
155 | 0 | int day = (i + firstDayOfWeek_ - 1) % 7 + 1; |
156 | |
|
157 | 0 | WString title = WDate::longDayName(day); |
158 | 0 | impl_->bindString("t" + std::to_string(i), |
159 | 0 | title, |
160 | 0 | TextFormat::UnsafeXHTML); |
161 | |
|
162 | 0 | WString abbr; |
163 | 0 | switch (horizontalHeaderFormat_) { |
164 | 0 | case CalendarHeaderFormat::SingleLetterDayNames: |
165 | 0 | abbr = WString::fromUTF8(WDate::shortDayName(day).toUTF8().substr(0, 1)); |
166 | 0 | break; |
167 | 0 | case CalendarHeaderFormat::ShortDayNames: |
168 | 0 | abbr = WDate::shortDayName(day); |
169 | 0 | break; |
170 | 0 | case CalendarHeaderFormat::LongDayNames: |
171 | 0 | abbr = WDate::longDayName(day); |
172 | 0 | break; |
173 | 0 | } |
174 | | |
175 | 0 | impl_->bindString("d" + std::to_string(i), abbr, |
176 | 0 | TextFormat::UnsafeXHTML); |
177 | 0 | } |
178 | | |
179 | 0 | renderMonth(); |
180 | 0 | } |
181 | | |
182 | | void WCalendar::setHorizontalHeaderFormat(CalendarHeaderFormat format) |
183 | 0 | { |
184 | 0 | std::string d; |
185 | 0 | switch (format) { |
186 | 0 | case CalendarHeaderFormat::SingleLetterDayNames: |
187 | 0 | d = "d1"; break; |
188 | 0 | case CalendarHeaderFormat::ShortDayNames: |
189 | 0 | d = "d3"; break; |
190 | 0 | case CalendarHeaderFormat::LongDayNames: |
191 | 0 | d = "dlong"; break; |
192 | 0 | default: |
193 | 0 | LOG_ERROR("setHorizontalHeaderFormat(): " |
194 | 0 | "improper horizontal header format."); |
195 | 0 | format = CalendarHeaderFormat::SingleLetterDayNames; |
196 | 0 | d = "d1"; |
197 | 0 | } |
198 | | |
199 | 0 | horizontalHeaderFormat_ = format; |
200 | |
|
201 | 0 | impl_->bindString("table-class", d, TextFormat::UnsafeXHTML); |
202 | |
|
203 | 0 | setFirstDayOfWeek(firstDayOfWeek_); |
204 | 0 | } |
205 | | |
206 | | void WCalendar::renderMonth() |
207 | 0 | { |
208 | 0 | needRenderMonth_ = true; |
209 | |
|
210 | 0 | if (isRendered()) |
211 | 0 | scheduleRender(); |
212 | 0 | } |
213 | | |
214 | | void WCalendar::render(WFlags<RenderFlag> flags) |
215 | 0 | { |
216 | 0 | if (needRenderMonth_) { |
217 | 0 | #ifndef WT_TARGET_JAVA |
218 | 0 | char buf[30]; |
219 | | #else |
220 | | char *buf; |
221 | | #endif // WT_TARGET_JAVA |
222 | |
|
223 | 0 | int m = currentMonth_ - 1; |
224 | 0 | if (monthEdit_->currentIndex() != m) |
225 | 0 | monthEdit_->setCurrentIndex(m); |
226 | |
|
227 | 0 | int y = currentYear_; |
228 | 0 | Utils::itoa(y, buf); |
229 | 0 | if (yearEdit_->text().toUTF8() != buf) |
230 | 0 | yearEdit_->setText(WString::fromUTF8(buf)); |
231 | | |
232 | | // The first line contains the last day of the previous month. |
233 | 0 | WDate d(currentYear_, currentMonth_, 1); |
234 | 0 | d = d.addDays(-1); |
235 | |
|
236 | 0 | d = WDate::previousWeekday(d, firstDayOfWeek_); |
237 | |
|
238 | 0 | for (unsigned i = 0; i < 6; ++i) { |
239 | 0 | for (unsigned j = 0; j < 7; ++j) { |
240 | 0 | Utils::itoa(i * 7 + j, buf); |
241 | 0 | std::string cell = std::string("c") + buf; |
242 | |
|
243 | 0 | WDate date(d.year(), d.month(), d.day()); |
244 | |
|
245 | 0 | WWidget *w = impl_->resolveWidget(cell); |
246 | 0 | WWidget *rw = renderCell(w, date); |
247 | 0 | WInteractWidget* iw = dynamic_cast<WInteractWidget*>(rw->webWidget()); |
248 | |
|
249 | 0 | if (rw != w) |
250 | 0 | impl_->bindWidget(cell, std::unique_ptr<WWidget>(rw)); |
251 | |
|
252 | 0 | if (iw && iw != w) { |
253 | 0 | if (clicked().isConnected() |
254 | 0 | || (selectionMode_ == SelectionMode::Extended) |
255 | 0 | || (selectionMode_ != SelectionMode::Extended && |
256 | 0 | singleClickSelect_ && activated().isConnected())) { |
257 | 0 | const Coordinate c(i, j); |
258 | 0 | iw->clicked().connect |
259 | 0 | (this, |
260 | 0 | std::bind(&WCalendar::cellClicked, this, c)); |
261 | 0 | } |
262 | |
|
263 | 0 | if ((selectionMode_ != SelectionMode::Extended && |
264 | 0 | !singleClickSelect_ && (activated().isConnected() || |
265 | 0 | selectionChanged().isConnected()))) { |
266 | 0 | const Coordinate c(i, j); |
267 | 0 | iw->doubleClicked().connect |
268 | 0 | (this, |
269 | 0 | std::bind(&WCalendar::cellDblClicked, this, c)); |
270 | 0 | } |
271 | 0 | } |
272 | |
|
273 | 0 | d = d.addDays(1); |
274 | 0 | } |
275 | 0 | } |
276 | |
|
277 | 0 | needRenderMonth_ = false; |
278 | 0 | } |
279 | |
|
280 | 0 | WCompositeWidget::render(flags); |
281 | 0 | } |
282 | | |
283 | | WWidget *WCalendar::renderCell(WWidget* widget, const WDate& date) |
284 | 0 | { |
285 | 0 | WText* t = dynamic_cast<WText*>(widget); |
286 | |
|
287 | 0 | if (!t) { |
288 | 0 | t = new WText(); |
289 | 0 | t->setInline(false); |
290 | 0 | t->setTextFormat(TextFormat::Plain); |
291 | 0 | } |
292 | |
|
293 | 0 | #ifndef WT_TARGET_JAVA |
294 | 0 | char buf[30]; |
295 | | #else |
296 | | char *buf; |
297 | | #endif // WT_TARGET_JAVA |
298 | 0 | Utils::itoa(date.day(), buf); |
299 | 0 | t->setText(WString::fromUTF8(buf)); |
300 | |
|
301 | 0 | std::string styleClass; |
302 | |
|
303 | 0 | if (isInvalid(date)) |
304 | 0 | styleClass += " Wt-cal-oor"; |
305 | 0 | else if (date.month() != currentMonth()) |
306 | 0 | styleClass += " Wt-cal-oom"; |
307 | |
|
308 | 0 | if (isSelected(date)) |
309 | 0 | styleClass += " Wt-cal-sel"; |
310 | |
|
311 | 0 | WDate currentDate = WDate::currentDate(); |
312 | 0 | if (date.day() == currentDate.day() && date.month() == currentDate.month() && |
313 | 0 | date.year() == currentDate.year()) { |
314 | 0 | if (!isSelected(date)) |
315 | 0 | styleClass += " Wt-cal-now"; |
316 | 0 | t->setToolTip(WString::tr("Wt.WCalendar.today")); |
317 | 0 | } else |
318 | 0 | t->setToolTip(""); |
319 | |
|
320 | 0 | t->setStyleClass(styleClass.c_str()); |
321 | |
|
322 | 0 | return t; |
323 | 0 | } |
324 | | |
325 | | bool WCalendar::isSelected(const WDate& d) const |
326 | 0 | { |
327 | 0 | return selection_.find(d) != selection_.end(); |
328 | 0 | } |
329 | | |
330 | | void WCalendar::clearSelection() |
331 | 0 | { |
332 | 0 | selection_.clear(); |
333 | |
|
334 | 0 | renderMonth(); |
335 | 0 | } |
336 | | |
337 | | void WCalendar::select(const WDate& date) |
338 | 0 | { |
339 | 0 | selection_.clear(); |
340 | |
|
341 | 0 | selection_.insert(date); |
342 | 0 | renderMonth(); |
343 | 0 | } |
344 | | |
345 | | void WCalendar::browseTo(const WDate& date) |
346 | 0 | { |
347 | 0 | bool rerender = false; |
348 | |
|
349 | 0 | if (currentYear_ != date.year()) { |
350 | 0 | currentYear_ = date.year(); |
351 | 0 | rerender = true; |
352 | 0 | } |
353 | |
|
354 | 0 | if (currentMonth_ != date.month()) { |
355 | 0 | currentMonth_ = date.month(); |
356 | 0 | rerender = true; |
357 | 0 | } |
358 | |
|
359 | 0 | if (rerender) { |
360 | 0 | emitCurrentPageChanged(); |
361 | 0 | renderMonth(); |
362 | 0 | } |
363 | 0 | } |
364 | | |
365 | | void WCalendar::select(const std::set<WDate>& dates) |
366 | 0 | { |
367 | 0 | if (selectionMode_ == SelectionMode::Extended) { |
368 | 0 | selection_ = dates; |
369 | 0 | renderMonth(); |
370 | 0 | } else if (selectionMode_ == SelectionMode::Single) { |
371 | 0 | if (dates.empty()) |
372 | 0 | clearSelection(); |
373 | 0 | else |
374 | 0 | select(*dates.begin()); |
375 | 0 | } |
376 | 0 | } |
377 | | |
378 | | void WCalendar::selectInCurrentMonth(const WDate& d) |
379 | 0 | { |
380 | 0 | if (d.month() == currentMonth_ && |
381 | 0 | selectionMode_ != SelectionMode::None) { |
382 | 0 | if (selectionMode_ == SelectionMode::Extended) { |
383 | 0 | if (isSelected(d)) |
384 | 0 | selection_.erase(d); |
385 | 0 | else |
386 | 0 | selection_.insert(d); |
387 | 0 | } else { |
388 | 0 | selection_.clear(); |
389 | 0 | selection_.insert(d); |
390 | 0 | } |
391 | |
|
392 | 0 | renderMonth(); |
393 | 0 | selectionChanged().emit(); |
394 | 0 | } |
395 | 0 | } |
396 | | |
397 | | bool WCalendar::isInvalid(const WDate& dt) |
398 | 0 | { |
399 | 0 | return ((!bottom_.isNull() && dt < bottom_) || |
400 | 0 | (!top_.isNull() && dt > top_)); |
401 | 0 | } |
402 | | |
403 | | void WCalendar::cellClicked(Coordinate weekday) |
404 | 0 | { |
405 | 0 | WDate dt = dateForCell(weekday.i, weekday.j); |
406 | 0 | if (isInvalid(dt)) |
407 | 0 | return; |
408 | | |
409 | 0 | selectInCurrentMonth(dt); |
410 | 0 | clicked().emit(dt); |
411 | |
|
412 | 0 | if (selectionMode_ != SelectionMode::Extended && |
413 | 0 | singleClickSelect_) |
414 | 0 | activated().emit(dt); |
415 | 0 | } |
416 | | |
417 | | void WCalendar::cellDblClicked(Coordinate weekday) |
418 | 0 | { |
419 | 0 | WDate dt = dateForCell(weekday.i, weekday.j); |
420 | 0 | if (isInvalid(dt)) |
421 | 0 | return; |
422 | | |
423 | 0 | selectInCurrentMonth(dt); |
424 | |
|
425 | 0 | if (selectionMode_ != SelectionMode::Extended && |
426 | 0 | !singleClickSelect_) |
427 | 0 | activated().emit(dt); |
428 | 0 | } |
429 | | |
430 | | WDate WCalendar::dateForCell(int week, int dayOfWeek) |
431 | 0 | { |
432 | 0 | WDate d(currentYear_, currentMonth_, 1); |
433 | 0 | d = d.addDays(-1); |
434 | 0 | d = WDate::previousWeekday(d, firstDayOfWeek_); |
435 | 0 | d = d.addDays(week * 7 + dayOfWeek); |
436 | 0 | return d; |
437 | 0 | } |
438 | | |
439 | | void WCalendar::emitCurrentPageChanged() |
440 | 0 | { |
441 | 0 | currentPageChanged().emit(currentYear_, currentMonth_); |
442 | 0 | } |
443 | | |
444 | | void WCalendar::browseToPreviousYear() |
445 | 0 | { |
446 | 0 | --currentYear_; |
447 | |
|
448 | 0 | emitCurrentPageChanged(); |
449 | 0 | renderMonth(); |
450 | 0 | } |
451 | | |
452 | | void WCalendar::browseToPreviousMonth() |
453 | 0 | { |
454 | 0 | if (--currentMonth_ == 0) { |
455 | 0 | currentMonth_ = 12; |
456 | 0 | --currentYear_; |
457 | 0 | } |
458 | |
|
459 | 0 | emitCurrentPageChanged(); |
460 | 0 | renderMonth(); |
461 | 0 | } |
462 | | |
463 | | void WCalendar::browseToNextYear() |
464 | 0 | { |
465 | 0 | ++currentYear_; |
466 | |
|
467 | 0 | emitCurrentPageChanged(); |
468 | 0 | renderMonth(); |
469 | 0 | } |
470 | | |
471 | | void WCalendar::browseToNextMonth() |
472 | 0 | { |
473 | 0 | if (++currentMonth_ == 13) { |
474 | 0 | currentMonth_ = 1; |
475 | 0 | ++currentYear_; |
476 | 0 | } |
477 | |
|
478 | 0 | emitCurrentPageChanged(); |
479 | 0 | renderMonth(); |
480 | 0 | } |
481 | | |
482 | | void WCalendar::monthChanged(int newMonth) |
483 | 0 | { |
484 | 0 | ++newMonth; |
485 | |
|
486 | 0 | if (currentMonth_ != newMonth |
487 | 0 | && (newMonth >= 1 && newMonth <= 12)) { |
488 | |
|
489 | 0 | currentMonth_ = newMonth; |
490 | |
|
491 | 0 | emitCurrentPageChanged(); |
492 | 0 | renderMonth(); |
493 | 0 | } |
494 | 0 | } |
495 | | |
496 | | void WCalendar::yearChanged(WString yearStr) |
497 | 0 | { |
498 | 0 | try { |
499 | 0 | int year = Utils::stoi(yearStr.toUTF8()); |
500 | |
|
501 | 0 | if (currentYear_ != year && |
502 | 0 | (year >= 1900 && year <= 2200)) { // ?? |
503 | 0 | currentYear_ = year; |
504 | |
|
505 | 0 | emitCurrentPageChanged(); |
506 | 0 | renderMonth(); |
507 | 0 | } |
508 | 0 | } catch (std::exception& e) { |
509 | 0 | } |
510 | 0 | } |
511 | | |
512 | | void WCalendar::setBottom(const WDate& bottom) |
513 | 0 | { |
514 | 0 | if (bottom_ != bottom) { |
515 | 0 | bottom_ = bottom; |
516 | 0 | renderMonth(); |
517 | 0 | } |
518 | 0 | } |
519 | | |
520 | | void WCalendar::setTop(const WDate& top) |
521 | 0 | { |
522 | 0 | if (top_ != top) { |
523 | 0 | top_ = top; |
524 | 0 | renderMonth(); |
525 | 0 | } |
526 | 0 | } |
527 | | } |