Tìm hiểu về gem fullcalendar và tạo lịch bằng datepicker

Tài liệu tham khảo:

http://arshaw.com/fullcalendar/

http://www.eyecon.ro/bootstrap-datepicker

Screenshot from 2015-07-23 08:02:46.png

Chắc các bạn ai cũng biết đến trang google calendar của google. Giao diện trang đó là các lịch biểu được thiết kế rất đẹp và tiện lơi. Bài viết này mình xin giới thiệu cách đơn giản để hiển thị và sử dụng các lịch biểu bằng việc sử dụng hai gem sau trong RoR là: fullcalendar và datepicker!

I, Gem Fullcalendar

1, Giới thiệu

Đây là một gem giúp hiển thị một lịch biểu lớn với nhiều view được tùy biến như: Day, Week, Month, 4Day...Ngoài ra cũng có thêm các button giúp di chuyển thời gian lịch hiển thị cũng như giúp hiển thị các event nào đó được đặt trên lịch.

2, Để cài đặt và sử dụng:

  • Thêm vào Gemfile trong ứng dụng của bạn:
gem 'fullcalendar-rails'

Trong file app/assets/javascripts/application.js thêm:

//= require fullcalendar

$(document).ready(function() {
  $('#calendar').fullCalendar()
  weekends: true,
  height: $(window).height() - 180,
  editable: true,
  header: {
    left: 'prev,next today',
    center: 'title',
    right: 'month,agendaWeek,agendaDay'
  },
  defaultView: 'month',
  eventLimit: true,
  keepOpen: false,
  selectable: true,
  selectHelper: true,
});
  • Trong file app/assets/stylesheets/application.css thêm:
 *= require fullcalendar
  • Sau đó tạo file: fullcalendar.css trong app/assets/stylesheets/ với code:
    .fc {
        direction: ltr;
        text-align: left;
        }
    .fc table {
        border-collapse: collapse;
        border-spacing: 0;
        }
    html .fc,
    .fc table {
        font-size: 1em;
        }
    .fc td,
    .fc th {
        padding: 0;
        vertical-align: top;
        }
    .fc-header td {
        white-space: nowrap;
        }
    .fc-header-left {
        width: 25%;
        text-align: left;
        }
    .fc-header-center {
        text-align: center;
        }
    .fc-header-right {
        width: 25%;
        text-align: right;
        }
    .fc-header-title {
        display: inline-block;
        vertical-align: top;
        }
    .fc-header-title h2 {
        margin-top: 0;
        white-space: nowrap;
        }
    .fc .fc-header-space {
        padding-left: 10px;
        }
    .fc-header .fc-button {
        margin-bottom: 1em;
        vertical-align: top;
        }
    .fc-header .fc-button {
        margin-right: -1px;
        }
    .fc-header .fc-corner-right,
    .fc-header .ui-corner-right {
        margin-right: 0;
        }
    .fc-header .fc-state-hover,
    .fc-header .ui-state-hover {
        z-index: 2;
        }
    .fc-header .fc-state-down {
        z-index: 3;
        }
    .fc-header .fc-state-active,
    .fc-header .ui-state-active {
        z-index: 4;
        }
    .fc-content {
        clear: both;
        zoom: 1;
        }
    .fc-view {
        width: 100%;
        overflow: hidden;
        }
    .fc-widget-header,
    .fc-widget-content {
        border: 1px solid #ddd;
        }
    .fc-state-highlight {
        background: #fcf8e3;
        }
    .fc-cell-overlay {
        background: #bce8f1;
        opacity: .3;
        filter: alpha(opacity=30);
        }
    .fc-button {
        position: relative;
        display: inline-block;
        padding: 0 .6em;
        overflow: hidden;
        height: 1.9em;
        line-height: 1.9em;
        white-space: nowrap;
        cursor: pointer;
        }
    .fc-state-default {
        border: 1px solid;
        }
    .fc-state-default.fc-corner-left {
        border-top-left-radius: 4px;
        border-bottom-left-radius: 4px;
        }
    .fc-state-default.fc-corner-right {
        border-top-right-radius: 4px;
        border-bottom-right-radius: 4px;
        }
    .fc-text-arrow {
        margin: 0 .1em;
        font-size: 2em;
        font-family: "Courier New", Courier, monospace;
        vertical-align: baseline;
        }
    .fc-button-prev .fc-text-arrow,
    .fc-button-next .fc-text-arrow {
        font-weight: bold;
        }
    .fc-button .fc-icon-wrap {
        position: relative;
        float: left;
        top: 50%;
        }
    .fc-button .ui-icon {
        position: relative;
        float: left;
        margin-top: -50%;
        *margin-top: 0;
        *top: -50%;
        }
    .fc-state-default {
        background-color: #f5f5f5;
        background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
        background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
        background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
        background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
        background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
        background-repeat: repeat-x;
        border-color: #e6e6e6 #e6e6e6 #bfbfbf;
        border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
        color: #333;
        text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
        }
    .fc-state-hover,
    .fc-state-down,
    .fc-state-active,
    .fc-state-disabled {
        color: #333333;
        background-color: #e6e6e6;
        }
    .fc-state-hover {
        color: #333333;
        text-decoration: none;
        background-position: 0 -15px;
        -webkit-transition: background-position 0.1s linear;
           -moz-transition: background-position 0.1s linear;
             -o-transition: background-position 0.1s linear;
                transition: background-position 0.1s linear;
        }
    .fc-state-down,
    .fc-state-active {
        background-color: #cccccc;
        background-image: none;
        outline: 0;
        box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
        }

    .fc-state-disabled {
        cursor: default;
        background-image: none;
        opacity: 0.65;
        filter: alpha(opacity=65);
        box-shadow: none;
        }
    .fc-event-container > * {
        z-index: 8;
        }
    .fc-event-container > .ui-draggable-dragging,
    .fc-event-container > .ui-resizable-resizing {
        z-index: 9;
        }
    .fc-event {
        border: 1px solid #3a87ad;
        background-color: #3a87ad;
        color: #fff;
        font-size: .85em;
        cursor: default;
        }
    a.fc-event {
        text-decoration: none;
        }
    a.fc-event,
    .fc-event-draggable {
        cursor: pointer;
        }
    .fc-rtl .fc-event {
        text-align: right;
        }
    .fc-event-inner {
        width: 100%;
        height: 100%;
        overflow: hidden;
        }
    .fc-event-time,
    .fc-event-title {
        padding: 0 1px;
        }
    .fc .ui-resizable-handle {
        display: block;
        position: absolute;
        z-index: 99999;
        overflow: hidden;
        font-size: 300%;
        line-height: 50%;
        }
    .fc-event-hori {
        border-width: 1px 0;
        margin-bottom: 1px;
        }
    .fc-ltr .fc-event-hori.fc-event-start,
    .fc-rtl .fc-event-hori.fc-event-end {
        border-left-width: 1px;
        border-top-left-radius: 3px;
        border-bottom-left-radius: 3px;
        }
    .fc-ltr .fc-event-hori.fc-event-end,
    .fc-rtl .fc-event-hori.fc-event-start {
        border-right-width: 1px;
        border-top-right-radius: 3px;
        border-bottom-right-radius: 3px;
        }
    .fc-event-hori .ui-resizable-e {
        top: 0           !important; /* importants override pre jquery ui 1.7 styles */
        right: -3px      !important;
        width: 7px       !important;
        height: 100%     !important;
        cursor: e-resize;
        }
    .fc-event-hori .ui-resizable-w {
        top: 0           !important;
        left: -3px       !important;
        width: 7px       !important;
        height: 100%     !important;
        cursor: w-resize;
        }
    .fc-event-hori .ui-resizable-handle {
        _padding-bottom: 14px;
        }
    table.fc-border-separate {
        border-collapse: separate;
        }
    .fc-border-separate th,
    .fc-border-separate td {
        border-width: 1px 0 0 1px;
        }
    .fc-border-separate th.fc-last,
    .fc-border-separate td.fc-last {
        border-right-width: 1px;
        }
    .fc-border-separate tr.fc-last th,
    .fc-border-separate tr.fc-last td {
        border-bottom-width: 1px;
        }
    .fc-border-separate tbody tr.fc-first td,
    .fc-border-separate tbody tr.fc-first th {
        border-top-width: 0;
        }
    .fc-grid th {
        text-align: center;
        }
    .fc .fc-week-number {
        width: 22px;
        text-align: center;
        }
    .fc .fc-week-number div {
        padding: 0 2px;
        }
    .fc-grid .fc-day-number {
        float: right;
        padding: 0 2px;
        }
    .fc-grid .fc-other-month .fc-day-number {
        opacity: 0.3;
        filter: alpha(opacity=30);
        }
    .fc-grid .fc-day-content {
        clear: both;
        padding: 2px 2px 1px; /* distance between events and day edges */
        }
    .fc-grid .fc-event-time {
        font-weight: bold;
        }
    .fc-rtl .fc-grid .fc-day-number {
        float: left;
        }
    .fc-rtl .fc-grid .fc-event-time {
        float: right;
        }
    .fc-agenda table {
        border-collapse: separate;
        }
    .fc-agenda-days th {
        text-align: center;
        }
    .fc-agenda .fc-agenda-axis {
        width: 50px;
        padding: 0 4px;
        vertical-align: middle;
        text-align: right;
        white-space: nowrap;
        font-weight: normal;
        }
    .fc-agenda .fc-week-number {
        font-weight: bold;
        }
    .fc-agenda .fc-day-content {
        padding: 2px 2px 1px;
        }
    .fc-agenda-days .fc-agenda-axis {
        border-right-width: 1px;
        }
    .fc-agenda-days .fc-col0 {
        border-left-width: 0;
        }
    .fc-agenda-allday th {
        border-width: 0 1px;
        }
    .fc-agenda-allday .fc-day-content {
        min-height: 34px;
        _height: 34px;
        }
    .fc-agenda-divider-inner {
        height: 2px;
        overflow: hidden;
        }
    .fc-widget-header .fc-agenda-divider-inner {
        background: #eee;
        }
    .fc-agenda-slots th {
        border-width: 1px 1px 0;
        }
    .fc-agenda-slots td {
        border-width: 1px 0 0;
        background: none;
        }

    .fc-agenda-slots td div {
        height: 20px;
        }

    .fc-agenda-slots tr.fc-slot0 th,
    .fc-agenda-slots tr.fc-slot0 td {
        border-top-width: 0;
        }
    .fc-agenda-slots tr.fc-minor th,
    .fc-agenda-slots tr.fc-minor td {
        border-top-style: dotted;
        }
    .fc-agenda-slots tr.fc-minor th.ui-widget-header {
        border-top-style: solid;
        }
    .fc-event-vert {
        border-width: 0 1px;
        }
    .fc-event-vert.fc-event-start {
        border-top-width: 1px;
        border-top-left-radius: 3px;
        border-top-right-radius: 3px;
        }
    .fc-event-vert.fc-event-end {
        border-bottom-width: 1px;
        border-bottom-left-radius: 3px;
        border-bottom-right-radius: 3px;
        }
    .fc-event-vert .fc-event-time {
        white-space: nowrap;
        font-size: 10px;
        }
    .fc-event-vert .fc-event-inner {
        position: relative;
        z-index: 2;
        }
    .fc-event-vert .fc-event-bg {
        position: absolute;
        z-index: 1;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: #fff;
        opacity: .25;
        filter: alpha(opacity=25);
        }
    .fc .ui-draggable-dragging .fc-event-bg,
    .fc-select-helper .fc-event-bg {
        display: none\9;
        }
    .fc-event-vert .ui-resizable-s {
        bottom: 0        !important;
        width: 100%      !important;
        height: 8px      !important;
        overflow: hidden !important;
        line-height: 8px !important;
        font-size: 11px  !important;
        font-family: monospace;
        text-align: center;
        cursor: s-resize;
        }
    .fc-agenda .ui-resizable-resizing {
        _overflow: hidden;
        }
  • Trong file view, chẳng hạn: app/views/static_pages/home.html.erb
<% if user_signed_in? %>
  <div class="col-md-9">
    <div id='calendar'></div>
  </div>
  <div class="col-md-3">

  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Sign up please!</h1>
      <%= link_to "Sign up now!", new_user_registration_path, class: "btn btn-lg btn-primary" %>
  </div>
<% end %>
  • Có thể tùy chỉnh các view của calendar, chẳng hạn để tạo thêm 1 view 4Days ta làm như sau:
    • Trong block calendar trong file apllication.js, phần header, ta khai báo thêm một nút 4Days trong phần right
    • Sau đó custom view cho view 4Days
right: 'month,agendaWeek,agendaFourDay,agendaDay'
header: {
  left: 'prev,next today',
  center: 'title',
  right: 'month,agendaWeek,agendaFourDay,agendaDay'
},
  views: {
    agendaFourDay: {
      type: 'agenda',
      duration: { days: 4 },
      buttonText: '4 days'
    }
},

3, Một số tính năng:

  • Để tạo event khi click vào một ngày nào đó trên calendar, trong block calendar trong file js:
dayClick: function(date, jsEvent, view) {
//code một event
}
  • Để tạo event bằng cách kéo thả để chọn time trong view Day hay Week:
select: function (start, end, jsEvent, view) {
  //code của bạn
}
  • Để hiển thị event bất kì nào đó lên calendar, trong block calendar trong file js, add thêm:
events: [
  {
  title: 'All Day Event',
  start: '2015-02-01'
  },
  {
  title: 'Long Event',
  start: '2015-02-07',
  end: '2015-02-10'
  },
  {
  id: 999,
  title: 'Repeating Event',
  start: '2015-02-09T16:00:00'
  },
  {
  id: 999,
  title: 'Repeating Event',
  start: '2015-02-16T16:00:00'
  },
  {
  title: 'Conference',
  start: '2015-02-11',
  end: '2015-02-13'
  },
  {
  title: 'Meeting',
  start: '2015-02-12T10:30:00',
  end: '2015-02-12T12:30:00'
  },
  {
  title: 'Lunch',
  start: '2015-02-12T12:00:00'
  }
]
  • Để hiển thị trực tiếp các event từ database ta làm như sau: Giả sử bạn đã có sẵn model Schedules trong database với các thuộc tính như: title, start_time, finish_time, description...
    • Trong controller:
def index
  @schedules = Schedule.all
    render json: {schedules: @schedules.as_json}
    respond_to do |format|
      format.html
      format.json {render json: {schedules: @schedules.as_json}}
    end
end
  • Sau đó trong block calendar trong file js, ta thêm vào:
events: function(start, end, timezone, callback) {
  $.ajax({
    url: '/schedules.json',
    type: 'GET',
    success: function(doc) {
      var events = [];
      if(doc.schedules){
        $.map(doc.schedules, function(schedule) {
          events.push({
            id: schedule.id,
            title: schedule.title,
            start: schedule.start_time,
            end: schedule.finish_time
          });
        });
      }
    callback(events);
    }
  });
}

II, Tạo lịch bằng datepicker

1, Datepicker giúp hiển thị time dưới dạng cuốn lịch nhỏ, bạn có thể tra thứ, ngày, tháng, năm tại một ngày bất kì nào đó một cách nhanh chóng

2, Cài đặt

  • Trong file application.js thêm:
//= require bootstrap-datepicker
  • Tạo file: app/assets/javascripts/bootstrap-datepicker.js, file app/assets/stylesheets/bootstrap-datepicker.css với nội dung trong file zip được download từ: http://www.eyecon.ro/bootstrap-datepicker/

  • Trong file app/assets/stylesheets/application.scss:

*= require bootstrap-datepicker
  • Trong view, khai báo hiển thị mini calendar: app/views/pages/home.html.erb
<div class="col-md-3">
  <div id='mini-calendar'></div>
</div>
  • Hiển thị mini-calendar và tương tác với main calendar

    • Để hiển thị mini-calendar ta thiết lập trong file application.js với các thuộc tính như: inline, sideBySide, todayHightLight, showButtonPanel,...

    • Để tương tác giữa mini-calendar và calendar có nhiều cách, giả sử khi click vào một ngày bất kì nào đó trên mini-calendar thì calendar sẽ chuyển sang view của đúng ngày đó:

$('#mini-calendar').datepicker({
    inline: true,
    sideBySide: true,
    todayHighlight: true,
    showButtonPanel: true,
  }).on('changeDate', function(ev){
    $('#calendar').fullCalendar('gotoDate', new Date(Date.parse(ev.date)));
    $('#calendar').fullCalendar('changeView','month');
    $('#calendar').fullCalendar('changeView','agendaDay');
  });

III, Lời kết

Trên đây là điều cơ bản của hai gem fullcalendar và datepicker Do giới hạn của bài viết nên có rất nhiều tính năng khác của fullcalendar và datepicker mà mình chưa đề cập trong bài viết này.

Đây là lần đầu mình viết bài trên viblo.asia. Không tránh khỏi các khiếm khuyết, rất mong sự góp ý của các bạn. Thanks for read!


All Rights Reserved