├── example.png
├── favicon.ico
├── README.md
├── style.css
├── LICENSE
├── iframe.html
├── index.html
├── script.js
├── example.ics
├── embed.css
├── embed.js
├── ical.min.js
└── ical.min.js.map
/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GRA0007/modern-cal-embed/HEAD/example.png
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GRA0007/modern-cal-embed/HEAD/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Modern iCal Calendar Embed
2 |
3 |
4 |
5 | This is a fully browser-side iCal (ics) calendar embed. It takes a link to an iCal file, and parses it and turns it into an embed. To use it, simply visit the link below and fill out the fields.
6 |
7 | https://gra0007.github.io/modern-cal-embed/
8 |
9 | **Note:** To use iCal urls that don't specify an `Access-Control-Allow-Origin` header, like from Google calendar, you'll need to use a cors proxy like https://cors-anywhere.herokuapp.com/.
10 |
11 | You can test the embed's functionality using the following ics file url:
12 | `https://gra0007.github.io/modern-cal-embed/example.ics`
13 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 20px;
3 | background: #FFF;
4 | font-family: sans-serif;
5 | }
6 | main {
7 | display: grid;
8 | width: 100%;
9 | grid-template-columns: .6fr 1.4fr;
10 | grid-gap: 20px;
11 | }
12 | iframe {
13 | border: 1px solid #777;
14 | width: 100%;
15 | margin-top: 20px;
16 | border-radius: 3px;
17 | }
18 |
19 | h1 {
20 | margin: 0;
21 | }
22 | #about {
23 | display: block;
24 | margin: 0 0 30px;
25 | }
26 | h2 {
27 | margin-top: 0;
28 | }
29 |
30 | label {
31 | display: inline-block;
32 | margin-top: 6px;
33 | }
34 | input[type=url] {
35 | width: 100%;
36 | box-sizing: border-box;
37 | }
38 | input[type=url], select {
39 | padding: 4px 6px;
40 | background-color: inherit;
41 | font: inherit;
42 | font-size: .9em;
43 | border: 1px solid #777;
44 | border-radius: 3px;
45 | }
46 | input[type=color], button {
47 | padding: 4px 6px;
48 | border: 1px solid #777;
49 | border-radius: 3px;
50 | background: none;
51 | }
52 |
53 | @media screen and (max-width: 650px) {
54 | main {
55 | display: block;
56 | }
57 | #embed {
58 | margin-top: 20px;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Benjamin Grant
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Calendar Embed
5 |
6 |
7 |
8 |
9 |
10 | Calendar is loading...
11 |
12 |
13 |
Calendar title
14 |
15 | Today
16 |
17 |
21 |
22 |
23 | Wednesday, August 12
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | Agenda
35 | Month
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Modern iCal Calendar Embed
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 | Modern iCal Calendar Embed
23 | Handcrafted by Ben Grant using ical.js . Visit the Github repo .
24 |
25 |
26 |
27 |
Settings
28 |
29 | iCal URL Add cors proxy to url
30 |
31 |
32 |
33 | Title
34 |
35 |
36 | Navigation buttons
37 |
38 |
39 | Date
40 |
41 |
42 | View selector
43 |
44 |
45 | Always show details
46 |
47 |
48 | Start week on Monday
49 |
50 | Default view
51 |
52 | Agenda
53 | Month
54 |
55 |
56 | Background color
57 |
58 | Text color
59 |
60 | Theme color
61 |
62 | Theme text color
63 |
64 |
65 |
66 | Embed
67 | Copy
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/script.js:
--------------------------------------------------------------------------------
1 | const CORS_URL = 'https://cors-anywhere.herokuapp.com/';
2 |
3 | const ical_field = document.getElementById('ical');
4 | const show_title_field = document.getElementById('show_title');
5 | const show_nav_field = document.getElementById('show_nav');
6 | const show_date_field = document.getElementById('show_date');
7 | const show_details_field = document.getElementById('show_details');
8 | const show_view_field = document.getElementById('show_view');
9 | const monday_start_field = document.getElementById('monday_start');
10 | const default_view_field = document.getElementById('default_view');
11 | const color_field = document.getElementById('color');
12 | const colorbg_field = document.getElementById('colorbg');
13 | const colortxt_field = document.getElementById('colortxt');
14 | const colorsectxt_field = document.getElementById('colorsecondarytxt');
15 | const embed_field = document.getElementById('embed_link');
16 | const copy_button = document.getElementById('copy_url');
17 | const cors_button = document.getElementById('add_cors');
18 | const iframe = document.getElementById('iframe');
19 |
20 | // Defaults
21 | let ical = 'https://gra0007.github.io/modern-cal-embed/example.ics';
22 | let show_title = 1;
23 | let show_nav = 1;
24 | let show_date = 1;
25 | let show_details = 0;
26 | let show_view = 1;
27 | let monday_start = 0;
28 | let default_view = 0;
29 | let color = '#1A73E8';
30 | let colorbg = '#FFFFFF';
31 | let colortxt = '#000000';
32 | let colorsecondarytxt = '#FFFFFF';
33 |
34 | // Reload iframe with new params
35 | function refresh() {
36 | let embed = `${document.URL.substr(0,document.URL.lastIndexOf('/'))}/iframe.html?ical=${encodeURIComponent(ical)}&title=${show_title}&nav=${show_nav}&date=${show_date}&view=${show_view}&details=${show_details}&monstart=${monday_start}&dview=${default_view}&color=${encodeURIComponent(color)}&colorbg=${encodeURIComponent(colorbg)}&colortxt=${encodeURIComponent(colortxt)}&colorsecondarytxt=${encodeURIComponent(colorsecondarytxt)}`;
37 | embed_field.value = embed;
38 | iframe.src = embed;
39 | }
40 |
41 | ical_field.addEventListener('change', () => {
42 | ical = ical_field.value;
43 | refresh();
44 | });
45 |
46 | show_title_field.addEventListener('change', () => {
47 | show_title = show_title_field.checked ? 1 : 0;
48 | refresh();
49 | });
50 |
51 | show_nav_field.addEventListener('change', () => {
52 | show_nav = show_nav_field.checked ? 1 : 0;
53 | refresh();
54 | });
55 |
56 | show_date_field.addEventListener('change', () => {
57 | show_date = show_date_field.checked ? 1 : 0;
58 | refresh();
59 | });
60 |
61 | show_details_field.addEventListener('change', () => {
62 | show_details = show_details_field.checked ? 1 : 0;
63 | refresh();
64 | });
65 |
66 | show_view_field.addEventListener('change', () => {
67 | show_view = show_view_field.checked ? 1 : 0;
68 | refresh();
69 | });
70 |
71 | monday_start_field.addEventListener('change', () => {
72 | monday_start = monday_start_field.checked ? 1 : 0;
73 | refresh();
74 | });
75 |
76 | color_field.addEventListener('change', () => {
77 | color = color_field.value;
78 | refresh();
79 | });
80 | colorbg_field.addEventListener('change', () => {
81 | colorbg = colorbg_field.value;
82 | refresh();
83 | });
84 | colortxt_field.addEventListener('change', () => {
85 | colortxt = colortxt_field.value;
86 | refresh();
87 | });
88 | colorsectxt_field.addEventListener('change', () => {
89 | colorsecondarytxt = colorsectxt_field.value;
90 | refresh();
91 | });
92 |
93 | default_view_field.addEventListener('change', () => {
94 | default_view = default_view_field.value;
95 | refresh();
96 | });
97 |
98 | copy_button.addEventListener('click', () => {
99 | embed_field.select();
100 | embed_field.setSelectionRange(0, 99999);
101 | document.execCommand("copy");
102 | copy_button.innerHTML = 'Copied!';
103 | window.setTimeout(() => {
104 | copy_button.innerHTML = 'Copy';
105 | }, 1000);
106 | });
107 |
108 | cors_button.addEventListener('click', () => {
109 | if (!ical_field.value.startsWith(CORS_URL)) {
110 | ical_field.value = CORS_URL + ical_field.value;
111 | }
112 | ical = ical_field.value;
113 | refresh();
114 | });
115 |
116 | refresh();
117 |
--------------------------------------------------------------------------------
/example.ics:
--------------------------------------------------------------------------------
1 | BEGIN:VCALENDAR
2 | PRODID:-//Google Inc//Google Calendar 70.9054//EN
3 | VERSION:2.0
4 | CALSCALE:GREGORIAN
5 | METHOD:PUBLISH
6 | X-WR-CALNAME:Test calendar
7 | X-WR-TIMEZONE:Australia/Sydney
8 | X-WR-CALDESC:A test ics file for the modern ical embed
9 | BEGIN:VTIMEZONE
10 | TZID:Australia/Sydney
11 | X-LIC-LOCATION:Australia/Sydney
12 | BEGIN:STANDARD
13 | TZOFFSETFROM:+1100
14 | TZOFFSETTO:+1000
15 | TZNAME:AEST
16 | DTSTART:19700405T030000
17 | RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU
18 | END:STANDARD
19 | BEGIN:DAYLIGHT
20 | TZOFFSETFROM:+1000
21 | TZOFFSETTO:+1100
22 | TZNAME:AEDT
23 | DTSTART:19701004T020000
24 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU
25 | END:DAYLIGHT
26 | END:VTIMEZONE
27 | BEGIN:VEVENT
28 | DTSTART;TZID=Australia/Sydney:20200814T130000
29 | DTEND;TZID=Australia/Sydney:20200814T133000
30 | RRULE:FREQ=WEEKLY;BYDAY=FR
31 | DTSTAMP:20200814T014029Z
32 | UID:299lkegd0cprc9djor49tqoplp@google.com
33 | CREATED:20200814T013731Z
34 | DESCRIPTION:
35 | LAST-MODIFIED:20200814T013731Z
36 | LOCATION:
37 | SEQUENCE:0
38 | STATUS:CONFIRMED
39 | SUMMARY:Call with Mum
40 | TRANSP:OPAQUE
41 | END:VEVENT
42 | BEGIN:VEVENT
43 | DTSTART;TZID=Australia/Sydney:20200814T090000
44 | DTEND;TZID=Australia/Sydney:20200814T103000
45 | RRULE:FREQ=WEEKLY;BYDAY=FR
46 | DTSTAMP:20200814T014029Z
47 | UID:2fce9lrif30kcg6c0evk8pdbo7@google.com
48 | CREATED:20200814T013547Z
49 | DESCRIPTION:Remember to ask about molar
50 | LAST-MODIFIED:20200814T013547Z
51 | LOCATION:Dentist
52 | SEQUENCE:0
53 | STATUS:CONFIRMED
54 | SUMMARY:Dentist appointment
55 | TRANSP:OPAQUE
56 | END:VEVENT
57 | BEGIN:VEVENT
58 | DTSTART;TZID=Australia/Sydney:20200815T110000
59 | DTEND;TZID=Australia/Sydney:20200815T160000
60 | RRULE:FREQ=WEEKLY;BYDAY=SA
61 | DTSTAMP:20200814T014029Z
62 | UID:14i0k7r0it0qcclfj2q3m0kas2@google.com
63 | CREATED:20200814T013418Z
64 | DESCRIPTION:
65 | LAST-MODIFIED:20200814T013418Z
66 | LOCATION:Hanging Rock\, 139 S Rock Rd\, Woodend VIC 3442\, Australia
67 | SEQUENCE:0
68 | STATUS:CONFIRMED
69 | SUMMARY:Picnic with family
70 | TRANSP:OPAQUE
71 | END:VEVENT
72 | BEGIN:VEVENT
73 | DTSTART;TZID=Australia/Sydney:20200813T143000
74 | DTEND;TZID=Australia/Sydney:20200813T153000
75 | RRULE:FREQ=WEEKLY;BYDAY=TH
76 | DTSTAMP:20200814T014029Z
77 | UID:4q6bv44o5fcvgkeui6fc50ar15@google.com
78 | CREATED:20200814T013312Z
79 | DESCRIPTION:
80 | LAST-MODIFIED:20200814T013312Z
81 | LOCATION:https://example.com/call
82 | SEQUENCE:0
83 | STATUS:CONFIRMED
84 | SUMMARY:Online conference call
85 | TRANSP:OPAQUE
86 | END:VEVENT
87 | BEGIN:VEVENT
88 | DTSTART;VALUE=DATE:20200812
89 | DTEND;VALUE=DATE:20200813
90 | RRULE:FREQ=WEEKLY;BYDAY=WE
91 | DTSTAMP:20200814T014029Z
92 | UID:4l5tfbpustle12roqskimjac3f@google.com
93 | CREATED:20200814T013210Z
94 | DESCRIPTION:
95 | LAST-MODIFIED:20200814T013210Z
96 | LOCATION:
97 | SEQUENCE:0
98 | STATUS:CONFIRMED
99 | SUMMARY:Katie's Birthday
100 | TRANSP:TRANSPARENT
101 | END:VEVENT
102 | BEGIN:VEVENT
103 | DTSTART;TZID=Australia/Sydney:20200811T173000
104 | DTEND;TZID=Australia/Sydney:20200811T173000
105 | RRULE:FREQ=WEEKLY;BYDAY=TU
106 | DTSTAMP:20200814T014029Z
107 | UID:0fjc486didjnjle7jeip2ttent@google.com
108 | CREATED:20200814T013108Z
109 | DESCRIPTION:
110 | LAST-MODIFIED:20200814T013108Z
111 | LOCATION:
112 | SEQUENCE:0
113 | STATUS:CONFIRMED
114 | SUMMARY:Reminder to check mailbox
115 | TRANSP:OPAQUE
116 | END:VEVENT
117 | BEGIN:VEVENT
118 | DTSTART;TZID=Australia/Sydney:20200813T170000
119 | DTEND;TZID=Australia/Sydney:20200814T130000
120 | RRULE:FREQ=WEEKLY;BYDAY=TH
121 | DTSTAMP:20200814T014029Z
122 | UID:3fonamt1sj3fgs5bar49fc0p13@google.com
123 | CREATED:20200814T013027Z
124 | DESCRIPTION:
125 | LAST-MODIFIED:20200814T013027Z
126 | LOCATION:Sam's house
127 | SEQUENCE:0
128 | STATUS:CONFIRMED
129 | SUMMARY:Jake sleepover
130 | TRANSP:OPAQUE
131 | END:VEVENT
132 | BEGIN:VEVENT
133 | DTSTART;VALUE=DATE:20200811
134 | DTEND;VALUE=DATE:20200812
135 | RRULE:FREQ=WEEKLY;BYDAY=TU
136 | DTSTAMP:20200814T014029Z
137 | UID:2et88f5oc7vi3pvbdfkeb7h3se@google.com
138 | CREATED:20200814T012859Z
139 | DESCRIPTION:
140 | LAST-MODIFIED:20200814T012859Z
141 | LOCATION:
142 | SEQUENCE:0
143 | STATUS:CONFIRMED
144 | SUMMARY:Casual clothes day at work
145 | TRANSP:TRANSPARENT
146 | END:VEVENT
147 | BEGIN:VEVENT
148 | DTSTART;TZID=Australia/Sydney:20200810T120000
149 | DTEND;TZID=Australia/Sydney:20200810T130000
150 | RRULE:FREQ=WEEKLY;BYDAY=MO
151 | DTSTAMP:20200814T014029Z
152 | UID:32ldbigccsv7e9jurlndg7q1ac@google.com
153 | CREATED:20200814T012824Z
154 | DESCRIPTION:Don't forget to bring a present!
155 | LAST-MODIFIED:20200814T012824Z
156 | LOCATION:A fancy restaurant
157 | SEQUENCE:0
158 | STATUS:CONFIRMED
159 | SUMMARY:Lunch with Sarah
160 | TRANSP:OPAQUE
161 | END:VEVENT
162 | BEGIN:VEVENT
163 | DTSTART;TZID=Australia/Sydney:20200810T100000
164 | DTEND;TZID=Australia/Sydney:20200810T113000
165 | RRULE:FREQ=WEEKLY;BYDAY=MO
166 | DTSTAMP:20200814T014029Z
167 | UID:01t42lh9bot1kuq7gl3dhgtsrt@google.com
168 | CREATED:20200814T012712Z
169 | DESCRIPTION:
170 | LAST-MODIFIED:20200814T012723Z
171 | LOCATION:The office
172 | SEQUENCE:1
173 | STATUS:CONFIRMED
174 | SUMMARY:Morning meeting
175 | TRANSP:OPAQUE
176 | END:VEVENT
177 | END:VCALENDAR
178 |
--------------------------------------------------------------------------------
/embed.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --background-color: #FFF;
3 | --dark-gray: #777;
4 | --border-color: #AAA;
5 | --text-color: #000;
6 | --theme-color: #1A73E8;
7 | --theme-text-color: #FFF;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | background-color: var(--background-color);
13 | color: var(--text-color);
14 | font-family: sans-serif;
15 | min-height: 100vh;
16 | }
17 | #loading {
18 | display: flex;
19 | position: absolute;
20 | z-index: 1000;
21 | background-color: var(--background-color);
22 | color: var(--dark-gray);
23 | top: 0;
24 | left: 0;
25 | height: 100%;
26 | width: 100%;
27 | align-items: center;
28 | justify-content: center;
29 | text-align: center;
30 | }
31 | #loading.done {
32 | display: none;
33 | }
34 |
35 | #top {
36 | position: sticky;
37 | top: 0;
38 | left: 0;
39 | right: 0;
40 | background-color: var(--background-color);
41 | padding: 4px;
42 | z-index: 20;
43 | }
44 |
45 | #title {
46 | margin: 0;
47 | margin-bottom: 6px;
48 | font-size: 1.3em;
49 | }
50 |
51 | nav {
52 | display: flex;
53 | align-items: center;
54 | }
55 | nav .spacer {
56 | flex: 1;
57 | }
58 |
59 | nav button, nav select {
60 | font: inherit;
61 | color: inherit;
62 | background: 0;
63 | font-size: 0.8em;
64 | border: 1px solid var(--border-color);
65 | border-radius: 3px;
66 | padding: 4px 8px;
67 | }
68 | nav #arrows {
69 | display: flex;
70 | align-items: center;
71 | margin: 0 10px;
72 | }
73 | nav #arrows button {
74 | border: 0;
75 | padding: 0;
76 | border-radius: 100px;
77 | height: 30px;
78 | width: 30px;
79 | display: block;
80 | cursor: pointer;
81 | transition: background-color .2s;
82 | outline: 0;
83 | }
84 | nav #arrows button:hover {
85 | background-color: rgba(0,0,0,.1);
86 | }
87 | nav #arrows button:focus {
88 | background-color: rgba(0,0,0,.2);
89 | }
90 | nav #arrows button svg {
91 | height: 24px;
92 | width: 24px;
93 | padding: 3px;
94 | }
95 |
96 | nav #date_label {
97 | display: flex;
98 | align-items: center;
99 | position: relative;
100 | }
101 | nav #date_label:hover,
102 | nav #date_label:focus,
103 | nav #date_label:focus-within {
104 | text-decoration: underline;
105 | }
106 | nav #date_label svg {
107 | height: 24px;
108 | width: 24px;
109 | padding-left: 4px;
110 | }
111 | nav #date_dropdown {
112 | position: absolute;
113 | top: 100%;
114 | background-color: var(--background-color);
115 | width: 100%;
116 | left: 0;
117 | opacity: 0;
118 | pointer-events: none;
119 | }
120 | nav #date_label:focus #date_dropdown,
121 | nav #date_label:focus-within #date_dropdown {
122 | opacity: 1;
123 | pointer-events: all;
124 | }
125 | nav #date {
126 | width: 100%;
127 | }
128 |
129 | #month.hidden, #agenda.hidden {
130 | display: none;
131 | }
132 |
133 | #agenda {
134 | table-layout: fixed;
135 | border-collapse: collapse;
136 | width: calc(100% - 8px);
137 | box-sizing: border-box;
138 | margin: 4px;
139 | }
140 | #emptystate td {
141 | text-align: left !important;
142 | color: var(--dark-gray);
143 | }
144 | #agenda .emptyday {
145 | padding-left: 5px;
146 | vertical-align: middle;
147 | font-style: italic;
148 | color: var(--dark-gray);
149 | }
150 | #agenda tr td {
151 | padding-top: 14px;
152 | vertical-align: top;
153 | }
154 | #agenda tr td:first-child {
155 | width: 60px;
156 | text-align: center;
157 | }
158 | #agenda .dayname,
159 | #agenda .month {
160 | display: block;
161 | font-size: .6em;
162 | font-weight: bold;
163 | letter-spacing: .1em;
164 | color: var(--dark-gray);
165 | }
166 | #agenda .day {
167 | display: flex;
168 | font-size: 1.2em;
169 | align-items: center;
170 | justify-content: center;
171 | }
172 | #agenda .today .dayname,
173 | #agenda .today .month {
174 | color: var(--theme-color);
175 | }
176 | #agenda .today .day span {
177 | display: flex;
178 | align-items: center;
179 | justify-content: center;
180 | color: var(--theme-text-color);
181 | background-color: var(--theme-color);
182 | height: 1.6em;
183 | width: 1.6em;
184 | border-radius: 100px;
185 | margin: 4px 0;
186 | }
187 |
188 | #agenda .event {
189 | background-color: var(--theme-color);
190 | color: var(--theme-text-color);
191 | margin-bottom: 8px;
192 | border-radius: 3px;
193 | margin-left: 5px;
194 | }
195 | #agenda .event a {
196 | color: inherit;
197 | }
198 | #agenda .event .summary {
199 | padding: 10px;
200 | cursor: pointer;
201 | }
202 | #agenda .event.open.always .summary {
203 | cursor: initial;
204 | }
205 | #agenda .event .name {
206 | display: block;
207 | font-weight: bold;
208 | }
209 | #agenda .event:hover .name,
210 | #agenda .event:focus .name,
211 | #agenda .event.open .name {
212 | text-decoration: underline;
213 | }
214 | #agenda .event.open.always .name {
215 | text-decoration: none;
216 | }
217 | #agenda .event.open .time {
218 | display: none;
219 | }
220 | #agenda .event .details {
221 | display: none;
222 | padding: 0 10px 10px;
223 | }
224 | #agenda .event.open .details {
225 | margin-top: 4px;
226 | display: block;
227 | }
228 |
229 | #agenda .indicator {
230 | height: 2px;
231 | width: 100%;
232 | background-color: var(--text-color);
233 | margin-bottom: 8px;
234 | position: relative;
235 | }
236 | #agenda .indicator:before {
237 | content: '';
238 | position: absolute;
239 | left: 0;
240 | top: -4px;
241 | height: 10px;
242 | width: 10px;
243 | background-color: var(--text-color);
244 | border-radius: 100px;
245 | }
246 |
247 |
248 | #month {
249 | table-layout: fixed;
250 | border-collapse: collapse;
251 | width: calc(100% - 8px);
252 | box-sizing: border-box;
253 | margin: 4px;
254 | }
255 | #month td {
256 | border: 1px solid var(--border-color);
257 | vertical-align: top;
258 | overflow: hidden;
259 | }
260 | #month .labels td {
261 | border: 0;
262 | text-align: center;
263 | color: var(--dark-gray);
264 | font-weight: bold;
265 | height: 1px;
266 | }
267 | #month td .date {
268 | text-align: center;
269 | display: block;
270 | color: var(--dark-gray);
271 | margin-top: 1px;
272 | }
273 | #month td .date.current {
274 | color: var(--text-color);
275 | }
276 | #month td .date span {
277 | display: inline-flex;
278 | align-items: center;
279 | justify-content: center;
280 | height: 1.7em;
281 | width: 1.7em;
282 | border-radius: 100px;
283 | }
284 | #month td .date.today span {
285 | background-color: var(--theme-color);
286 | color: var(--theme-text-color);
287 | }
288 | #month .event {
289 | display: block;
290 | white-space: nowrap;
291 | cursor: pointer;
292 | user-select: none;
293 | font-size: .9em;
294 | background-color: var(--theme-color);
295 | color: var(--theme-text-color);
296 | margin: 2px 0;
297 | box-sizing: border-box;
298 | padding: 2px 4px;
299 | border-radius: 3px;
300 | }
301 | #month .past .event {
302 | opacity: .6;
303 | }
304 |
305 | #monthDetails {
306 | position: fixed;
307 | top: 0;
308 | left: 0;
309 | right: 0;
310 | bottom: 0;
311 | pointer-events: none;
312 | z-index: 5000;
313 | display: none;
314 | overflow-y: auto;
315 | }
316 | #monthDetails.shown {
317 | display: flex;
318 | }
319 | #monthDetails .dialog {
320 | background-color: var(--background-color);
321 | pointer-events: all;
322 | max-width: 60%;
323 | box-shadow: 0 5px 30px rgba(0,0,0,.6);
324 | border-radius: 10px;
325 | box-sizing: border-box;
326 | padding: 40px 20px 20px;
327 | position: relative;
328 | margin: auto;
329 | }
330 | #monthDetails button {
331 | position: absolute;
332 | top: 14px;
333 | right: 14px;
334 | border: 0;
335 | padding: 0;
336 | border-radius: 100px;
337 | height: 30px;
338 | width: 30px;
339 | display: block;
340 | cursor: pointer;
341 | transition: background-color .2s;
342 | outline: 0;
343 | background-color: transparent;
344 | }
345 | #monthDetails button:hover {
346 | background-color: rgba(0,0,0,.1);
347 | }
348 | #monthDetails button:focus {
349 | background-color: rgba(0,0,0,.2);
350 | }
351 | #monthDetails button svg {
352 | height: 24px;
353 | width: 24px;
354 | padding: 3px;
355 | }
356 | #monthDetails .summary {
357 | display: block;
358 | font-size: 1.6em;
359 | margin-bottom: 14px;
360 | }
361 |
--------------------------------------------------------------------------------
/embed.js:
--------------------------------------------------------------------------------
1 | const AGENDA_DAYS = 20;
2 | const DAYS_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
3 | const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
4 | let ampm = (h) => (h < 12 || h === 24) ? "am" : "pm";
5 |
6 | const url = new URL(window.location.href);
7 | const loading = document.getElementById('loading');
8 |
9 | const ical = url.searchParams.get('ical');
10 | let show_title = url.searchParams.get('title') || 1;
11 | const show_nav = url.searchParams.get('nav') || 1;
12 | const show_date = url.searchParams.get('date') || 1;
13 | const show_details = url.searchParams.get('details') || 0;
14 | const show_view = url.searchParams.get('view') || 1;
15 | const default_view = url.searchParams.get('dview') || 0;
16 | const monday_start = url.searchParams.get('monstart') || 0;
17 | const color = url.searchParams.get('color') || '#1A73E8';
18 | const colorBG = url.searchParams.get('colorbg') || '#FFFFFF';
19 | const colorText = url.searchParams.get('colortxt') || '#000000';
20 | const colorThemeText = url.searchParams.get('colorsecondarytxt') || '#FFFFFF';
21 |
22 | let today = new Date();
23 | today.setHours(0,0,0,0);
24 | let selectedDay = new Date(today.valueOf());
25 | let selectedView = default_view;
26 |
27 | function getHumanDate(date) {
28 | return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,0)}-${date.getDate().toString().padStart(2,0)}`;
29 | }
30 |
31 | function createDateCell(date, todayd = false) {
32 | let day = date.getDay();
33 | let dateM = date.getDate();
34 | let month = date.getMonth();
35 | let dateCell = document.createElement('td');
36 | dateCell.tabIndex = '-1';
37 | dateCell.dataset.date = getHumanDate(date);
38 | dateCell.onfocus = () => {
39 | selectDay(getHumanDate(date), false)
40 | };
41 | if (todayd) {
42 | dateCell.className = 'today';
43 | }
44 | let dayEl = document.createElement('span');
45 | dayEl.className = 'dayname';
46 | dayEl.appendChild(document.createTextNode(DAYS_OF_WEEK[day].substring(0,3).toUpperCase()));
47 | dateCell.appendChild(dayEl);
48 | let dateEl = document.createElement('span');
49 | dateEl.className = 'day';
50 | let dateSpan = document.createElement('span');
51 | dateSpan.appendChild(document.createTextNode(dateM));
52 | dateEl.appendChild(dateSpan);
53 | dateCell.appendChild(dateEl);
54 | let monthEl = document.createElement('span');
55 | monthEl.className = 'month';
56 | monthEl.appendChild(document.createTextNode(MONTHS[month].substring(0,3).toUpperCase()));
57 | dateCell.appendChild(monthEl);
58 | return dateCell;
59 | }
60 |
61 | function selectDay(date, focus = true, events = null) {
62 | let newSelection = new Date(date + 'T00:00');
63 | let newMonth = false;
64 | if (selectedDay.getMonth() != newSelection.getMonth() && events != null) {
65 | renderMonth(events, newSelection);
66 | newMonth = true;
67 | }
68 |
69 | selectedDay = newSelection;
70 |
71 | document.querySelector('#date_label span').innerHTML = `${DAYS_OF_WEEK[selectedDay.getDay()]}, ${MONTHS[selectedDay.getMonth()]} ${selectedDay.getDate()}`;
72 | document.getElementById('date').value = getHumanDate(selectedDay);
73 |
74 | let selectedElement = document.querySelector(`td[data-date='${getHumanDate(selectedDay)}']`);
75 | if (selectedElement && (focus || newMonth)) {
76 | selectedElement.focus();
77 | }
78 | }
79 |
80 | function setView(newView, events) {
81 | selectedView = newView;
82 | if (selectedView == 1) {
83 | renderMonth(events);
84 | document.getElementById('agenda').classList.add('hidden');
85 | } else {
86 | renderAgenda(events);
87 | document.getElementById('month').classList.add('hidden');
88 | }
89 | }
90 |
91 | function eventDetails(event) {
92 | let startTime = `${(event.startDate.getHours() % 12) || 12}:${event.startDate.getMinutes() < 10 ? '0' : ''}${event.startDate.getMinutes()}`;
93 | let endTime = `${(event.endDate.getHours() % 12) || 12}:${event.endDate.getMinutes() < 10 ? '0' : ''}${event.endDate.getMinutes()}`;
94 | let startM = ampm(event.startDate.getHours());
95 | let endM = ampm(event.endDate.getHours());
96 |
97 | let eDetails = document.createElement('div');
98 | eDetails.className = 'details';
99 |
100 | let whenLabel = document.createElement('strong');
101 | whenLabel.appendChild(document.createTextNode('When: '));
102 | let when = document.createElement('span');
103 | when.className = 'when';
104 | let whenText = `${DAYS_OF_WEEK[event.startDate.getDay()].substring(0,3)}, ${MONTHS[event.startDate.getMonth()]} ${event.startDate.getDate()}, ${startTime}${startM} - ${endTime}${endM}`;
105 | if (event.days == 1 && event.allDay) {
106 | whenText = `${DAYS_OF_WEEK[event.startDate.getDay()]}, ${MONTHS[event.startDate.getMonth()].substring(0,3)} ${event.startDate.getDate()}, ${event.startDate.getFullYear()}`;
107 | } else if (event.days % 1 == 0 && event.allDay) {
108 | let newEnd = new Date(event.endDate.valueOf());
109 | newEnd.setDate(newEnd.getDate()-1);
110 | whenText = `${MONTHS[event.startDate.getMonth()].substring(0,3)} ${event.startDate.getDate()} - ${MONTHS[newEnd.getMonth()].substring(0,3)} ${newEnd.getDate()}, ${event.startDate.getFullYear()}`;
111 | } else if (event.days > 1) {
112 | whenText = `${MONTHS[event.startDate.getMonth()]} ${event.startDate.getDate()}, ${startTime}${startM} - ${MONTHS[event.endDate.getMonth()]} ${event.endDate.getDate()}, ${endTime}${endM}`;
113 | }
114 |
115 | when.appendChild(document.createTextNode(whenText));
116 | eDetails.appendChild(whenLabel);
117 | eDetails.appendChild(when);
118 |
119 | if (event.location != '') {
120 | eDetails.appendChild(document.createElement('br'));
121 | let whereLabel = document.createElement('strong');
122 | whereLabel.appendChild(document.createTextNode('Where: '));
123 | let where = document.createElement('span');
124 | where.className = 'where';
125 | let whereText = document.createTextNode(event.location);
126 | if (event.location.startsWith('http')) {
127 | whereText = document.createElement('a');
128 | whereText.href = event.location;
129 | whereText.target = '_blank';
130 | whereText.appendChild(document.createTextNode(event.location));
131 | }
132 | where.appendChild(whereText);
133 | eDetails.appendChild(whereLabel);
134 | eDetails.appendChild(where);
135 | }
136 |
137 | if (event.description != '') {
138 | eDetails.appendChild(document.createElement('br'));
139 | let descLabel = document.createElement('strong');
140 | descLabel.appendChild(document.createTextNode('Description: '));
141 | let desc = document.createElement('span');
142 | desc.className = 'description';
143 | desc.innerHTML = event.description;
144 | eDetails.appendChild(descLabel);
145 | eDetails.appendChild(desc);
146 | }
147 |
148 | return eDetails;
149 | }
150 |
151 | function renderAgenda(events) {
152 | // Filter after today
153 | events = events.filter((e) => {
154 | let end = new Date(e.endDate.valueOf());
155 | end.setHours(0,0,0,0);
156 | return end >= today;
157 | });
158 |
159 | // Create elements
160 | let days = [];
161 | let row;
162 | let column;
163 | let prevDay = null;
164 | let indicator = document.createElement('div');
165 | indicator.className = 'indicator';
166 | let nowDate = new Date();
167 | let now = `${(nowDate.getHours() % 12) || 12}:${nowDate.getMinutes() < 10 ? '0' : ''}${nowDate.getMinutes()}`;
168 | let nowM = ampm(nowDate.getHours());
169 | indicator.title = `${now} ${nowM}`;
170 | let indicatorset = false;
171 | let todayHasEvents = false;
172 | for (let i = 0; i < (events.length < AGENDA_DAYS ? events.length : AGENDA_DAYS); i++) {
173 | let tomorrow = new Date(today.valueOf());
174 | tomorrow.setDate(tomorrow.getDate() + 1);
175 | if (events[i].startDate > tomorrow && !todayHasEvents) {
176 | todayHasEvents = true;
177 | row = document.createElement('tr');
178 | row.appendChild(createDateCell(
179 | events[i].startDate,
180 | true
181 | ));
182 | column = document.createElement('td');
183 | column.className = 'emptyday';
184 | column.appendChild(document.createTextNode('No events today'));
185 | row.appendChild(column);
186 | days.push(row);
187 | }
188 | if (prevDay != events[i].startDate.toDateString()) {
189 | prevDay = events[i].startDate.toDateString();
190 | row = document.createElement('tr');
191 |
192 | let curDay = new Date(events[i].startDate.valueOf());
193 | curDay.setHours(0,0,0,0);
194 | if (curDay.getTime() == today.getTime()) {
195 | todayHasEvents = true;
196 | }
197 |
198 | row.appendChild(createDateCell(
199 | events[i].startDate,
200 | curDay.getTime() == today.getTime()
201 | ));
202 | column = document.createElement('td');
203 | }
204 |
205 | // Indicator
206 | let eventDay = new Date(events[i].endDate.valueOf());
207 | eventDay.setHours(0,0,0,0);
208 | if (nowDate < events[i].endDate && !indicatorset && today.getTime() == eventDay.getTime()) {
209 | column.appendChild(indicator);
210 | indicatorset = true;
211 | }
212 |
213 | let event = document.createElement('div');
214 | event.className = 'event';
215 |
216 | let summary = document.createElement('div');
217 | summary.className = 'summary';
218 | if (show_details == 0) {
219 | summary.tabIndex = '0';
220 | summary.onkeypress = (e) => {
221 | if (e.keyCode === 13) {
222 | event.classList.toggle('open');
223 | }
224 | };
225 | summary.onclick = () => event.classList.toggle('open');
226 | } else {
227 | event.className = 'event open always';
228 | }
229 |
230 | let eName = document.createElement('span');
231 | eName.className = 'name';
232 | eName.appendChild(document.createTextNode(events[i].name));
233 | summary.appendChild(eName);
234 |
235 | let startTime = `${(events[i].startDate.getHours() % 12) || 12}:${events[i].startDate.getMinutes() < 10 ? '0' : ''}${events[i].startDate.getMinutes()}`;
236 | let endTime = `${(events[i].endDate.getHours() % 12) || 12}:${events[i].endDate.getMinutes() < 10 ? '0' : ''}${events[i].endDate.getMinutes()}`;
237 | let startM = ampm(events[i].startDate.getHours());
238 | let endM = ampm(events[i].endDate.getHours());
239 |
240 | if (!events[i].allDay) {
241 | let eTime = document.createElement('span');
242 | eTime.className = 'time';
243 | let timeText = `${startTime} ${startM == endM ? '' : startM} - ${endTime} ${endM}`;
244 | if (events[i].days === 0) {
245 | timeText = `${startTime} ${startM}`;
246 | } else if (events[i].days > 1 && !events[i].allDay) {
247 | timeText = `${MONTHS[events[i].startDate.getMonth()]} ${events[i].startDate.getDate()}, ${startTime}${startM} - ${MONTHS[events[i].endDate.getMonth()]} ${events[i].endDate.getDate()}, ${endTime}${endM}`;
248 | }
249 | eTime.appendChild(document.createTextNode(timeText));
250 | summary.appendChild(eTime);
251 | }
252 | event.appendChild(summary);
253 |
254 | event.appendChild(eventDetails(events[i]));
255 |
256 | column.appendChild(event);
257 |
258 | if (events[i].endDate < nowDate && today.getTime() == eventDay.getTime()) {
259 | column.appendChild(indicator);
260 | }
261 |
262 | if (i+1 == events.length || events[i].startDate.toDateString() != events[i+1].startDate.toDateString()) {
263 | row.appendChild(column);
264 | days.push(row);
265 | }
266 | }
267 |
268 | let agenda = document.getElementById('agenda');
269 | agenda.innerHTML = '';
270 | agenda.classList.remove('hidden');
271 | for (let i = 0; i < days.length; i++) {
272 | agenda.appendChild(days[i]);
273 | }
274 |
275 | // Empty state
276 | if (events.length == 0) {
277 | let emptystate = document.createElement('tr');
278 | emptystate.id = 'emptystate';
279 | let emptydata = document.createElement('td');
280 | emptydata.appendChild(document.createTextNode('No upcoming events'));
281 | emptystate.appendChild(emptydata);
282 | agenda.appendChild(emptystate);
283 | }
284 | }
285 |
286 | function showMonthDetails(event) {
287 | let details = document.getElementById('monthDetails');
288 |
289 | document.querySelector('#monthDetails .summary').innerHTML = event.name;
290 |
291 | document.querySelector('#monthDetails .details').innerHTML = '';
292 | document.querySelector('#monthDetails .details').appendChild(eventDetails(event));
293 |
294 | details.classList.add('shown');
295 | }
296 |
297 | function renderMonth(events, fromDay = new Date(today.valueOf())) {
298 | let monthStartDate = new Date(fromDay.getFullYear(), fromDay.getMonth(), 1);
299 | let monthEndDate = new Date(fromDay.getFullYear(), fromDay.getMonth() + 1, 0);
300 | while (monthStartDate.getDay() != monday_start) {
301 | monthStartDate.setDate(monthStartDate.getDate() - 1);
302 | }
303 | while (monthEndDate.getDay() != (monday_start == 1 ? 0 : 6)) {
304 | monthEndDate.setDate(monthEndDate.getDate() + 1);
305 | }
306 | let days = (monthEndDate - monthStartDate) / (24 * 60 * 60 * 1000) + 1;
307 | let weeks = days/7;
308 |
309 | let rows = [];
310 |
311 | // Labels
312 | let labelRow = document.createElement('tr');
313 | labelRow.className = 'labels';
314 | for (let i = 0; i < 7; i++) {
315 | let label = document.createElement('td');
316 | let n = i + parseInt(monday_start);
317 | label.appendChild(document.createTextNode(DAYS_OF_WEEK[(n == 7 ? 0 : n)].substring(0,3)));
318 | labelRow.appendChild(label);
319 | }
320 | rows.push(labelRow);
321 |
322 | let day = new Date(monthStartDate.valueOf());
323 | for (let i = 0; i < weeks; i++) {
324 | let weekRow = document.createElement('tr');
325 | for (let j = 0; j < 7; j++) {
326 | let dayCell = document.createElement('td');
327 | dayCell.dataset.date = getHumanDate(day);
328 | dayCell.onfocus = () => {
329 | selectDay(dayCell.dataset.date, false, events);
330 | };
331 | dayCell.tabIndex = '-1';
332 | if (day < today) {
333 | dayCell.className = 'past';
334 | }
335 | let dateEl = document.createElement('span');
336 | dateEl.classList.add('date');
337 | if (day.getMonth() == fromDay.getMonth()) {
338 | dateEl.classList.add('current');
339 | }
340 | if (getHumanDate(day) == getHumanDate(today)) {
341 | dateEl.classList.add('today');
342 | }
343 | let dateText = document.createElement('span');
344 | dateText.appendChild(document.createTextNode(day.getDate()));
345 | dateEl.appendChild(dateText);
346 | dayCell.appendChild(dateEl);
347 |
348 | let dayEvents = events.filter((e) => getHumanDate(e.startDate) == getHumanDate(day));
349 |
350 | for (let e = 0; e < dayEvents.length; e++) {
351 | let event = document.createElement('div');
352 | event.className = 'event';
353 | event.tabIndex = '0';
354 | event.appendChild(document.createTextNode(dayEvents[e].name));
355 | event.onkeypress = (e) => {
356 | if (e.keyCode === 13) {
357 | showMonthDetails(dayEvents[e]);
358 | }
359 | };
360 | event.onclick = () => {showMonthDetails(dayEvents[e])};
361 | dayCell.appendChild(event);
362 | }
363 | weekRow.appendChild(dayCell);
364 |
365 | day.setDate(day.getDate()+1);
366 | }
367 | rows.push(weekRow);
368 | }
369 |
370 | let topHeight = 0;
371 | let topEl = document.getElementById('top');
372 | if (topEl.style.display != 'none') {
373 | topHeight = topEl.clientHeight;
374 | }
375 | let monthEl = document.getElementById('month');
376 | monthEl.style.height = `calc(100vh - ${topHeight+8}px)`;
377 | monthEl.innerHTML = '';
378 | monthEl.classList.remove('hidden');
379 | for (let i = 0; i < rows.length; i++) {
380 | monthEl.appendChild(rows[i]);
381 | }
382 | }
383 |
384 | function renderCalendar(meta, events) {
385 | // Sort events
386 | events.sort((a,b) => a.startDate - b.startDate);
387 |
388 | // Title
389 | if (show_title == 1) {
390 | show_title = meta.calname != null;
391 | }
392 | if (show_title == 1) {
393 | document.getElementById('title').innerHTML = meta.calname;
394 | } else {
395 | document.getElementById('title').style.display = 'none';
396 | }
397 |
398 | // Nav
399 | let btn_today = document.getElementById('btn_today');
400 | let arrows = document.getElementById('arrows');
401 | btn_today.onclick = () => {
402 | // Scroll to today
403 | selectDay(getHumanDate(today), true, events);
404 | };
405 | document.getElementById('btn_prev').onclick = () => {
406 | let prevDay = new Date(selectedDay.valueOf());
407 | prevDay.setDate(prevDay.getDate() - 1);
408 | selectDay(getHumanDate(prevDay), true, events);
409 | };
410 | document.getElementById('btn_next').onclick = () => {
411 | let prevDay = new Date(selectedDay.valueOf());
412 | prevDay.setDate(prevDay.getDate() + 1);
413 | selectDay(getHumanDate(prevDay), true, events);
414 | };
415 | if (show_nav == 0) {
416 | btn_today.style.display = 'none';
417 | arrows.style.display = 'none';
418 | }
419 |
420 | // View
421 | let view = document.getElementById('view');
422 | view.value = default_view;
423 | view.onchange = () => {
424 | setView(view.value, events);
425 | };
426 | if (show_view == 0) {
427 | view.style.display = 'none';
428 | }
429 |
430 | // Date
431 | let date_label = document.getElementById('date_label');
432 | let date_input = document.getElementById('date');
433 | document.querySelector('#date_label span').innerHTML = `${DAYS_OF_WEEK[selectedDay.getDay()]}, ${MONTHS[selectedDay.getMonth()]} ${selectedDay.getDate()}`;
434 | date_input.value = getHumanDate(selectedDay);
435 | date_input.onchange = () => {
436 | selectDay(date_input.value, true, events);
437 | };
438 | if (show_date == 0) {
439 | date_label.style.display = 'none';
440 | }
441 |
442 | // Remove nav element
443 | if (show_title == 0 && show_nav == 0 && show_date == 0 && show_view == 0) {
444 | document.getElementById('top').style.display = 'none';
445 | }
446 |
447 | // Colors
448 | document.documentElement.style.setProperty('--theme-color', color);
449 | document.documentElement.style.setProperty('--text-color', colorText);
450 | document.documentElement.style.setProperty('--background-color', colorBG);
451 | document.documentElement.style.setProperty('--theme-text-color', colorThemeText);
452 |
453 | setView(selectedView, events);
454 |
455 | loading.style.display = 'none';
456 | }
457 |
458 | function parseCalendar(data) {
459 | let jCal = ICAL.parse(data);
460 | let comp = new ICAL.Component(jCal);
461 |
462 | const meta = {
463 | calname: comp.getFirstPropertyValue('x-wr-calname'),
464 | timezone: new ICAL.Timezone(comp.getFirstSubcomponent('vtimezone')).tzid,
465 | caldesc: comp.getFirstPropertyValue('x-wr-caldesc')
466 | };
467 |
468 | let eventData = comp.getAllSubcomponents('vevent');
469 | let events = [];
470 |
471 | // Copy event data to custom array
472 | for (let i = 0; i < eventData.length; i++) {
473 | let event = new ICAL.Event(eventData[i]);
474 | let duration = event.endDate.subtractDate(event.startDate);
475 | events.push({
476 | uid: event.uid,
477 | name: event.summary,
478 | location: event.location,
479 | description: event.description,
480 | startDate: event.startDate.toJSDate(),
481 | endDate: event.endDate.toJSDate(),
482 | allDay: event.startDate.isDate,
483 | days: (duration.toSeconds()/86400)
484 | });
485 | if (event.isRecurring()) {
486 | let expand = new ICAL.RecurExpansion({
487 | component: eventData[i],
488 | dtstart: event.startDate
489 | });
490 |
491 | let j = 0;
492 | let next;
493 | while (j < 10 && (next = expand.next())) {
494 | if (j > 0) {
495 | let endDate = next.clone();
496 | endDate.addDuration(duration);
497 | events.push({
498 | uid: event.uid,
499 | name: event.summary,
500 | location: event.location,
501 | description: event.description,
502 | startDate: next.toJSDate(),
503 | endDate: endDate.toJSDate(),
504 | allDay: event.startDate.isDate,
505 | days: (duration.toSeconds()/86400)
506 | });
507 | }
508 | j++;
509 | }
510 | }
511 | }
512 | renderCalendar(meta, events);
513 | }
514 |
515 | if (ical) {
516 | fetch(ical).then((response) => {
517 | response.text().then((text) => {
518 | parseCalendar(text);
519 | });
520 | }).catch((e) => {
521 | console.error(e);
522 | loading.innerHTML = "Error: iCal URL doesn't exist or isn't valid iCal links (like those from Google calendar) will need to use a cors proxy";
523 | });
524 | } else {
525 | loading.innerHTML = "Error: no iCal URL provided";
526 | }
527 |
--------------------------------------------------------------------------------
/ical.min.js:
--------------------------------------------------------------------------------
1 | "object"==typeof module?ICAL=module.exports:"object"!=typeof ICAL&&(this.ICAL={}),ICAL.foldLength=75,ICAL.newLineChar="\r\n",ICAL.helpers={updateTimezones:function(t){var e,r,n,i,a,s;if(!t||"vcalendar"!==t.name)return t;for(e=t.getAllSubcomponents(),r=[],n={},a=0;a>18&63,r=a>>12&63,n=a>>6&63,i=63&a,c[u++]=s.charAt(e)+s.charAt(r)+s.charAt(n)+s.charAt(i),o>16&255,r=s>>8&255,n=255&s,c[h++]=64==i?String.fromCharCode(e):64==a?String.fromCharCode(e,r):String.fromCharCode(e,r,n),u=this.changes.length)break}var s=this.changes[n];if(s.utcOffset-s.prevUtcOffset<0&&0=this.changes.length?this.changes.length-1:e},_ensureCoverage:function(t){if(-1==ICAL.Timezone._minimumExpansionYear){var e=ICAL.Time.now();ICAL.Timezone._minimumExpansionYear=e.year}var r=t;if(rICAL.Timezone.MAX_YEAR&&(r=ICAL.Timezone.MAX_YEAR),!this.changes.length||this.expandedUntilYeart)&&l);)n.year=l.year,n.month=l.month,n.day=l.day,n.hour=l.hour,n.minute=l.minute,n.second=l.second,n.isDate=l.isDate,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n)}}else(n=s()).year=i.year,n.month=i.month,n.day=i.day,n.hour=i.hour,n.minute=i.minute,n.second=i.second,ICAL.Timezone.adjust_change(n,0,0,0,-n.prevUtcOffset),r.push(n);return r},toString:function(){return this.tznames?this.tznames:this.tzid}},ICAL.Timezone._compare_change_fn=function(t,e){return t.yeare.year?1:t.monthe.month?1:t.daye.day?1:t.houre.hour?1:t.minutee.minute?1:t.seconde.second?1:0},ICAL.Timezone.convert_time=function(t,e,r){if(t.isDate||e.tzid==r.tzid||e==ICAL.Timezone.localTimezone||r==ICAL.Timezone.localTimezone)return t.zone=r,t;var n=e.utcOffset(t);return t.adjust(0,0,0,-n),n=r.utcOffset(t),t.adjust(0,0,0,n),null},ICAL.Timezone.fromData=function(t){return(new ICAL.Timezone).fromData(t)},ICAL.Timezone.utcTimezone=ICAL.Timezone.fromData({tzid:"UTC"}),ICAL.Timezone.localTimezone=ICAL.Timezone.fromData({tzid:"floating"}),ICAL.Timezone.adjust_change=function(t,e,r,n,i){return ICAL.Time.prototype.adjust.call(t,e,r,n,i,t)},ICAL.Timezone._minimumExpansionYear=-1,ICAL.Timezone.MAX_YEAR=2035,ICAL.Timezone.EXTRA_COVERAGE=5}(),ICAL.TimezoneService=function(){var r,t={get count(){return Object.keys(r).length},reset:function(){r=Object.create(null);var t=ICAL.Timezone.utcTimezone;r.Z=t,r.UTC=t,r.GMT=t},has:function(t){return!!r[t]},get:function(t){return r[t]},register:function(t,e){if(t instanceof ICAL.Component&&"vtimezone"===t.name&&(t=(e=new ICAL.Timezone(t)).tzid),!(e instanceof ICAL.Timezone))throw new TypeError("timezone must be ICAL.Timezone or ICAL.Component");r[t]=e},remove:function(t){return delete r[t]}};return t.reset(),t}(),function(){function t(e){Object.defineProperty(ICAL.Time.prototype,e,{get:function(){return this._pendingNormalization&&(this._normalize(),this._pendingNormalization=!1),this._time[e]},set:function(t){return"isDate"===e&&t&&!this._time.isDate&&this.adjust(0,0,0,0),this._cachedUnixTime=null,this._pendingNormalization=!0,this._time[e]=t}})}ICAL.Time=function(t,e){var r=(this.wrappedJSObject=this)._time=Object.create(null);r.year=0,r.month=1,r.day=1,r.hour=0,r.minute=0,r.second=0,r.isDate=!1,this.fromData(t,e)},ICAL.Time._dowCache={},ICAL.Time._wnCache={},ICAL.Time.prototype={icalclass:"icaltime",_cachedUnixTime:null,get icaltype(){return this.isDate?"date":"date-time"},zone:null,_pendingNormalization:!1,clone:function(){return new ICAL.Time(this._time,this.zone)},reset:function(){this.fromData(ICAL.Time.epochTime),this.zone=ICAL.Timezone.utcTimezone},resetTo:function(t,e,r,n,i,a,s){this.fromData({year:t,month:e,day:r,hour:n,minute:i,second:a,zone:s})},fromJSDate:function(t,e){return t?e?(this.zone=ICAL.Timezone.utcTimezone,this.year=t.getUTCFullYear(),this.month=t.getUTCMonth()+1,this.day=t.getUTCDate(),this.hour=t.getUTCHours(),this.minute=t.getUTCMinutes(),this.second=t.getUTCSeconds()):(this.zone=ICAL.Timezone.localTimezone,this.year=t.getFullYear(),this.month=t.getMonth()+1,this.day=t.getDate(),this.hour=t.getHours(),this.minute=t.getMinutes(),this.second=t.getSeconds()):this.reset(),this._cachedUnixTime=null,this},fromData:function(t,e){if(t)for(var r in t)if(Object.prototype.hasOwnProperty.call(t,r)){if("icaltype"===r)continue;this[r]=t[r]}if(e&&(this.zone=e),!t||"isDate"in t?t&&"isDate"in t&&(this.isDate=t.isDate):this.isDate=!("hour"in t),t&&"timezone"in t){var n=ICAL.TimezoneService.get(t.timezone);this.zone=n||ICAL.Timezone.localTimezone}return t&&"zone"in t&&(this.zone=t.zone),this.zone||(this.zone=ICAL.Timezone.localTimezone),this._cachedUnixTime=null,this},dayOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+e;if(r in ICAL.Time._dowCache)return ICAL.Time._dowCache[r];var n=this.day,i=this.month+(this.month<3?12:0),a=this.year-(this.month<3?1:0),s=n+a+ICAL.helpers.trunc(26*(i+1)/10)+ICAL.helpers.trunc(a/4);return s=((s+=6*ICAL.helpers.trunc(a/100)+ICAL.helpers.trunc(a/400))+7-e)%7+1,ICAL.Time._dowCache[r]=s},dayOfYear:function(){var t=ICAL.Time.isLeapYear(this.year)?1:0;return ICAL.Time.daysInYearPassedMonth[t][this.month-1]+this.day},startOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day-=(this.dayOfWeek()+7-e)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},endOfWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.clone();return r.day+=(7-this.dayOfWeek()+e-ICAL.Time.SUNDAY)%7,r.isDate=!0,r.hour=0,r.minute=0,r.second=0,r},startOfMonth:function(){var t=this.clone();return t.day=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfMonth:function(){var t=this.clone();return t.day=ICAL.Time.daysInMonth(t.month,t.year),t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startOfYear:function(){var t=this.clone();return t.day=1,t.month=1,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},endOfYear:function(){var t=this.clone();return t.day=31,t.month=12,t.isDate=!0,t.hour=0,t.minute=0,t.second=0,t},startDoyWeek:function(t){var e=t||ICAL.Time.SUNDAY,r=this.dayOfWeek()-e;return r<0&&(r+=7),this.dayOfYear()-r},getDominicalLetter:function(){return ICAL.Time.getDominicalLetter(this.year)},nthWeekDay:function(t,e){var r,n=ICAL.Time.daysInMonth(this.month,this.year),i=e,a=0,s=this.clone();if(0<=i){s.day=1,0!=i&&i--,a=s.day;var o=t-s.dayOfWeek();o<0&&(o+=7),a+=o,a-=t,r=t}else{s.day=n,i++,(r=s.dayOfWeek()-t)<0&&(r+=7),r=n-r}return a+(r+=7*i)},isNthWeekDay:function(t,e){var r=this.dayOfWeek();return 0===e&&r===t||this.nthWeekDay(t,e)===this.day},weekNumber:function(t){var e,r=(this.year<<12)+(this.month<<8)+(this.day<<3)+t;if(r in ICAL.Time._wnCache)return ICAL.Time._wnCache[r];var n=this.clone();n.isDate=!0;var i=this.year;12==n.month&&25ICAL.Time.daysInYearPassedMonth[a][12])return a=ICAL.Time.isLeapYear(r)?1:0,n-=ICAL.Time.daysInYearPassedMonth[a][12],r++,ICAL.Time.fromDayOfYear(n,r);i.year=r,i.isDate=!0;for(var s=11;0<=s;s--)if(n>ICAL.Time.daysInYearPassedMonth[a][s]){i.month=s+1,i.day=n-ICAL.Time.daysInYearPassedMonth[a][s];break}return i.auto_normalize=!0,i},ICAL.Time.fromStringv2=function(t){return new ICAL.Time({year:parseInt(t.substr(0,4),10),month:parseInt(t.substr(5,2),10),day:parseInt(t.substr(8,2),10),isDate:!0})},ICAL.Time.fromDateString=function(t){return new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),isDate:!0})},ICAL.Time.fromDateTimeString=function(t,e){if(t.length<19)throw new Error('invalid date-time value: "'+t+'"');var r;return t[19]&&"Z"===t[19]?r="Z":e&&(r=e.getParameter("tzid")),new ICAL.Time({year:ICAL.helpers.strictParseInt(t.substr(0,4)),month:ICAL.helpers.strictParseInt(t.substr(5,2)),day:ICAL.helpers.strictParseInt(t.substr(8,2)),hour:ICAL.helpers.strictParseInt(t.substr(11,2)),minute:ICAL.helpers.strictParseInt(t.substr(14,2)),second:ICAL.helpers.strictParseInt(t.substr(17,2)),timezone:r})},ICAL.Time.fromString=function(t,e){return 10ICAL.Time.THURSDAY&&(r.day+=7),i>ICAL.Time.THURSDAY&&(r.day-=7),r.day-=n-i,r},ICAL.Time.getDominicalLetter=function(t){var e="GFEDCBA",r=(t+(t/4|0)+(t/400|0)-(t/100|0)-1)%7;return ICAL.Time.isLeapYear(t)?e[(6+r)%7]+e[r]:e[r]},ICAL.Time.epochTime=ICAL.Time.fromData({year:1970,month:1,day:1,hour:0,minute:0,second:0,isDate:!1,timezone:"Z"}),ICAL.Time._cmp_attr=function(t,e,r){return t[r]>e[r]?1:t[r] '+e);if(void 0!==r&&rs||0==this.last.day)throw new Error("Malformed values in BYDAY part")}else if(this.has_by_data("BYMONTHDAY")&&this.last.day<0){s=ICAL.Time.daysInMonth(this.last.month,this.last.year);this.last.day=s+this.last.day+1}},next:function(){var t,e=this.last?this.last.clone():null;if(this.rule.count&&this.occurrence_number>=this.rule.count||this.rule.until&&0i)){if(n<0)n=i+(n+1);else if(0===n)continue;-1===a.indexOf(n)&&a.push(n)}return a.sort(function(t,e){return t-e})},_byDayAndMonthDay:function(t){var e,r,n,i,a=this.by_data.BYDAY,s=0,o=a.length,u=0,h=this,c=this.last.day;function l(){for(i=ICAL.Time.daysInMonth(h.last.month,h.last.year),e=h.normalizeByMonthDayRules(h.last.year,h.last.month,h.by_data.BYMONTHDAY),n=e.length;e[s]<=c&&(!t||e[s]!=c)&&s=this.by_data.BYMONTHDAY.length&&(this.by_indices.BYMONTHDAY=0,this.increment_month());e=ICAL.Time.daysInMonth(this.last.month,this.last.year);(a=this.by_data.BYMONTHDAY[this.by_indices.BYMONTHDAY])<0&&(a=e+a+1),ee?t=0:this.last.day=this.by_data.BYMONTHDAY[0]}return t},next_weekday_by_week:function(){var t=0;if(0==this.next_hour())return t;if(!this.has_by_data("BYDAY"))return 1;for(;;){var e=new ICAL.Time;this.by_indices.BYDAY++,this.by_indices.BYDAY==Object.keys(this.by_data.BYDAY).length&&(this.by_indices.BYDAY=0,t=1);var r=this.by_data.BYDAY[this.by_indices.BYDAY],n=this.ruleDayOfWeek(r)[1];(n-=this.rule.wkst)<0&&(n+=7),e.year=this.last.year,e.month=this.last.month,e.day=this.last.day;var i=e.startDoyWeek(this.rule.wkst);if(!(n+i<1)||t){var a=ICAL.Time.fromDayOfYear(i+n,this.last.year);return this.last.year=a.year,this.last.month=a.month,this.last.day=a.day,t}}},next_year:function(){if(0==this.next_hour())return 0;if(++this.days_index==this.days.length)for(this.days_index=0;this.increment_year(this.rule.interval),this.expand_year_days(this.last.year),0==this.days.length;);return this._nextByYearDay(),1},_nextByYearDay:function(){var t=this.days[this.days_index],e=this.last.year;t<1&&(t+=1,e+=1);var r=ICAL.Time.fromDayOfYear(t,e);this.last.day=r.day,this.last.month=r.month},ruleDayOfWeek:function(t,e){var r=t.match(/([+-]?[0-9])?(MO|TU|WE|TH|FR|SA|SU)/);return r?[parseInt(r[1]||0,10),t=ICAL.Recur.icalDayToNumericDay(r[2],e)]:[0,0]},next_generic:function(t,e,r,n,i){var a=t in this.by_data,s=this.rule.freq==e,o=0;if(i&&0==this[i]())return o;if(a){this.by_indices[t]++;this.by_indices[t];var u=this.by_data[t];this.by_indices[t]==u.length&&(this.by_indices[t]=0,o=1),this.last[r]=u[this.by_indices[t]]}else s&&this["increment_"+r](this.rule.interval);return a&&o&&s&&this["increment_"+n](1),o},increment_monthday:function(t){for(var e=0;er&&(this.last.day-=r,this.increment_month())}},increment_month:function(){if(this.last.day=1,this.has_by_data("BYMONTH"))this.by_indices.BYMONTH++,this.by_indices.BYMONTH==this.by_data.BYMONTH.length&&(this.by_indices.BYMONTH=0,this.increment_year(1)),this.last.month=this.by_data.BYMONTH[this.by_indices.BYMONTH];else{"MONTHLY"==this.rule.freq?this.last.month+=this.rule.interval:this.last.month++,this.last.month--;var t=ICAL.helpers.trunc(this.last.month/12);this.last.month%=12,this.last.month++,0!=t&&this.increment_year(t)}},increment_year:function(t){this.last.year+=t},increment_generic:function(t,e,r,n){this.last[e]+=t;var i=ICAL.helpers.trunc(this.last[e]/r);this.last[e]%=r,0!=i&&this["increment_"+n](i)},has_by_data:function(t){return t in this.rule.parts},expand_year_days:function(t){var e=new ICAL.Time;this.days=[];var r={},n=["BYDAY","BYWEEKNO","BYMONTHDAY","BYMONTH","BYYEARDAY"];for(var i in n)if(n.hasOwnProperty(i)){var a=n[i];a in this.rule.parts&&(r[a]=this.rule.parts[a])}if("BYMONTH"in r&&"BYWEEKNO"in r){var s=1,o={};e.year=t,e.isDate=!0;for(var u=0;ue[0]?1:e[0]>t[0]?-1:0}return t.prototype={THISANDFUTURE:"THISANDFUTURE",exceptions:null,strictExceptions:!1,relateException:function(t){if(this.isRecurrenceException())throw new Error("cannot relate exception to exceptions");if(t instanceof ICAL.Component&&(t=new ICAL.Event(t)),this.strictExceptions&&t.uid!==this.uid)throw new Error("attempted to relate unrelated exception");var e=t.recurrenceId.toString();if((this.exceptions[e]=t).modifiesFuture()){var r=[t.recurrenceId.toUnixTime(),e],n=ICAL.helpers.binsearchInsert(this.rangeExceptions,r,i);this.rangeExceptions.splice(n,0,r)}},modifiesFuture:function(){return!!this.component.hasProperty("recurrence-id")&&this.component.getFirstProperty("recurrence-id").getParameter("range")===this.THISANDFUTURE},findRangeException:function(t){if(!this.rangeExceptions.length)return null;var e=t.toUnixTime(),r=ICAL.helpers.binsearchInsert(this.rangeExceptions,[e],i);if(--r<0)return null;var n=this.rangeExceptions[r];return e