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