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.
- package/LICENSE +1 -1
- package/README.md +364 -612
- package/dist/simple-calendar-js.min.css +13 -0
- package/dist/simple-calendar-js.min.js +15 -0
- package/frameworks/simple-calendar-js-angular.component.ts +292 -0
- package/frameworks/simple-calendar-js-react.jsx +161 -0
- package/frameworks/simple-calendar-js-vue.js +208 -0
- package/package.json +8 -4
- package/dist/SimpleCalendarJs.min.css +0 -10
- package/dist/SimpleCalendarJs.min.js +0 -10
|
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}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">‹</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">›</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">‹</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">›</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>`:" "}\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>`:" "}\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
|
+
}
|