simple-calendar-js 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SimpleCalendarJs v3.0.0 — simple-calendar-js.css
3
+ * A clean, modern, and feature-rich JavaScript calendar component with zero dependencies
4
+ *
5
+ * @author Pedro Lopes <simplecalendarjs@gmail.com>
6
+ * @homepage https://www.simplecalendarjs.com
7
+ * @license SEE LICENSE IN LICENSE
8
+ * @repository https://github.com/pclslopes/SimpleCalendarJs
9
+ *
10
+ * All styles scoped under .uc-calendar to prevent leaking.
11
+ * Override any --cal-* variable in :root or on .uc-calendar.
12
+ */
13
+ :root{--cal-bg:#ffffff;--cal-text:#111827;--cal-text-subtle:#6b7280;--cal-text-muted:#9ca3af;--cal-border:#e5e7eb;--cal-border-strong:#d1d5db;--cal-primary:#4f46e5;--cal-primary-dark:#4338ca;--cal-primary-light:#eef2ff;--cal-event-bg:var(--cal-primary);--cal-event-text:#ffffff;--cal-event-border-radius:3px;--cal-today-bg:#eef2ff;--cal-today-text:var(--cal-primary);--cal-hover:#f9fafb;--cal-hover-strong:#f3f4f6;--cal-selected-bg:#ede9fe;--cal-font-family:inherit;--cal-font-size:13px;--cal-time-col-width:64px;--cal-hour-height:60px;--cal-cell-min-height:112px;--cal-event-height:22px;--cal-event-gap:2px;--cal-header-day-height:30px;--cal-day-name-height:36px;--cal-now-color:#ef4444;--cal-toolbar-bg:var(--cal-bg);--cal-radius:8px;--cal-shadow:0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.06);--cal-transition:150ms ease}.uc-calendar{font-family:var(--cal-font-family);font-size:var(--cal-font-size);color:var(--cal-text);background:var(--cal-bg);border:1px solid var(--cal-border);border-radius:var(--cal-radius);overflow:hidden;display:flex;flex-direction:column;position:relative;min-height:500px;-webkit-font-smoothing:antialiased}.uc-calendar *,.uc-calendar ::after,.uc-calendar ::before{box-sizing:border-box;margin:0;padding:0}.uc-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;background:var(--cal-toolbar-bg);border-bottom:1px solid var(--cal-border);flex-shrink:0;flex-wrap:wrap}.uc-toolbar-section{display:flex;align-items:center;gap:4px}.uc-toolbar-center{flex:1;display:flex;justify-content:center;position:relative}.uc-title{font-size:15px;font-weight:600;color:var(--cal-text);white-space:nowrap;letter-spacing:-.01em;display:flex;align-items:baseline;gap:5px}.uc-title-main{text-transform:capitalize}.uc-year-btn{background:0 0;border:none;font:inherit;font-weight:600;font-size:15px;color:var(--cal-primary);cursor:pointer;padding:1px 4px;border-radius:4px;line-height:inherit;text-decoration:underline;text-decoration-style:dotted;text-underline-offset:2px;transition:background var(--cal-transition),color var(--cal-transition)}.uc-year-btn.uc-open,.uc-year-btn:hover{background:var(--cal-primary-light);text-decoration:none}.uc-year-picker{position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);background:var(--cal-bg);border:1px solid var(--cal-border);border-radius:10px;box-shadow:0 4px 24px rgba(0,0,0,.12);padding:10px;z-index:200;min-width:210px}.uc-year-picker-nav{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}.uc-year-range{font-size:12px;font-weight:600;color:var(--cal-text-subtle)}.uc-year-nav-btn{background:0 0;border:none;font-size:18px;line-height:1;color:var(--cal-text);cursor:pointer;padding:2px 7px;border-radius:5px;font-family:inherit;transition:background var(--cal-transition)}.uc-year-nav-btn:hover{background:var(--cal-hover-strong)}.uc-year-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}.uc-year-item{background:0 0;border:none;border-radius:6px;font-size:13px;font-weight:500;font-family:inherit;color:var(--cal-text);cursor:pointer;padding:7px 4px;text-align:center;transition:background var(--cal-transition),color var(--cal-transition)}.uc-year-item:hover{background:var(--cal-hover-strong)}.uc-year-item.uc-today-year{color:var(--cal-primary);font-weight:700}.uc-year-item.uc-active{background:var(--cal-primary);color:#fff;font-weight:700}.uc-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;border:1px solid var(--cal-border);background:var(--cal-bg);color:var(--cal-text);border-radius:6px;padding:5px 10px;font-size:13px;font-family:inherit;font-weight:500;cursor:pointer;white-space:nowrap;line-height:1.4;transition:background var(--cal-transition),border-color var(--cal-transition),color var(--cal-transition);user-select:none}.uc-btn:hover{background:var(--cal-hover-strong);border-color:var(--cal-border-strong)}.uc-btn:active{background:var(--cal-selected-bg)}.uc-btn:focus-visible{outline:2px solid var(--cal-primary);outline-offset:2px}.uc-nav-btn{font-size:18px;padding:4px 8px;line-height:1;border-color:transparent;background:0 0}.uc-nav-btn:hover{border-color:var(--cal-border)}.uc-today-btn{padding:5px 12px}.uc-today-btn.uc-active{background:var(--cal-primary-light);color:var(--cal-primary);font-weight:600;border-color:transparent}.uc-view-switcher{display:flex;border:1px solid var(--cal-border);border-radius:6px;overflow:hidden}.uc-view-btn{border:none;border-radius:0;border-right:1px solid var(--cal-border);background:0 0;color:var(--cal-text-subtle);font-weight:500;padding:5px 11px}.uc-view-btn:last-child{border-right:none}.uc-view-btn:hover{background:var(--cal-hover-strong);color:var(--cal-text);border-color:transparent}.uc-view-btn.uc-active{background:var(--cal-primary-light);color:var(--cal-primary);font-weight:600}.uc-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.7);z-index:100;pointer-events:none}.uc-spinner{width:28px;height:28px;border:3px solid var(--cal-border);border-top-color:var(--cal-primary);border-radius:50%;animation:uc-spin .6s linear infinite}@keyframes uc-spin{to{transform:rotate(360deg)}}.uc-view-container{flex:1;overflow:hidden;display:flex;flex-direction:column}.uc-month-view{display:flex;flex-direction:column;flex:1;overflow:hidden}.uc-month-header{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--cal-border);flex-shrink:0}.uc-month-day-name{height:var(--cal-day-name-height,36px);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--cal-text-subtle);user-select:none}.uc-month-body{flex:1;display:flex;flex-direction:column;overflow:hidden}.uc-week-row{flex:1;position:relative;min-height:var(--cal-cell-min-height);border-bottom:1px solid var(--cal-border)}.uc-week-row:last-child{border-bottom:none}.uc-week-cells{position:absolute;inset:0;display:grid;grid-template-columns:repeat(7,1fr);z-index:1}.uc-day-cell{border-right:1px solid var(--cal-border);padding:4px 6px 4px 6px;cursor:pointer;transition:background var(--cal-transition);min-height:var(--cal-cell-min-height)}.uc-day-cell:last-child{border-right:none}.uc-day-cell:hover{background:var(--cal-hover)}.uc-day-cell.uc-today{background:var(--cal-today-bg)}.uc-day-cell.uc-other-month .uc-day-number{color:var(--cal-text-muted)}.uc-day-number{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;font-size:13px;font-weight:500;border-radius:50%;color:var(--cal-text);cursor:pointer;transition:background var(--cal-transition),color var(--cal-transition);line-height:1}.uc-day-number:hover{background:var(--cal-hover-strong)}.uc-today .uc-day-number{background:var(--cal-primary);color:#fff;font-weight:700}.uc-today .uc-day-number:hover{background:var(--cal-primary-dark)}.uc-week-events{position:absolute;inset:0;z-index:2;pointer-events:none;overflow:hidden}.uc-event-bar{position:absolute;height:var(--cal-event-height);background:var(--cal-event-bg);color:var(--cal-event-text);border-radius:var(--cal-event-border-radius);display:flex;align-items:center;gap:4px;padding:0 6px;overflow:hidden;cursor:pointer;pointer-events:auto;white-space:nowrap;font-size:12px;font-weight:500;transition:filter var(--cal-transition),opacity var(--cal-transition);z-index:3}.uc-event-bar:hover{filter:brightness(.92)}.uc-event-bar:active{filter:brightness(.85)}.uc-event-bar.uc-continues-left{border-top-left-radius:0;border-bottom-left-radius:0}.uc-event-bar.uc-continues-right{border-top-right-radius:0;border-bottom-right-radius:0}.uc-event-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.uc-event-time{font-size:11px;opacity:.85;flex-shrink:0}.uc-more-link{position:absolute;height:var(--cal-event-height);display:flex;align-items:center;padding:0 6px;font-size:11px;font-weight:600;color:var(--cal-text-subtle);cursor:pointer;pointer-events:auto;border-radius:var(--cal-event-border-radius);white-space:nowrap;transition:background var(--cal-transition),color var(--cal-transition);z-index:3}.uc-more-link:hover{background:var(--cal-hover-strong);color:var(--cal-text)}.uc-day-view,.uc-week-view{display:flex;flex-direction:column;flex:1;overflow:hidden}.uc-week-header{display:flex;border-bottom:1px solid var(--cal-border);flex-shrink:0;background:var(--cal-bg)}.uc-time-gutter-spacer{width:var(--cal-time-col-width);flex-shrink:0;border-right:1px solid var(--cal-border)}.uc-all-day-label{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--cal-text-muted);white-space:nowrap}.uc-week-day-headers{flex:1;display:grid;grid-template-columns:repeat(7,1fr)}.uc-week-day-headers.uc-day-header-single{grid-template-columns:1fr}.uc-week-day-header{padding:8px 4px;display:flex;flex-direction:column;align-items:center;gap:2px;border-right:1px solid var(--cal-border);cursor:default}.uc-week-day-header:last-child{border-right:none}.uc-week-day-name{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--cal-text-subtle);user-select:none}.uc-week-day-num{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;font-size:16px;font-weight:600;border-radius:50%;color:var(--cal-text);cursor:pointer;transition:background var(--cal-transition),color var(--cal-transition)}.uc-week-day-num:hover{background:var(--cal-hover-strong)}.uc-week-day-header.uc-today .uc-week-day-name{color:var(--cal-primary)}.uc-week-day-header.uc-today .uc-week-day-num{background:var(--cal-primary);color:#fff}.uc-week-day-header.uc-today .uc-week-day-num:hover{background:var(--cal-primary-dark)}.uc-all-day-section{display:flex;border-bottom:1px solid var(--cal-border);flex-shrink:0;background:var(--cal-bg)}.uc-all-day-events{flex:1;position:relative;min-height:calc(var(--cal-event-height) + 6px);padding:2px 0}.uc-time-body{flex:1;overflow-y:auto;overflow-x:hidden;position:relative}.uc-time-body::-webkit-scrollbar{width:6px}.uc-time-body::-webkit-scrollbar-track{background:0 0}.uc-time-body::-webkit-scrollbar-thumb{background:var(--cal-border-strong);border-radius:3px}.uc-time-grid-inner{display:flex;flex-direction:row;height:calc(24 * var(--cal-hour-height));position:relative}.uc-time-gutter{width:var(--cal-time-col-width);flex-shrink:0;border-right:1px solid var(--cal-border);position:relative}.uc-hour-cell{height:var(--cal-hour-height);position:relative;display:flex;align-items:flex-start;justify-content:flex-end;padding-right:8px}.uc-hour-label{font-size:11px;font-weight:500;color:var(--cal-text-subtle);user-select:none;pointer-events:none;white-space:nowrap;transform:translateY(-50%);margin-top:1px}.uc-time-columns{flex:1;display:grid;grid-template-columns:repeat(var(--uc-col-count,7),1fr);position:relative}.uc-time-col{position:relative;border-right:1px solid var(--cal-border);height:calc(24 * var(--cal-hour-height));cursor:pointer}.uc-time-col:last-child{border-right:none}.uc-time-col.uc-today{background:var(--cal-today-bg)}.uc-hour-row{height:var(--cal-hour-height);border-bottom:1px solid var(--cal-border);position:relative;pointer-events:none}.uc-half-hour-line{position:absolute;top:50%;left:0;right:0;border-top:1px dashed var(--cal-border);pointer-events:none}.uc-timed-event{position:absolute;background:var(--cal-event-bg);color:var(--cal-event-text);border-radius:var(--cal-event-border-radius);padding:3px 6px;overflow:hidden;cursor:pointer;display:flex;flex-direction:column;gap:1px;font-size:12px;font-weight:500;border-left:3px solid rgba(0,0,0,.15);transition:filter var(--cal-transition),box-shadow var(--cal-transition);z-index:2;min-height:18px}.uc-timed-event:hover{filter:brightness(.92);box-shadow:0 2px 8px rgba(0,0,0,.15);z-index:4}.uc-timed-event .uc-event-title{font-weight:600;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uc-timed-event .uc-event-time{font-size:11px;opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.uc-timed-event--short{flex-direction:row;align-items:center;gap:4px}.uc-timed-event--short .uc-event-time{flex-shrink:0}.uc-timed-event--short .uc-event-title{flex:1;min-width:0}.uc-now-indicator{position:absolute;left:0;right:0;pointer-events:none;z-index:10;display:flex;align-items:center}.uc-now-dot{width:10px;height:10px;border-radius:50%;background:var(--cal-now-color);flex-shrink:0;margin-left:-5px}.uc-now-line{flex:1;height:2px;background:var(--cal-now-color)}.uc-time-col:hover{background:var(--cal-hover)}.uc-time-col.uc-today:hover{background:color-mix(in srgb,var(--cal-today-bg) 80%,var(--cal-hover-strong))}@supports not (color:color-mix(in srgb,red 50%,blue)){.uc-time-col.uc-today:hover{background:var(--cal-today-bg)}}@media (max-width:768px){.uc-toolbar{padding:8px 10px}.uc-title{font-size:14px}.uc-toolbar-center{order:-1;width:100%;flex:none}.uc-toolbar-section{justify-content:center}.uc-view-btn{padding:5px 8px;font-size:12px}:root{--cal-time-col-width:52px;--cal-hour-height:52px;--cal-cell-min-height:88px;--cal-event-height:20px}.uc-week-day-num{width:24px;height:24px;font-size:13px}.uc-week-day-name{font-size:10px}.uc-hour-cell:nth-child(odd) .uc-hour-label{visibility:hidden}}@media (max-width:480px){.uc-today-btn{display:none}.uc-toolbar-section.uc-toolbar-right{gap:2px}}.uc-calendar.uc-no-grid .uc-day-cell,.uc-calendar.uc-no-grid .uc-day-header-row,.uc-calendar.uc-no-grid .uc-day-name-row,.uc-calendar.uc-no-grid .uc-grid-line,.uc-calendar.uc-no-grid .uc-hour-cell,.uc-calendar.uc-no-grid .uc-time-col,.uc-calendar.uc-no-grid .uc-time-gutter,.uc-calendar.uc-no-grid .uc-week-day-header,.uc-calendar.uc-no-grid .uc-week-row{border:none!important}.uc-calendar.uc-no-grid .uc-all-day-section{border-top:none!important;border-left:none!important;border-right:none!important}.uc-calendar.uc-dark,.uc-dark .uc-calendar{--cal-bg:#1f2937;--cal-text:#f9fafb;--cal-text-subtle:#9ca3af;--cal-text-muted:#6b7280;--cal-border:#374151;--cal-border-strong:#4b5563;--cal-hover:#374151;--cal-hover-strong:#4b5563;--cal-selected-bg:#312e81;--cal-today-bg:#1e1b4b;--cal-primary-light:#1e1b4b;--cal-toolbar-bg:#111827}@media print{.uc-toolbar{display:none}.uc-time-body{overflow:visible}.uc-calendar{border:none}}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * SimpleCalendarJs v3.0.0
3
+ * A clean, modern, and feature-rich JavaScript calendar component with zero dependencies
4
+ *
5
+ * @author Pedro Lopes <simplecalendarjs@gmail.com>
6
+ * @homepage https://www.simplecalendarjs.com
7
+ * @license SEE LICENSE IN LICENSE
8
+ * @repository https://github.com/pclslopes/SimpleCalendarJs
9
+ */
10
+ !function(t,e){"undefined"!=typeof module&&module.exports?module.exports=e():"function"==typeof define&&define.amd?define([],e):t.SimpleCalendarJs=e()}("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:this,function(){"use strict";function t(t){const e=new Date(t);return e.setHours(0,0,0,0),e}function e(t){const e=new Date(t);return e.setHours(23,59,59,999),e}function a(t,e){const a=new Date(t);return a.setDate(a.getDate()+e),a}function n(t,e){return t.getFullYear()===e.getFullYear()&&t.getMonth()===e.getMonth()&&t.getDate()===e.getDate()}function s(t){return n(t,new Date)}function i(e,a){return Math.floor((t(a)-t(e))/864e5)}function o(t){return t instanceof Date?t:new Date(t)}function r(e,n){const s=e.getDay(),i=t(a(e,-((s-n+7)%7)));return Array.from({length:7},(t,e)=>a(i,e))}function c(t,e,n){const s=new Date(t,e,1),i=new Date(t,e+1,0),o=(s.getDay()-n+7)%7,r=7*Math.ceil((o+i.getDate())/7),c=a(s,-o);return Array.from({length:r},(t,e)=>a(c,e))}function l(t,e,a){const n={hour:"numeric",hour12:!a};return 0!==t.getMinutes()&&(n.minute="2-digit"),new Intl.DateTimeFormat(e,n).format(t)}function d(t,e,a){return Array.from({length:7},(n,s)=>{const i=new Date(2025,0,5+(e+s)%7);return new Intl.DateTimeFormat(t,{weekday:a}).format(i)})}function h(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}const u={"en-US":{today:"Today",month:"Month",week:"Week",day:"Day"},"en-GB":{today:"Today",month:"Month",week:"Week",day:"Day"},"es-ES":{today:"Hoy",month:"Mes",week:"Semana",day:"Día"},"es-MX":{today:"Hoy",month:"Mes",week:"Semana",day:"Día"},"fr-FR":{today:"Aujourd'hui",month:"Mois",week:"Semaine",day:"Jour"},"fr-CA":{today:"Aujourd'hui",month:"Mois",week:"Semaine",day:"Jour"},"de-DE":{today:"Heute",month:"Monat",week:"Woche",day:"Tag"},"it-IT":{today:"Oggi",month:"Mese",week:"Settimana",day:"Giorno"},"pt-PT":{today:"Hoje",month:"Mês",week:"Semana",day:"Dia"},"pt-BR":{today:"Hoje",month:"Mês",week:"Semana",day:"Dia"},"nl-NL":{today:"Vandaag",month:"Maand",week:"Week",day:"Dag"},"pl-PL":{today:"Dzisiaj",month:"Miesiąc",week:"Tydzień",day:"Dzień"},"ru-RU":{today:"Сегодня",month:"Месяц",week:"Неделя",day:"День"},"tr-TR":{today:"Bugün",month:"Ay",week:"Hafta",day:"Gün"},"sv-SE":{today:"Idag",month:"Månad",week:"Vecka",day:"Dag"},"da-DK":{today:"I dag",month:"Måned",week:"Uge",day:"Dag"},"fi-FI":{today:"Tänään",month:"Kuukausi",week:"Viikko",day:"Päivä"},"no-NO":{today:"I dag",month:"Måned",week:"Uke",day:"Dag"},"cs-CZ":{today:"Dnes",month:"Měsíc",week:"Týden",day:"Den"},"hu-HU":{today:"Ma",month:"Hónap",week:"Hét",day:"Nap"},"ro-RO":{today:"Astăzi",month:"Lună",week:"Săptămână",day:"Zi"},"el-GR":{today:"Σήμερα",month:"Μήνας",week:"Εβδομάδα",day:"Ημέρα"},"ja-JP":{today:"今日",month:"月",week:"週",day:"日"},"ko-KR":{today:"오늘",month:"월",week:"주",day:"일"},"zh-CN":{today:"今天",month:"月",week:"周",day:"日"},"zh-TW":{today:"今天",month:"月",week:"週",day:"日"},"ar-SA":{today:"اليوم",month:"شهر",week:"أسبوع",day:"يوم"},"he-IL":{today:"היום",month:"חודש",week:"שבוע",day:"יום"},"hi-IN":{today:"आज",month:"महीना",week:"सप्ताह",day:"दिन"},"th-TH":{today:"วันนี้",month:"เดือน",week:"สัปดาห์",day:"วัน"},"vi-VN":{today:"Hôm nay",month:"Tháng",week:"Tuần",day:"Ngày"},"id-ID":{today:"Hari ini",month:"Bulan",week:"Minggu",day:"Hari"},"ms-MY":{today:"Hari ini",month:"Bulan",week:"Minggu",day:"Hari"},"uk-UA":{today:"Сьогодні",month:"Місяць",week:"Тиждень",day:"День"}};function v(t,e){const a=t.split("-")[0];return(u[t]||u[a]||u["en-US"])[e]}function y(t){return!!t.allDay||!n(t.start,t.end)}function g(a,s){const o=t(s[0]),r=e(s[s.length-1]),c=a.filter(t=>t.start<=r&&t.end>=o).map(a=>({...a,_visStart:new Date(Math.max(a.start.getTime(),o.getTime())),_visEnd:new Date(Math.min(a.end.getTime(),r.getTime())),_isStart:t(a.start)>=o,_isEnd:e(a.end)<=r}));c.sort((t,e)=>{const a=t.start-e.start;if(0!==a)return a;const n=i(t._visStart,t._visEnd);return i(e._visStart,e._visEnd)-n||(t._origStart||t.start)-(e._origStart||e.start)});const l=[],d=[];for(const t of c){const e=s.findIndex(e=>n(e,t._visStart)),a=s.findIndex(e=>n(e,t._visEnd)),i=-1===e?0:e,o=-1===a?s.length-1:a;let r=l.findIndex(t=>t<=i);-1===r?(r=l.length,l.push(o+1)):l[r]=o+1,d.push({event:t,startCol:i,endCol:o,slot:r,isStart:t._isStart,isEnd:t._isEnd})}return d}
11
+ /* ============================================================
12
+ ES MODULE EXPORT
13
+ Also available as window.SimpleCalendarJs via the IIFE wrapper.
14
+ ============================================================ */
15
+ return class{constructor(e,a={}){if("string"==typeof e){if(this._el=document.querySelector(e),!this._el)throw new Error(`SimpleCalendarJs: no element for "${e}"`)}else this._el=e;this._opts=Object.assign({defaultView:"month",defaultDate:null,weekStartsOn:0,locale:"default",use24Hour:!1,showTimeInItems:!0,showGridLines:!0,showToolbar:!0,showTodayButton:!0,showNavigation:!0,showTitle:!0,showYearPicker:!0,showViewSwitcher:!0,enabledViews:["month","week","day"],fetchEvents:null,onEventClick:null,onSlotClick:null,onViewChange:null,onNavigate:null},a),this._view=this._opts.defaultView,this._date=t(this._opts.defaultDate||new Date),this._events=[],this._nowInterval=null,this._yearPickerOpen=!1,this._yearPickerBase=0,this._yearOutsideHandler=null,this._root=document.createElement("div"),this._root.className="uc-calendar",this._opts.showGridLines||this._root.classList.add("uc-no-grid"),this._el.appendChild(this._root),this._onClick=this._handleClick.bind(this),this._root.addEventListener("click",this._onClick),this._fetchAndRender(),this._startNowUpdater()}setView(t){t!==this._view&&(this._view=t,this._opts.onViewChange&&this._opts.onViewChange(t),this._fetchAndRender())}navigate(t){const e=new Date(this._date);"month"===this._view?(e.setMonth(e.getMonth()+t),e.setDate(1)):"week"===this._view?e.setDate(e.getDate()+7*t):e.setDate(e.getDate()+t),this._date=e;const a=this._getRange();this._opts.onNavigate&&this._opts.onNavigate(a.start,a.end),this._fetchAndRender()}goToToday(){this._date=t(new Date);const e=this._getRange();this._opts.onNavigate&&this._opts.onNavigate(e.start,e.end),this._fetchAndRender()}goToDate(e){this._date=t(o(e)),this._fetchAndRender()}destroy(){this._nowInterval&&clearInterval(this._nowInterval),this._yearOutsideHandler&&document.removeEventListener("click",this._yearOutsideHandler),this._root.removeEventListener("click",this._onClick),this._root.remove()}_getRange(){if("month"===this._view){const a=c(this._date.getFullYear(),this._date.getMonth(),this._opts.weekStartsOn);return{start:t(a[0]),end:e(a[a.length-1])}}if("week"===this._view){const a=r(this._date,this._opts.weekStartsOn);return{start:t(a[0]),end:e(a[6])}}return{start:t(this._date),end:e(this._date)}}async _fetchAndRender(){if(this._root.innerHTML=this._buildShell(),!this._opts.fetchEvents)return void this._renderView();const t=this._root.querySelector(".uc-loading");t&&(t.style.display="flex");const e=this._getRange();try{const t=await this._opts.fetchEvents(e.start,e.end);this._events=(t||[]).map(t=>({...t,start:o(t.start),end:o(t.end)}))}catch(t){this._events=[]}t&&(t.style.display="none"),this._renderView()}_renderView(){const t=this._root.querySelector(".uc-view-container");if(t)if("month"===this._view)t.innerHTML=this._buildMonthView();else if("week"===this._view){const e=r(this._date,this._opts.weekStartsOn);t.innerHTML=this._buildWeekOrDayView(e),this._scrollToBusinessHours(t)}else t.innerHTML=this._buildWeekOrDayView([this._date]),this._scrollToBusinessHours(t)}_scrollToBusinessHours(t){requestAnimationFrame(()=>{const e=t.querySelector(".uc-time-body");if(!e)return;const a=parseFloat(getComputedStyle(this._root).getPropertyValue("--cal-hour-height"))||60;e.scrollTop=7*a})}_renderToolbar(){const t=this._root.querySelector(".uc-toolbar");if(!t)return;const e=document.createElement("div");e.innerHTML=this._buildToolbar(),this._root.replaceChild(e.firstElementChild,t)}_buildShell(){return`\n ${this._buildToolbar()}\n <div class="uc-loading" style="display:none">\n <div class="uc-spinner"></div>\n </div>\n <div class="uc-view-container"></div>\n `}_buildToolbar(){if(!this._opts.showToolbar)return"";const t=this._date.getFullYear(),e=(new Date).getFullYear();let a;if("month"===this._view)a=new Intl.DateTimeFormat(this._opts.locale,{month:"long"}).format(this._date);else if("week"===this._view){const t=r(this._date,this._opts.weekStartsOn);a=function(t,e,a){if(t.getMonth()===e.getMonth()&&t.getFullYear()===e.getFullYear())return`${new Intl.DateTimeFormat(a,{month:"long"}).format(t)} ${t.getDate()}–${e.getDate()}`;const n=t=>new Intl.DateTimeFormat(a,{month:"short",day:"numeric"}).format(t);return`${n(t)} – ${n(e)}`}(t[0],t[6],this._opts.locale)}else a=new Intl.DateTimeFormat(this._opts.locale,{weekday:"long",month:"long",day:"numeric"}).format(this._date);let s="";if(this._opts.showYearPicker&&this._yearPickerOpen){const a=this._yearPickerBase,n=Array.from({length:12},(n,s)=>{const i=a+s,o=i===t;return`<button class="${"uc-year-item"+(o?" uc-active":"")+(i===e&&!o?" uc-today-year":"")}" data-action="select-year" data-year="${i}">${i}</button>`}).join("");s=`\n <div class="uc-year-picker">\n <div class="uc-year-picker-nav">\n <button class="uc-year-nav-btn" data-action="year-prev" aria-label="Previous years">&#8249;</button>\n <span class="uc-year-range">${a} – ${a+11}</span>\n <button class="uc-year-nav-btn" data-action="year-next" aria-label="Next years">&#8250;</button>\n </div>\n <div class="uc-year-grid">${n}</div>\n </div>`}const i=this._opts.locale,o=v(i,"today"),c=v(i,"month"),l=v(i,"week"),d=v(i,"day");let u="";if(this._opts.showNavigation||this._opts.showTodayButton){const t=this._opts.showNavigation?'<button class="uc-btn uc-nav-btn" data-action="prev" aria-label="Previous">&#8249;</button>':"",e=new Date,a=n(this._date,e)?" uc-active":"";u=`\n <div class="uc-toolbar-section uc-toolbar-left">\n ${t}${this._opts.showTodayButton?`<button class="uc-btn uc-today-btn${a}" data-action="today">${h(o)}</button>`:""}${this._opts.showNavigation?'<button class="uc-btn uc-nav-btn" data-action="next" aria-label="Next">&#8250;</button>':""}\n </div>`}let y="";if(this._opts.showTitle){const e=this._opts.showYearPicker?`<button class="uc-year-btn${this._yearPickerOpen?" uc-open":""}" data-action="year-pick" aria-label="Select year">${t}</button>`:t;y=`\n <div class="uc-toolbar-section uc-toolbar-center">\n <h2 class="uc-title">\n <span class="uc-title-main">${h(a)}</span>\n ${e}\n </h2>\n ${s}\n </div>`}let g="";if(this._opts.showViewSwitcher){const t=this._opts.enabledViews,e=[];t.includes("month")&&e.push(`<button class="uc-btn uc-view-btn${"month"===this._view?" uc-active":""}" data-view="month">${h(c)}</button>`),t.includes("week")&&e.push(`<button class="uc-btn uc-view-btn${"week"===this._view?" uc-active":""}" data-view="week">${h(l)}</button>`),t.includes("day")&&e.push(`<button class="uc-btn uc-view-btn${"day"===this._view?" uc-active":""}" data-view="day">${h(d)}</button>`),e.length>0&&(g=`\n <div class="uc-toolbar-section uc-toolbar-right">\n <div class="uc-view-switcher">\n ${e.join("")}\n </div>\n </div>`)}return`\n <div class="uc-toolbar">\n ${u}${y}${g}\n </div>\n `}_buildMonthView(){const{locale:a,weekStartsOn:n}=this._opts,s=c(this._date.getFullYear(),this._date.getMonth(),n),i=d(a,n,"short"),o=this._events.map(a=>({...a,_origStart:a.start,start:y(a)?t(a.start):a.start,end:y(a)?e(a.end):a.end})),r=i.map(t=>`<div class="uc-month-day-name">${h(t)}</div>`).join(""),l=[];for(let t=0;t<s.length;t+=7)l.push(s.slice(t,t+7));return`\n <div class="uc-month-view">\n <div class="uc-month-header">${r}</div>\n <div class="uc-month-body">${l.map(t=>this._buildWeekRow(t,o)).join("")}</div>\n </div>\n `}_buildWeekRow(t,e){const a=this._date.getMonth(),i=e.filter(y),o=e.filter(t=>!y(t)),r=g(i,t),c=Array.from({length:7},()=>new Set);for(const{startCol:t,endCol:e,slot:a}of r)for(let n=t;n<=e;n++)c[n].add(a);const d=t.map(t=>o.filter(e=>n(e.start,t)).sort((t,e)=>t.start-e.start)),u=t.map((t,e)=>`\n <div class="uc-day-cell${s(t)?" uc-today":""}${t.getMonth()!==a?" uc-other-month":""}" data-date="${t.toISOString()}" data-action="day-click">\n <span class="uc-day-number" data-action="day-number" data-date="${t.toISOString()}">${t.getDate()}</span>\n </div>`).join("");let v="";for(const{event:t,startCol:e,endCol:a,slot:n,isStart:s,isEnd:i}of r){if(n>=3)continue;const o=100/7,r=e*o,c=(a-e+1)*o,l=`calc(var(--cal-header-day-height) + ${n} * (var(--cal-event-height) + var(--cal-event-gap)) + 4px)`,d=t.color||"var(--cal-event-bg)",u=s?"var(--cal-event-border-radius)":"0",y=i?"var(--cal-event-border-radius)":"0";v+=`\n <div class="uc-event-bar${s?"":" uc-continues-left"}${i?"":" uc-continues-right"}"\n style="left:calc(${r}% + 2px);width:calc(${c}% - 4px);top:${l};background:${h(d)};border-radius:${u} ${y} ${y} ${u};"\n data-event-id="${h(t.id)}" data-action="event-click" title="${h(t.title)}">\n ${s?`<span class="uc-event-title">${h(t.title)}</span>`:"&nbsp;"}\n </div>`}for(let e=0;e<7;e++){const a=100/7,n=e*a,s=t[e],i=([...c[e]].filter(t=>t<3).length,[...c[e]].filter(t=>t>=3).length),o=[...c[e]],r=o.length>0?Math.max(...o)+1:0,u=[];for(let t=r;t<3;t++)u.push(t);const y=d[e];let g=i;if(y.forEach((t,e)=>{if(e<u.length){const s=`calc(var(--cal-header-day-height) + ${u[e]} * (var(--cal-event-height) + var(--cal-event-gap)) + 4px)`,i=t.color||"var(--cal-event-bg)",o=l(t.start,this._opts.locale,this._opts.use24Hour),r=this._opts.showTimeInItems?`<span class="uc-event-time">${h(o)}</span>`:"";v+=`\n <div class="uc-event-bar"\n style="left:calc(${n}% + 2px);width:calc(${a}% - 4px);top:${s};background:${h(i)};"\n data-event-id="${h(t.id)}" data-action="event-click" title="${h(t.title)}">\n ${r}\n <span class="uc-event-title">${h(t.title)}</span>\n </div>`}else g++}),g>0){v+=`\n <div class="uc-more-link"\n style="left:calc(${n}% + 2px);width:calc(${a}% - 4px);top:${"calc(var(--cal-header-day-height) + 3 * (var(--cal-event-height) + var(--cal-event-gap)) + 4px)"};"\n data-date="${s.toISOString()}" data-action="more-click">\n +${g} more\n </div>`}}return`\n <div class="uc-week-row">\n <div class="uc-week-cells">${u}</div>\n <div class="uc-week-events">${v}</div>\n </div>`}_buildWeekOrDayView(a){const{locale:i,weekStartsOn:o,use24Hour:r}=this._opts,c=1===a.length,u=c?" uc-day-header-single":"",v=c?[new Intl.DateTimeFormat(i,{weekday:"short"}).format(a[0])]:d(i,o,"short"),p=t(a[0]),_=e(a[a.length-1]),w=this._events.filter(t=>y(t)&&t.start<=_&&e(t.end)>=p).map(a=>({...a,start:t(a.start),end:e(a.end)})),m=this._events.filter(t=>!y(t)&&t.start>=p&&t.start<=_),k=a.map((t,e)=>{const a=s(t)?" uc-today":"",n=t.getDate();return`\n <div class="uc-week-day-header${a}">\n <span class="uc-week-day-name">${h(v[e])}</span>\n <span class="uc-week-day-num" data-action="day-number" data-date="${t.toISOString()}">${n}</span>\n </div>`}).join(""),f=c?w.map((t,e)=>({event:t,startCol:0,endCol:0,slot:e,isStart:!0,isEnd:!0})):g(w,a),$=f.length?Math.max(...f.map(t=>t.slot))+1:0;let b="";for(const{event:t,startCol:e,endCol:n,slot:s,isStart:i,isEnd:o}of f){const r=100/a.length,c=i?"var(--cal-event-border-radius)":"0",l=o?"var(--cal-event-border-radius)":"0";b+=`\n <div class="uc-event-bar${i?"":" uc-continues-left"}${o?"":" uc-continues-right"}"\n style="left:calc(${e*r}% + 2px);width:calc(${(n-e+1)*r}% - 4px);top:${`calc(${s} * (var(--cal-event-height) + 3px) + 2px)`};background:${h(t.color||"var(--cal-event-bg)")};border-radius:${c} ${l} ${l} ${c};"\n data-event-id="${h(t.id)}" data-action="event-click" title="${h(t.title)}">\n ${i?`<span class="uc-event-title">${h(t.title)}</span>`:"&nbsp;"}\n </div>`}const D=`calc(${Math.max(1,$)} * (var(--cal-event-height) + 3px) + 6px)`,S=new Date,T=60*S.getHours()+S.getMinutes(),M=a.length;return`\n <div class="${c?"uc-day-view":"uc-week-view"}">\n <div class="uc-week-header">\n <div class="uc-time-gutter-spacer"></div>\n <div class="uc-week-day-headers${u}">${k}</div>\n </div>\n <div class="uc-all-day-section">\n <div class="uc-time-gutter-spacer uc-all-day-label">all-day</div>\n <div class="uc-all-day-events" style="min-height:${D}">${b}</div>\n </div>\n <div class="uc-time-body">\n <div class="uc-time-grid-inner">\n <div class="uc-time-gutter">${Array.from({length:24},(t,e)=>`<div class="uc-hour-cell"><span class="uc-hour-label">${h(0===e?"":l(new Date(2e3,0,1,e),"en-US",r))}</span></div>`).join("")}</div>\n <div class="uc-time-columns" style="--uc-col-count:${M}">${a.map(t=>{const e=s(t)?" uc-today":"",a=Array.from({length:24},()=>'<div class="uc-hour-row"><div class="uc-half-hour-line"></div></div>').join(""),o=function(t){if(!t.length)return[];const e=[...t].sort((t,e)=>t.start-e.start||e.end-t.end),a=[],n=[];for(const t of e){let e=a.findIndex(e=>e<=t.start);-1===e?(e=a.length,a.push(t.end)):a[e]=t.end,n.push({event:t,col:e})}return n.map(t=>{const e=n.filter(e=>e.event.start<t.event.end&&e.event.end>t.event.start),a=Math.max(...e.map(t=>t.col))+1;return{...t,totalCols:a}})}(m.filter(e=>n(e.start,t))).map(({event:t,col:e,totalCols:a})=>{const n=60*t.start.getHours()+t.start.getMinutes(),s=60*t.end.getHours()+t.end.getMinutes(),o=`calc(${n} / 60 * var(--cal-hour-height))`,c=`calc(${Math.max(s-n,30)} / 60 * var(--cal-hour-height))`,d=100/a,u=`calc(${e*d}% + 1px)`,v=`calc(${d}% - 2px)`,y=t.color||"var(--cal-event-bg)",g=l(t.start,i,r),p=s-n<=60?" uc-timed-event--short":"",_=this._opts.showTimeInItems?`<span class="uc-event-time">${h(g)}</span>`:"";return`\n <div class="uc-timed-event${p}"\n style="top:${o};height:${c};left:${u};width:${v};background:${h(y)};"\n data-event-id="${h(t.id)}" data-action="event-click" title="${h(t.title)}">\n ${_}\n <span class="uc-event-title">${h(t.title)}</span>\n </div>`}).join(""),c=s(t)?`<div class="uc-now-indicator" style="top:calc(${T} / 60 * var(--cal-hour-height));">\n <span class="uc-now-dot"></span>\n <span class="uc-now-line"></span>\n </div>`:"";return`<div class="uc-time-col${e}" data-date="${t.toISOString()}" data-action="slot-col">\n ${a}${o}${c}\n </div>`}).join("")}</div>\n </div>\n </div>\n </div>`}_closeYearPicker(){this._yearOutsideHandler&&(document.removeEventListener("click",this._yearOutsideHandler),this._yearOutsideHandler=null),this._yearPickerOpen=!1,this._renderToolbar()}_handleClick(e){const a=e.target.closest("[data-view]");if(a)return void this.setView(a.dataset.view);const n=e.target.closest("[data-action]");if(!n)return;switch(n.dataset.action){case"prev":this.navigate(-1);break;case"next":this.navigate(1);break;case"today":this.goToToday();break;case"year-pick":e.stopPropagation(),this._yearPickerOpen?this._closeYearPicker():(this._yearPickerOpen=!0,this._yearPickerBase=this._date.getFullYear()-4,this._renderToolbar(),this._yearOutsideHandler=t=>{t.target.closest(".uc-year-picker")||t.target.closest('[data-action="year-pick"]')||this._closeYearPicker()},setTimeout(()=>document.addEventListener("click",this._yearOutsideHandler),0));break;case"year-prev":e.stopPropagation(),this._yearPickerBase-=12,this._renderToolbar();break;case"year-next":e.stopPropagation(),this._yearPickerBase+=12,this._renderToolbar();break;case"select-year":{e.stopPropagation();const t=parseInt(n.dataset.year,10);this._date=new Date(this._date.getFullYear()!==t?new Date(this._date).setFullYear(t):this._date),this._closeYearPicker();const a=this._getRange();this._opts.onNavigate&&this._opts.onNavigate(a.start,a.end),this._fetchAndRender();break}case"event-click":{e.stopPropagation();const t=n.dataset.eventId,a=this._events.find(e=>String(e.id)===String(t));a&&this._opts.onEventClick&&this._opts.onEventClick(a,e);break}case"day-click":{if(e.target.closest('[data-action="day-number"]'))break;if(e.target.closest('[data-action="event-click"]'))break;const t=new Date(n.dataset.date);this._opts.onSlotClick&&this._opts.onSlotClick(t,e);break}case"day-number":{if(e.stopPropagation(),!this._opts.enabledViews.includes("day"))break;const a=new Date(n.dataset.date);this._date=t(a),this.setView("day");break}case"more-click":{if(e.stopPropagation(),!this._opts.enabledViews.includes("day"))break;const a=new Date(n.dataset.date);this._date=t(a),this.setView("day");break}case"slot-col":if(e.target.closest('[data-action="event-click"]'))break;if(this._opts.onSlotClick){const t=n,a=t.getBoundingClientRect(),s=e.clientY-a.top,i=parseFloat(getComputedStyle(this._root).getPropertyValue("--cal-hour-height"))||60,o=Math.max(0,Math.floor(s/i*60)),r=Math.floor(o/60)%24,c=15*Math.round(o%60/15),l=new Date(t.dataset.date);l.setHours(r,c,0,0),this._opts.onSlotClick(l,e)}}}_startNowUpdater(){this._nowInterval=setInterval(()=>{const t=this._root.querySelectorAll(".uc-now-indicator");if(!t.length)return;const e=new Date,a=`calc(${60*e.getHours()+e.getMinutes()} / 60 * var(--cal-hour-height))`;t.forEach(t=>t.style.top=a)},6e4)}}});
@@ -0,0 +1,292 @@
1
+ /**
2
+ * SimpleCalendarJs v3.0.0 — Angular Wrapper
3
+ * A clean, modern, and feature-rich JavaScript calendar component with zero dependencies
4
+ *
5
+ * @author Pedro Lopes <simplecalendarjs@gmail.com>
6
+ * @homepage https://www.simplecalendarjs.com
7
+ * @license SEE LICENSE IN LICENSE
8
+ * @repository https://github.com/pclslopes/SimpleCalendarJs
9
+ *
10
+ * Usage (Standalone Component):
11
+ * import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';
12
+ *
13
+ * @Component({
14
+ * selector: 'app-root',
15
+ * standalone: true,
16
+ * imports: [SimpleCalendarJsComponent],
17
+ * template: `
18
+ * <simple-calendar-js
19
+ * [defaultView]="view"
20
+ * [weekStartsOn]="1"
21
+ * [darkMode]="isDark"
22
+ * [fetchEvents]="fetchEvents"
23
+ * (eventClick)="onEventClick($event)"
24
+ * (slotClick)="onSlotClick($event)"
25
+ * (viewChange)="onViewChange($event)"
26
+ * (navigate)="onNavigate($event)"
27
+ * [customClass]="'my-calendar'"
28
+ * [customStyle]="{ height: '600px' }"
29
+ * ></simple-calendar-js>
30
+ * `
31
+ * })
32
+ *
33
+ * Usage (NgModule):
34
+ * import { SimpleCalendarJsComponent } from './simple-calendar-js-angular.component';
35
+ *
36
+ * @NgModule({
37
+ * declarations: [SimpleCalendarJsComponent],
38
+ * exports: [SimpleCalendarJsComponent]
39
+ * })
40
+ *
41
+ * All options accepted by `new SimpleCalendarJs()` are valid @Input properties.
42
+ * Callbacks use Angular @Output events.
43
+ * The darkMode input toggles dark theme without re-creating the calendar.
44
+ *
45
+ * Accessing methods via ViewChild:
46
+ * @ViewChild(SimpleCalendarJsComponent) calendar!: SimpleCalendarJsComponent;
47
+ *
48
+ * ngAfterViewInit() {
49
+ * this.calendar.setView('week');
50
+ * this.calendar.navigate(1);
51
+ * this.calendar.goToDate(new Date());
52
+ * this.calendar.goToToday();
53
+ * }
54
+ *
55
+ * Example with theme switching:
56
+ * @Component({
57
+ * selector: 'app-calendar-demo',
58
+ * standalone: true,
59
+ * imports: [SimpleCalendarJsComponent],
60
+ * template: `
61
+ * <button (click)="isDark = !isDark">Toggle Theme</button>
62
+ * <simple-calendar-js
63
+ * [defaultView]="view"
64
+ * [darkMode]="isDark"
65
+ * [fetchEvents]="fetchEvents"
66
+ * (eventClick)="handleEventClick($event)"
67
+ * ></simple-calendar-js>
68
+ * `
69
+ * })
70
+ * export class CalendarDemoComponent {
71
+ * isDark = false;
72
+ * view = 'month';
73
+ *
74
+ * fetchEvents = async (start: Date, end: Date) => {
75
+ * // Fetch events...
76
+ * return events;
77
+ * };
78
+ *
79
+ * handleEventClick(event: any) {
80
+ * console.log('Event clicked:', event);
81
+ * }
82
+ * }
83
+ */
84
+
85
+ import {
86
+ Component,
87
+ Input,
88
+ Output,
89
+ EventEmitter,
90
+ OnInit,
91
+ OnDestroy,
92
+ OnChanges,
93
+ SimpleChanges,
94
+ ElementRef,
95
+ ViewChild,
96
+ AfterViewInit,
97
+ } from '@angular/core';
98
+
99
+ // Declare global SimpleCalendarJs
100
+ declare global {
101
+ interface Window {
102
+ SimpleCalendarJs: any;
103
+ }
104
+ const SimpleCalendarJs: any;
105
+ }
106
+
107
+ // Props that require re-creating the calendar when changed
108
+ const INIT_PROPS = [
109
+ 'defaultView',
110
+ 'defaultDate',
111
+ 'weekStartsOn',
112
+ 'locale',
113
+ 'use24Hour',
114
+ 'showTimeInItems',
115
+ 'showGridLines',
116
+ 'showToolbar',
117
+ 'showTodayButton',
118
+ 'showNavigation',
119
+ 'showTitle',
120
+ 'showYearPicker',
121
+ 'showViewSwitcher',
122
+ 'enabledViews',
123
+ ];
124
+
125
+ @Component({
126
+ selector: 'simple-calendar-js',
127
+ standalone: true,
128
+ template: `
129
+ <div
130
+ #calendarContainer
131
+ [class]="customClass"
132
+ [ngStyle]="customStyle || { height: '100%', minHeight: '500px' }"
133
+ ></div>
134
+ `,
135
+ styles: [],
136
+ })
137
+ export class SimpleCalendarJsComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
138
+ @ViewChild('calendarContainer', { static: true }) containerEl!: ElementRef;
139
+
140
+ // Init-only inputs
141
+ @Input() defaultView: 'month' | 'week' | 'day' = 'month';
142
+ @Input() defaultDate?: Date;
143
+ @Input() weekStartsOn: 0 | 1 = 0;
144
+ @Input() locale: string = 'default';
145
+ @Input() use24Hour: boolean = false;
146
+ @Input() showTimeInItems: boolean = true;
147
+ @Input() showGridLines: boolean = true;
148
+ @Input() showToolbar: boolean = true;
149
+ @Input() showTodayButton: boolean = true;
150
+ @Input() showNavigation: boolean = true;
151
+ @Input() showTitle: boolean = true;
152
+ @Input() showYearPicker: boolean = true;
153
+ @Input() showViewSwitcher: boolean = true;
154
+ @Input() enabledViews: string[] = ['month', 'week', 'day'];
155
+
156
+ // Callback inputs
157
+ @Input() fetchEvents?: (start: Date, end: Date) => Promise<any[]>;
158
+
159
+ // Theme input
160
+ @Input() darkMode: boolean = false;
161
+
162
+ // Style inputs
163
+ @Input() customClass: string = '';
164
+ @Input() customStyle?: { [key: string]: string };
165
+
166
+ // Output events
167
+ @Output() eventClick = new EventEmitter<any>();
168
+ @Output() slotClick = new EventEmitter<Date>();
169
+ @Output() viewChange = new EventEmitter<string>();
170
+ @Output() navigate = new EventEmitter<{ start: Date; end: Date }>();
171
+
172
+ private calendar: any = null;
173
+ private isViewInitialized = false;
174
+
175
+ ngOnInit() {
176
+ // Calendar will be created in ngAfterViewInit
177
+ }
178
+
179
+ ngAfterViewInit() {
180
+ this.isViewInitialized = true;
181
+ this.createCalendar();
182
+ }
183
+
184
+ ngOnDestroy() {
185
+ this.calendar?.destroy();
186
+ this.calendar = null;
187
+ }
188
+
189
+ ngOnChanges(changes: SimpleChanges) {
190
+ if (!this.isViewInitialized) return;
191
+
192
+ // Check if any init prop changed (requires re-creation)
193
+ const initPropChanged = INIT_PROPS.some((prop) => changes[prop] && !changes[prop].firstChange);
194
+
195
+ if (initPropChanged) {
196
+ this.createCalendar();
197
+ return;
198
+ }
199
+
200
+ // Handle dark mode change without re-creation
201
+ if (changes['darkMode'] && !changes['darkMode'].firstChange) {
202
+ if (this.calendar) {
203
+ this.calendar._root.classList.toggle('uc-dark', this.darkMode);
204
+ }
205
+ }
206
+
207
+ // Handle fetchEvents change without re-creation
208
+ if (changes['fetchEvents'] && !changes['fetchEvents'].firstChange) {
209
+ if (this.calendar && this.fetchEvents) {
210
+ this.calendar._opts.fetchEvents = this.fetchEvents;
211
+ }
212
+ }
213
+ }
214
+
215
+ private resolveClass(): any {
216
+ if (typeof SimpleCalendarJs !== 'undefined') return SimpleCalendarJs;
217
+ if (typeof window !== 'undefined' && window.SimpleCalendarJs) return window.SimpleCalendarJs;
218
+ return null;
219
+ }
220
+
221
+ private createCalendar() {
222
+ const Cal = this.resolveClass();
223
+ if (!Cal) {
224
+ console.error(
225
+ 'SimpleCalendarJsComponent: SimpleCalendarJs class not found. ' +
226
+ 'Make sure simple-calendar-js.js is imported or loaded as a script.'
227
+ );
228
+ return;
229
+ }
230
+
231
+ // Destroy previous instance
232
+ if (this.calendar) {
233
+ this.calendar.destroy();
234
+ this.calendar = null;
235
+ }
236
+
237
+ // Build options
238
+ const options: any = {
239
+ defaultView: this.defaultView,
240
+ defaultDate: this.defaultDate,
241
+ weekStartsOn: this.weekStartsOn,
242
+ locale: this.locale,
243
+ use24Hour: this.use24Hour,
244
+ showTimeInItems: this.showTimeInItems,
245
+ showGridLines: this.showGridLines,
246
+ showToolbar: this.showToolbar,
247
+ showTodayButton: this.showTodayButton,
248
+ showNavigation: this.showNavigation,
249
+ showTitle: this.showTitle,
250
+ showYearPicker: this.showYearPicker,
251
+ showViewSwitcher: this.showViewSwitcher,
252
+ enabledViews: this.enabledViews,
253
+ };
254
+
255
+ // Add callbacks
256
+ if (this.fetchEvents) {
257
+ options.fetchEvents = this.fetchEvents;
258
+ }
259
+ options.onEventClick = (event: any) => this.eventClick.emit(event);
260
+ options.onSlotClick = (date: Date) => this.slotClick.emit(date);
261
+ options.onViewChange = (view: string) => this.viewChange.emit(view);
262
+ options.onNavigate = (start: Date, end: Date) => this.navigate.emit({ start, end });
263
+
264
+ this.calendar = new Cal(this.containerEl.nativeElement, options);
265
+
266
+ // Apply dark mode if needed
267
+ if (this.darkMode) {
268
+ this.calendar._root.classList.add('uc-dark');
269
+ }
270
+ }
271
+
272
+ // Public methods
273
+ public setView(view: 'month' | 'week' | 'day'): void {
274
+ this.calendar?.setView(view);
275
+ }
276
+
277
+ public navigate(direction: number): void {
278
+ this.calendar?.navigate(direction);
279
+ }
280
+
281
+ public goToDate(date: Date): void {
282
+ this.calendar?.goToDate(date);
283
+ }
284
+
285
+ public goToToday(): void {
286
+ this.calendar?.goToToday();
287
+ }
288
+
289
+ public getInstance(): any {
290
+ return this.calendar;
291
+ }
292
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * SimpleCalendarJs v3.0.0 — React Wrapper
3
+ * A clean, modern, and feature-rich JavaScript calendar component with zero dependencies
4
+ *
5
+ * @author Pedro Lopes <simplecalendarjs@gmail.com>
6
+ * @homepage https://www.simplecalendarjs.com
7
+ * @license SEE LICENSE IN LICENSE
8
+ * @repository https://github.com/pclslopes/SimpleCalendarJs
9
+ *
10
+ * Imperative handle (via ref):
11
+ * const ref = useRef();
12
+ * // ref.current exposes: { setView, navigate, goToDate, goToToday }
13
+ *
14
+ * Example with theme switching:
15
+ * function MyApp() {
16
+ * const [isDark, setIsDark] = useState(false);
17
+ * return (
18
+ * <>
19
+ * <button onClick={() => setIsDark(!isDark)}>Toggle Theme</button>
20
+ * <SimpleCalendarJsReact
21
+ * darkMode={isDark}
22
+ * defaultView="month"
23
+ * fetchEvents={fetchEvents}
24
+ * />
25
+ * </>
26
+ * );
27
+ * }
28
+ */
29
+
30
+ import { useEffect, useRef, useImperativeHandle, forwardRef, memo } from 'react';
31
+
32
+ // Callback prop names — these are updated on the instance without re-mounting
33
+ const CALLBACK_PROPS = ['fetchEvents', 'onEventClick', 'onSlotClick', 'onViewChange', 'onNavigate'];
34
+
35
+ // Init-only props — changes require a full re-mount
36
+ const INIT_PROPS = [
37
+ 'defaultView',
38
+ 'defaultDate',
39
+ 'weekStartsOn',
40
+ 'locale',
41
+ 'use24Hour',
42
+ 'showTimeInItems',
43
+ 'showGridLines',
44
+ 'showToolbar',
45
+ 'showTodayButton',
46
+ 'showNavigation',
47
+ 'showTitle',
48
+ 'showYearPicker',
49
+ 'showViewSwitcher',
50
+ 'enabledViews',
51
+ ];
52
+
53
+ const SimpleCalendarJsReact = forwardRef(function SimpleCalendarJsReact(props, ref) {
54
+ const {
55
+ // Layout props
56
+ style,
57
+ className,
58
+ // Theme prop
59
+ darkMode = false,
60
+ // All other props forwarded to SimpleCalendarJs
61
+ ...calProps
62
+ } = props;
63
+
64
+ const containerRef = useRef(null);
65
+ const instanceRef = useRef(null);
66
+
67
+ // Track init-only props so we can detect if a full re-mount is needed
68
+ const prevInitProps = useRef({});
69
+
70
+ // Expose imperative API
71
+ useImperativeHandle(
72
+ ref,
73
+ () => ({
74
+ setView: (v) => instanceRef.current?.setView(v),
75
+ navigate: (d) => instanceRef.current?.navigate(d),
76
+ goToDate: (d) => instanceRef.current?.goToDate(d),
77
+ goToToday: () => instanceRef.current?.goToToday(),
78
+ getInstance: () => instanceRef.current,
79
+ }),
80
+ []
81
+ );
82
+
83
+ // Mount / re-mount when init-only props change
84
+ useEffect(() => {
85
+ const Cal = resolveClass();
86
+ if (!Cal) {
87
+ console.error(
88
+ 'SimpleCalendarJsReact: SimpleCalendarJs class not found. ' +
89
+ 'Make sure simple-calendar-js.js is imported or loaded as a script.'
90
+ );
91
+ return;
92
+ }
93
+
94
+ // Destroy previous instance if any
95
+ if (instanceRef.current) {
96
+ instanceRef.current.destroy();
97
+ instanceRef.current = null;
98
+ }
99
+
100
+ // Build options object from props
101
+ const options = {};
102
+ for (const key of [...INIT_PROPS, ...CALLBACK_PROPS]) {
103
+ if (calProps[key] !== undefined) options[key] = calProps[key];
104
+ }
105
+
106
+ instanceRef.current = new Cal(containerRef.current, options);
107
+ prevInitProps.current = pickKeys(calProps, INIT_PROPS);
108
+
109
+ return () => {
110
+ instanceRef.current?.destroy();
111
+ instanceRef.current = null;
112
+ };
113
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
+ }, INIT_PROPS.map((k) => calProps[k]));
115
+
116
+ // Update callbacks live without re-mounting
117
+ useEffect(() => {
118
+ const inst = instanceRef.current;
119
+ if (!inst) return;
120
+ for (const key of CALLBACK_PROPS) {
121
+ if (calProps[key] !== undefined) {
122
+ inst._opts[key] = calProps[key];
123
+ }
124
+ }
125
+ });
126
+
127
+ // Update dark mode live without re-mounting
128
+ useEffect(() => {
129
+ const inst = instanceRef.current;
130
+ if (!inst) return;
131
+ inst._root.classList.toggle('uc-dark', darkMode);
132
+ }, [darkMode]);
133
+
134
+ return (
135
+ <div
136
+ ref={containerRef}
137
+ className={className}
138
+ style={{ height: '100%', minHeight: 500, ...style }}
139
+ />
140
+ );
141
+ });
142
+
143
+ SimpleCalendarJsReact.displayName = 'SimpleCalendarJs';
144
+
145
+ export default memo(SimpleCalendarJsReact);
146
+
147
+ /* ---- helpers ---- */
148
+
149
+ function resolveClass() {
150
+ // Works whether SimpleCalendarJs was imported as an ES module
151
+ // or loaded as a UMD script (window.SimpleCalendarJs).
152
+ if (typeof SimpleCalendarJs !== 'undefined') return SimpleCalendarJs; // eslint-disable-line no-undef
153
+ if (typeof window !== 'undefined' && window.SimpleCalendarJs) return window.SimpleCalendarJs;
154
+ return null;
155
+ }
156
+
157
+ function pickKeys(obj, keys) {
158
+ const out = {};
159
+ for (const k of keys) if (k in obj) out[k] = obj[k];
160
+ return out;
161
+ }