silverbullet/Library/Widgets/Calendar.md
2025-11-17 10:32:46 +01:00

6.3 KiB


function generate_calendar(calendarID, year, month)
  local month_names =  {
    "Januari",   "Februari", "Mars",     "April",
    "Maj",       "Juni",     "Juli",     "Augusti",
    "September", "Oktober",  "November", "December"
  }
  local day_names = {"Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"}
  
  -- A specified year and month will always display that calendar.
  if year == 0 and month == 0 then
    local wantedMonth = clientStore.get("calendar:" .. calendarID)
    editor.flashNotification(wantedMonth)
    -- Calculate year and month from the current date plus a month offset.
    if wantedMonth == nil then
      wantedMonth = tonumber(os.date("%Y")) * 12 + (tonumber(os.date("%m"))-1)
    end
    year = (wantedMonth - (wantedMonth % 12)) / 12
    month = wantedMonth % 12 + 1
  end

  local events = query[[
    from index.tag "event"
    where
      _.at.match(string.format("^%d-%02d-", year, month))
    order by _.at
  ]]

  local days = get_days_in_month(year, month)

  -- Get the first day of the month (1 = Sunday, 2 = Monday, ..., 7 = Saturday)
  local first_day = tonumber(os.date("%w", os.time{year=year, month=month, day=1})) + 1

  -- Adjust the first day based on the week start setting
  first_day = first_day - 1
  if first_day == 0 then
      first_day = 7
  end

  local html = [[
    <table border='1'>
      <tr>
        <th colspan='7'>
          <span class="month-prev">&lt;</span>
          <span class="month-now">Idag</span>
          <span class="month-next">&gt;</span>
     ]] .. month_names[month] .. " " .. year .. [[ 
        </th>
      </tr>
  ]]
  
  html = html .. "<tr>"
  for _, day_name in ipairs(day_names) do
      html = html .. "<th>" .. day_name .. "</th>"
  end
  html = html .. "</tr>\n"

  local day = 1
  local started = false
  for week = 1, 6 do
      html = html .. "<tr>"
      for weekday = 1, 7 do
          if not started and weekday == first_day then
              started = true
          end
          if started and day <= days then
              local cellcontent = ""
              cellcontent = day
            
              for _, e in pairs(events) do
                local datematch = e.at.match(
                  string.format("^%d-%02d-%02d( +[0-9]+:[0-9]+)?", year, month, day))
                if datematch and #datematch >= 2 and not datematch[2] then
                    cellcontent = cellcontent .. string.format(
                      [[<a href="%s"><div class="event all-day">%s</div></a>]],
                      e.ref, e.name
                  )
                elseif datematch and #datematch >= 2 and datematch[2] then
                  cellcontent = cellcontent .. string.format(
                    [[
                      <a href="%s">
                        <div class="event">
                           <div class="time">%s</div>
                            <div>%s</div>
                        </div>
                      </a>
                    ]],
                    e.ref,
                    datematch[2],
                    e.name
                  )
                end
              end

              -- Mark today's date
              if day == tonumber(os.date("%d")) and month == tonumber(os.date("%m")) and year == tonumber(os.date("%Y")) then
                  html = html .. string.format([[<td class="today">%s</td>]], cellcontent)
              else
                  html = html .. string.format([[<td>%s</td>]], cellcontent)
              end

              day = day + 1
          else
              html = html .. "<td></td>"
          end
      end
      html = html .. "</tr>"
      if day > days then
          break
      end
  end

  html = html .. "</table>"

  local tmpl = js.window.document.createElement("template")
  tmpl.innerHTML = html
  
  tmpl.content.querySelector('.month-prev').addEventListener('click', function()
    storeMonth(calendarID, year, month, -1)
    codeWidget.refreshAll()
  end)
  
  tmpl.content.querySelector('.month-next').addEventListener('click', function()
    storeMonth(calendarID, year, month, 1)
    codeWidget.refreshAll()
  end)
  
  tmpl.content.querySelector('.month-now').addEventListener('click', function()
    clientStore.delete("calendar:" .. calendarID)
    storeMonth(calendarID, tonumber(os.date("%Y")), tonumber(os.date("%m")), 0)
    codeWidget.refreshAll()
  end)

  return widget.new{
      html = tmpl.content,
      display = "block",
      cssClasses = {"calendartable"}
  }
end

function storeMonth(calendarID, year, month, offset)
    local storedMonth = clientStore.get("calendar:" .. calendarID)
    if storedMonth == nil then
      storedMonth = year * 12 + month
    end
    clientStore.set("calendar:" .. calendarID, storedMonth  + offset)
    codeWidget.refreshAll()
end

-- Local function to get the correct number of days in a month (handles leap years)
local function get_days_in_month(year, month)
    local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    
    -- Check for leap year and adjust February
    if month == 2 and ((year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)) then
        return 29
    end
    
    return days_in_month[month]
end    
.calendartable {
  border-collapse: collapse; 

  table {
    border-color: var(--panel-border-color);
    border: 2px solid #444;
    border-collapse: collapse;
    
    th {
      width: 14%;
      padding: 3px; 
      text-align: left;  
      background-color: var(--panel-background-color);
      color: #852624;
      border: 1px solid #aaa;
    }
    
    td {
      width: 14%;
      padding: 3px; 
      text-align: left;
      vertical-align: top;
      border: 1px solid #aaa;
    
      &.today {
      background-color: #f6f3c2;
      }
    }
  }
  
  a {
    text-decoration-line: none;
    color: var(--root-color);
    
    &.mark {
      text-decoration-line: none;
      color: var(--ui-accent-text-color);
    }
  }
  
  span.extramarker {
    background-color: yellow;
  }
  
  .event {
    background-color: #2ca05a;
    color: #fff;
    border-radius: 4px;
    padding: 4px 8px;
    margin-top: 8px;
    max-width: 200px;
    white-space: normal;
    overflow-wrap: normal !important;
    word-break: normal !important;
    
    &.all-day {
      background-color: #852624;
    }

    .time {
      font-weight: bold;
    }
  }
  
  .month-prev,
  .month-next,
  .month-now {
    cursor: pointer;
  }
}