add-to-calendar-button
Advanced tools
Comparing version 1.6.4 to 1.7.0
const npmDelimiter = /\/\/ START INIT[\s\S]*?\/\/ END INIT/gm | ||
function process(content, exportPhrase) { | ||
return content.replace(npmDelimiter, `${exportPhrase} { atcb_init };`); | ||
content = content.replace('atcb_addToCalendar', 'addToCalendar'); | ||
return content.replace(npmDelimiter, `${exportPhrase} { addToCalendar, atcb_init };`); | ||
} | ||
@@ -6,0 +7,0 @@ module.exports = function(grunt) { |
declare module 'add-to-calendar-button' { | ||
export type ISO8601Date = string; | ||
export type ISO8601Time = string; | ||
export function atcb_init(): any; | ||
export function addToCalendar ( | ||
config: { | ||
name?: string; | ||
description?: string; | ||
startDate: ISO8601Date | "today"; | ||
endDate: ISO8601Date; | ||
startTime?: ISO8601Time; | ||
endTime?: ISO8601Time; | ||
location?: string; | ||
options: ("Apple" | "Google" | "iCal" | "Microsoft365" | "MicrosoftTeams" | "Outlook.com" | "Yahoo")[], | ||
timeZone?: string; | ||
timeZoneOffset?: string; | ||
iCalFileName?: string; | ||
/** | ||
* By default, the dropdown list will be opened on hover and closed as | ||
* soon as you mouse over the background overlay. If you want it to | ||
* instead close only when thebackground overlay is clicked, set this | ||
* to `"click"`. | ||
*/ | ||
trigger?: "click"; | ||
}, | ||
placeBelow?: HTMLElement | ||
): void; | ||
} |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
const atcbVersion = '1.6.4'; | ||
const atcbVersion = '1.7.0'; | ||
/* Creator: Jens Kuerschner (https://jenskuerschner.de) | ||
@@ -40,3 +40,3 @@ * Project: https://github.com/jekuer/add-to-calendar-button | ||
atcbConfig = JSON.parse(schema.innerHTML.replace(/(\r\n|\n|\r)/gm, "")); // we also remove any real code line breaks from the JSON before parsing it. Use <br> or \n explicitely in the description to create a line break | ||
atcbConfig = atcb_clean_schema_json(atcbConfig); | ||
atcbConfig = atcb_parse_schema_json(atcbConfig); | ||
// set flag to not delete HTML content later | ||
@@ -54,2 +54,4 @@ atcbConfig['deleteJSON'] = false; | ||
if (atcb_check_required(atcbConfig)) { | ||
// standardize line breaks and transform urls in the description | ||
atcbConfig = atcb_decorate_description(atcbConfig); | ||
// calculate the real date values in case that there are some special rules included (e.g. adding days dynamically) | ||
@@ -70,23 +72,28 @@ atcbConfig['startDate'] = atcb_date_calculation(atcbConfig['startDate']); | ||
// CLEAN/NORMALIZE JSON FROM SCHEMA.ORG MARKUP | ||
function atcb_clean_schema_json(atcbConfig) { | ||
Object.keys(atcbConfig['event']).forEach(key => { | ||
// move entries one level up, but skip schema types | ||
if (key.charAt(0) !== '@') { | ||
atcbConfig[key] = atcbConfig['event'][key]; | ||
} | ||
}); | ||
// clean schema date+time format | ||
const endpoints = ['start', 'end']; | ||
endpoints.forEach(function(point) { | ||
if (atcbConfig[point + 'Date'] != null) { | ||
let tmpSplitStartDate = atcbConfig[point + 'Date'].split('T'); | ||
if (tmpSplitStartDate[1] != null) { | ||
atcbConfig[point + 'Date'] = tmpSplitStartDate[0]; | ||
atcbConfig[point + 'Time'] = tmpSplitStartDate[1]; | ||
// NORMALIZE AND PARSE JSON FROM SCHEMA.ORG MARKUP | ||
function atcb_parse_schema_json(atcbConfig) { | ||
try { | ||
Object.keys(atcbConfig['event']).forEach(key => { | ||
// move entries one level up, but skip schema types | ||
if (key.charAt(0) !== '@') { | ||
atcbConfig[key] = atcbConfig['event'][key]; | ||
} | ||
}); | ||
// parse schema date+time format | ||
const endpoints = ['start', 'end']; | ||
endpoints.forEach(function(point) { | ||
if (atcbConfig[point + 'Date'] != null) { | ||
let tmpSplitStartDate = atcbConfig[point + 'Date'].split('T'); | ||
if (tmpSplitStartDate[1] != null) { | ||
atcbConfig[point + 'Date'] = tmpSplitStartDate[0]; | ||
atcbConfig[point + 'Time'] = tmpSplitStartDate[1]; | ||
} | ||
} | ||
} | ||
}); | ||
// drop the event block and return | ||
delete atcbConfig.event; | ||
}); | ||
// drop the event block and return | ||
delete atcbConfig.event; | ||
} | ||
catch(err) { | ||
console.error("add-to-calendar button problem: it seems like you use the schema.org style, but did not define it properly"); | ||
} | ||
return atcbConfig; | ||
@@ -110,8 +117,20 @@ } | ||
} | ||
}); | ||
}); | ||
return atcbConfig; | ||
} | ||
function atcb_decorate_description(atcbConfig) { | ||
// if no description or already decorated, return early | ||
if (!atcbConfig.description || atcbConfig.description_iCal) return atcbConfig; | ||
// make a copy of the given argument rather than mutating in place | ||
const data = Object.assign({}, atcbConfig); | ||
// standardize any line breaks in the description and transform URLs (but keep a clean copy without the URL magic for iCal) | ||
data.description = data.description.replace(/<br\s*\/?>/gmi, '\n'); | ||
data.description_iCal = data.description.replace('[url]','').replace('[/url]',''); | ||
data.description = data.description.replace(/\[url\](.*?)\[\/url\]/g, "<a href='$1' target='_blank' rel='noopener'>$1</a>"); | ||
return data | ||
} | ||
// CHECK FOR REQUIRED FIELDS | ||
@@ -121,3 +140,3 @@ function atcb_check_required(data) { | ||
if (data['options'] == null || data['options'].length < 1) { | ||
console.log("add-to-calendar button generation failed: no options set"); | ||
console.error("add-to-calendar button generation failed: no options set"); | ||
return false; | ||
@@ -129,3 +148,3 @@ } | ||
if (data[field] == null || data[field] == "") { | ||
console.log("add-to-calendar button generation failed: required setting missing [" + field + "]"); | ||
console.error("add-to-calendar button generation failed: required setting missing [" + field + "]"); | ||
return false; | ||
@@ -168,3 +187,3 @@ } | ||
if (!options.includes(cleanOption[0])) { | ||
console.log("add-to-calendar button generation failed: invalid option [" + cleanOption[0] + "]"); | ||
console.error("add-to-calendar button generation failed: invalid option [" + cleanOption[0] + "]"); | ||
return false; | ||
@@ -182,3 +201,3 @@ } | ||
if (dateParts.length < 3 || dateParts.length > 3) { | ||
console.log("add-to-calendar button generation failed: date misspelled [" + date + ": " + data[date] + "]"); | ||
console.error("add-to-calendar button generation failed: date misspelled [" + date + ": " + data[date] + "]"); | ||
return false; | ||
@@ -250,3 +269,2 @@ } | ||
buttonTrigger.classList.add('atcb_button'); | ||
buttonTrigger.dataset.atcbtn = buttonId; | ||
buttonTriggerWrapper.appendChild(buttonTrigger); | ||
@@ -257,25 +275,29 @@ buttonTrigger.innerHTML = '<span class="atcb_icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><path d="M81.61 4.73c0-2.61 2.58-4.73 5.77-4.73s5.77 2.12 5.77 4.73v20.72c0 2.61-2.58 4.73-5.77 4.73s-5.77-2.12-5.77-4.73V4.73h0zm-3.65 76.03c1.83 0 3.32 1.49 3.32 3.32s-1.49 3.32-3.32 3.32l-12.95-.04-.04 12.93c0 1.83-1.49 3.32-3.32 3.32s-3.32-1.49-3.32-3.32l.04-12.94-12.93-.05c-1.83 0-3.32-1.49-3.32-3.32s1.49-3.32 3.32-3.32l12.94.04.04-12.93c0-1.83 1.49-3.32 3.32-3.32s3.32 1.49 3.32 3.32l-.04 12.95 12.94.04h0zM29.61 4.73c0-2.61 2.58-4.73 5.77-4.73s5.77 2.12 5.77 4.73v20.72c0 2.61-2.58 4.73-5.77 4.73s-5.77-2.12-5.77-4.73V4.73h0zM6.4 45.32h110.08V21.47c0-.8-.33-1.53-.86-2.07-.53-.53-1.26-.86-2.07-.86H103c-1.77 0-3.2-1.43-3.2-3.2s1.43-3.2 3.2-3.2h10.55c2.57 0 4.9 1.05 6.59 2.74s2.74 4.02 2.74 6.59v27.06 65.03c0 2.57-1.05 4.9-2.74 6.59s-4.02 2.74-6.59 2.74H9.33c-2.57 0-4.9-1.05-6.59-2.74-1.69-1.7-2.74-4.03-2.74-6.6V48.53 21.47c0-2.57 1.05-4.9 2.74-6.59s4.02-2.74 6.59-2.74H20.6c1.77 0 3.2 1.43 3.2 3.2s-1.43 3.2-3.2 3.2H9.33c-.8 0-1.53.33-2.07.86-.53.53-.86 1.26-.86 2.07v23.85h0zm110.08 6.41H6.4v61.82c0 .8.33 1.53.86 2.07.53.53 1.26.86 2.07.86h104.22c.8 0 1.53-.33 2.07-.86.53-.53.86-1.26.86-2.07V51.73h0zM50.43 18.54c-1.77 0-3.2-1.43-3.2-3.2s1.43-3.2 3.2-3.2h21.49c1.77 0 3.2 1.43 3.2 3.2s-1.43 3.2-3.2 3.2H50.43h0z"/></svg></span>'; | ||
if (data['trigger'] == 'click') { | ||
buttonTrigger.addEventListener('mousedown', atcb_toggle); | ||
buttonTrigger.addEventListener('mousedown', () => atcb_toggle(data, buttonTrigger, true, false)); | ||
} else { | ||
buttonTrigger.addEventListener('touchstart', atcb_toggle, {passive: true}); | ||
buttonTrigger.addEventListener('mouseenter', atcb_open); | ||
buttonTrigger.addEventListener('touchstart', () => atcb_toggle(data, buttonTrigger, true, false), {passive: true}); | ||
buttonTrigger.addEventListener('mouseenter', () => addToCalendar(data, buttonTrigger, true, false)); | ||
} | ||
buttonTrigger.addEventListener('focus', atcb_open); | ||
buttonTrigger.addEventListener('keydown', function(event) { // trigger click on enter as well | ||
if (event.key == 'Enter') { | ||
atcb_toggle.call(event.target); | ||
atcb_toggle(data, buttonTrigger, true); | ||
} | ||
}); | ||
// standardize any line breaks in the description and transform URLs (but keep a clean copy without the URL magic for iCal) | ||
if (data['description'] != null) { | ||
data['description'] = data['description'].replace(/<br\s*\/?>/gmi, '\n'); | ||
data['description_iCal'] = data['description'].replace('[url]','').replace('[/url]',''); | ||
data['description'] = data['description'].replace(/\[url\](.*?)\[\/url\]/g, "<a href='$1' target='_blank' rel='noopener'>$1</a>"); | ||
// update the placeholder class to prevent multiple initializations | ||
button.classList.remove('atcb'); | ||
button.classList.add('atcb_initialized'); | ||
// show the placeholder div | ||
if (data['inline']) { | ||
button.style.display = 'inline-block'; | ||
} else { | ||
button.style.display = 'block'; | ||
} | ||
// generate the options list | ||
let optionsList = document.createElement('div'); | ||
optionsList.id = 'atcb_list_' + buttonId; | ||
// console log | ||
console.log("add-to-calendar button #" + (buttonId + 1) + " created"); | ||
} | ||
function atcb_generate_dropdown_list(data) { | ||
const optionsList = document.createElement('div'); | ||
optionsList.classList.add('atcb_list'); | ||
optionsList.style.display = 'none'; | ||
buttonTriggerWrapper.appendChild(optionsList); | ||
// generate the list items | ||
@@ -296,3 +318,3 @@ data['options'].forEach(function(option) { | ||
atcb_generate_ical(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -307,3 +329,3 @@ break; | ||
atcb_generate_google(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -318,3 +340,3 @@ break; | ||
atcb_generate_ical(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -329,3 +351,3 @@ break; | ||
atcb_generate_teams(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -340,3 +362,3 @@ break; | ||
atcb_generate_microsoft(data, '365'); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -351,3 +373,3 @@ break; | ||
atcb_generate_microsoft(data, 'outlook'); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -362,3 +384,3 @@ break; | ||
atcb_generate_yahoo(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -373,24 +395,19 @@ break; | ||
}); | ||
// create the background overlay, which also acts as trigger to close any dropdowns | ||
let bgOverlay = document.createElement('div'); | ||
bgOverlay.id = 'atcb_bgoverlay_' + buttonId; | ||
return optionsList; | ||
} | ||
// create the background overlay, which also acts as trigger to close any dropdowns | ||
function atcb_generate_bg_overlay(data) { | ||
const bgOverlay = document.createElement('div'); | ||
bgOverlay.classList.add('atcb_bgoverlay'); | ||
bgOverlay.style.display = 'none'; | ||
bgOverlay.tabIndex = 0; | ||
button.appendChild(bgOverlay); | ||
bgOverlay.addEventListener('click', atcb_close_all); | ||
bgOverlay.addEventListener('touchstart', atcb_close_all, {passive: true}); | ||
bgOverlay.addEventListener('mousemove', atcb_close_all); | ||
bgOverlay.addEventListener('focus', atcb_close_all); | ||
// update the placeholder class to prevent multiple initializations | ||
button.classList.remove('atcb'); | ||
button.classList.add('atcb_initialized'); | ||
// show the placeholder div | ||
if (data['inline']) { | ||
button.style.display = 'inline-block'; | ||
} else { | ||
button.style.display = 'block'; | ||
bgOverlay.addEventListener('click', () => atcb_close(true)); | ||
bgOverlay.addEventListener('touchstart', () => atcb_close(true), {passive: true}); | ||
bgOverlay.addEventListener('focus', () => atcb_close(false)); | ||
if (data['trigger'] !== 'click') { | ||
bgOverlay.addEventListener('mousemove', () => atcb_close(true)); | ||
} else { | ||
bgOverlay.classList.add('atcb_click'); | ||
} | ||
// console log | ||
console.log("add-to-calendar button #" + (buttonId + 1) + " created"); | ||
return bgOverlay; | ||
} | ||
@@ -401,34 +418,74 @@ | ||
// FUNCTIONS TO CONTROL THE INTERACTION | ||
function atcb_toggle() { | ||
function atcb_toggle(data, button, buttonGenerated, keyboardTrigger = true) { | ||
// check for state and adjust accordingly | ||
if (this.classList.contains('active')) { | ||
atcb_close.call(this); | ||
if (button.classList.contains('atcb_active')) { | ||
atcb_close(); | ||
} else { | ||
atcb_open.call(this); | ||
atcb_addToCalendar(data, button, buttonGenerated, keyboardTrigger); | ||
} | ||
} | ||
function atcb_open() { | ||
// add open class and show the list (+ the background overlay) | ||
this.classList.add('active'); | ||
let list = document.getElementById('atcb_list_' + this.dataset.atcbtn); | ||
list.style.display = 'block'; | ||
document.getElementById('atcb_bgoverlay_' + this.dataset.atcbtn).style.display = 'block'; | ||
} | ||
// show the dropdown list + background overlay | ||
function atcb_addToCalendar(data, button, buttonGenerated = false, keyboardTrigger = true) { | ||
// abort early if an add-to-calendar dropdown already opened | ||
if (document.querySelector('.atcb_list')) return | ||
function atcb_close() { | ||
// remove the active class, hide the list and the background overlay | ||
this.classList.remove('active'); | ||
let list = document.getElementById('atcb_list_' + this.dataset.atcbtn); | ||
list.style.display = 'none'; | ||
document.getElementById('atcb_bgoverlay_' + this.dataset.atcbtn).style.display = 'none'; | ||
// validate & decorate data | ||
if (!atcb_check_required(data) || !atcb_validate(data)) { | ||
throw new Error("Invalid data; see logs") | ||
} | ||
data = atcb_decorate_description(data); | ||
// generate list | ||
const list = atcb_generate_dropdown_list(data, buttonGenerated); | ||
// set list styles, set possible button to atcb_active and go for modal mode if no button is set | ||
if (button) { | ||
button.classList.add('atcb_active'); | ||
const rect = button.getBoundingClientRect(); | ||
list.style.width = rect.width + 'px'; | ||
list.style.top = rect.bottom + window.scrollY + 'px'; | ||
list.style.right = document.body.offsetWidth - rect.right + 'px'; | ||
} else { | ||
list.classList.add('atcb_modal') | ||
} | ||
if (buttonGenerated) { | ||
list.classList.add('atcb_generated_button'); | ||
} | ||
// add list to DOM | ||
document.body.appendChild(list); | ||
// add background overlay right after, so tabbing past last option closes list | ||
document.body.appendChild(atcb_generate_bg_overlay(data)); | ||
// give keyboard focus to first item in list, if not blocked, because there is definitely no keyboard trigger | ||
if (keyboardTrigger) { | ||
list.firstChild.focus(); | ||
} | ||
} | ||
function atcb_close_all() { | ||
// get all buttons | ||
let atcButtons = document.querySelectorAll('.atcb_button'); | ||
// and close them one by one | ||
for (let i = 0; i < atcButtons.length; i++) { | ||
atcb_close.call(atcButtons[i]); | ||
function atcb_close(blockFocus = false) { | ||
// 1. Focus triggering button (if not existing, try a custom element) | ||
if (!blockFocus) { | ||
let newFocusEl = document.querySelector('.atcb_active'); | ||
if (newFocusEl) { | ||
newFocusEl.focus(); | ||
} else { | ||
newFocusEl = document.querySelector('.atcb_customTrigger'); | ||
if (newFocusEl) { | ||
newFocusEl.focus(); | ||
} | ||
} | ||
} | ||
// 2. Inactivate all buttons | ||
Array.from(document.querySelectorAll('.atcb_active')).forEach(button => { | ||
button.classList.remove('atcb_active'); | ||
}) | ||
// 3. Remove dropdowns & bg overlays (should only be one of each) | ||
Array.from(document.querySelectorAll('.atcb_list')).concat( | ||
Array.from(document.querySelectorAll('.atcb_bgoverlay')) | ||
).forEach(el => el.remove()); | ||
} | ||
@@ -546,3 +603,3 @@ | ||
let now = new Date(); | ||
now = now.toISOString().replace(/\-/g, '').replace(/\:/g, '').replace(/\..../g, ''); | ||
now = now.toISOString().replace(/\..../g, '').replace(/[^a-z0-9]/gi,''); | ||
let formattedDate = atcb_generate_time(data, 'clean', 'ical'); | ||
@@ -554,24 +611,24 @@ let timeslot = ''; | ||
let ics_lines = [ | ||
"BEGIN:VCALENDAR", | ||
"VERSION:2.0", | ||
"CALSCALE:GREGORIAN", | ||
"BEGIN:VEVENT", | ||
"DTSTAMP:" + formattedDate['start'], | ||
"DTSTART" + timeslot + ":" + formattedDate['start'], | ||
"DTEND" + timeslot + ":" + formattedDate['end'], | ||
"SUMMARY:" + data['name'] | ||
]; | ||
if (data['description_iCal'] != null && data['description_iCal'] != '') { | ||
ics_lines.push("DESCRIPTION:" + data['description_iCal'].replace(/\n/g, '\\n')); | ||
} | ||
if (data['location'] != null && data['location'] != '') { | ||
ics_lines.push("LOCATION:" + data['location']); | ||
} | ||
ics_lines.push( | ||
"STATUS:CONFIRMED", | ||
"LAST-MODIFIED:" + now, | ||
"SEQUENCE:0", | ||
"END:VEVENT", | ||
"END:VCALENDAR" | ||
); | ||
"BEGIN:VCALENDAR", | ||
"VERSION:2.0", | ||
"CALSCALE:GREGORIAN", | ||
"BEGIN:VEVENT", | ||
"DTSTAMP:" + formattedDate['start'], | ||
"DTSTART" + timeslot + ":" + formattedDate['start'], | ||
"DTEND" + timeslot + ":" + formattedDate['end'], | ||
"SUMMARY:" + data['name'] | ||
]; | ||
if (data['description_iCal'] != null && data['description_iCal'] != '') { | ||
ics_lines.push("DESCRIPTION:" + data['description_iCal'].replace(/\n/g, '\\n')); | ||
} | ||
if (data['location'] != null && data['location'] != '') { | ||
ics_lines.push("LOCATION:" + data['location']); | ||
} | ||
ics_lines.push( | ||
"STATUS:CONFIRMED", | ||
"LAST-MODIFIED:" + now, | ||
"SEQUENCE:0", | ||
"END:VEVENT", | ||
"END:VCALENDAR" | ||
); | ||
let dlurl = 'data:text/calendar;charset=utf-8,'+encodeURIComponent(ics_lines.join('\r\n')); | ||
@@ -665,2 +722,11 @@ try { | ||
module.exports = { atcb_init }; | ||
// Global listener to ESC key to close dropdown | ||
document.addEventListener('keydown', evt => { | ||
if (evt.key === 'Escape') { | ||
atcb_close(); | ||
} | ||
}); | ||
module.exports = { addToCalendar, atcb_init }; |
@@ -6,3 +6,3 @@ /** | ||
*/ | ||
const atcbVersion = '1.6.4'; | ||
const atcbVersion = '1.7.0'; | ||
/* Creator: Jens Kuerschner (https://jenskuerschner.de) | ||
@@ -40,3 +40,3 @@ * Project: https://github.com/jekuer/add-to-calendar-button | ||
atcbConfig = JSON.parse(schema.innerHTML.replace(/(\r\n|\n|\r)/gm, "")); // we also remove any real code line breaks from the JSON before parsing it. Use <br> or \n explicitely in the description to create a line break | ||
atcbConfig = atcb_clean_schema_json(atcbConfig); | ||
atcbConfig = atcb_parse_schema_json(atcbConfig); | ||
// set flag to not delete HTML content later | ||
@@ -54,2 +54,4 @@ atcbConfig['deleteJSON'] = false; | ||
if (atcb_check_required(atcbConfig)) { | ||
// standardize line breaks and transform urls in the description | ||
atcbConfig = atcb_decorate_description(atcbConfig); | ||
// calculate the real date values in case that there are some special rules included (e.g. adding days dynamically) | ||
@@ -70,23 +72,28 @@ atcbConfig['startDate'] = atcb_date_calculation(atcbConfig['startDate']); | ||
// CLEAN/NORMALIZE JSON FROM SCHEMA.ORG MARKUP | ||
function atcb_clean_schema_json(atcbConfig) { | ||
Object.keys(atcbConfig['event']).forEach(key => { | ||
// move entries one level up, but skip schema types | ||
if (key.charAt(0) !== '@') { | ||
atcbConfig[key] = atcbConfig['event'][key]; | ||
} | ||
}); | ||
// clean schema date+time format | ||
const endpoints = ['start', 'end']; | ||
endpoints.forEach(function(point) { | ||
if (atcbConfig[point + 'Date'] != null) { | ||
let tmpSplitStartDate = atcbConfig[point + 'Date'].split('T'); | ||
if (tmpSplitStartDate[1] != null) { | ||
atcbConfig[point + 'Date'] = tmpSplitStartDate[0]; | ||
atcbConfig[point + 'Time'] = tmpSplitStartDate[1]; | ||
// NORMALIZE AND PARSE JSON FROM SCHEMA.ORG MARKUP | ||
function atcb_parse_schema_json(atcbConfig) { | ||
try { | ||
Object.keys(atcbConfig['event']).forEach(key => { | ||
// move entries one level up, but skip schema types | ||
if (key.charAt(0) !== '@') { | ||
atcbConfig[key] = atcbConfig['event'][key]; | ||
} | ||
}); | ||
// parse schema date+time format | ||
const endpoints = ['start', 'end']; | ||
endpoints.forEach(function(point) { | ||
if (atcbConfig[point + 'Date'] != null) { | ||
let tmpSplitStartDate = atcbConfig[point + 'Date'].split('T'); | ||
if (tmpSplitStartDate[1] != null) { | ||
atcbConfig[point + 'Date'] = tmpSplitStartDate[0]; | ||
atcbConfig[point + 'Time'] = tmpSplitStartDate[1]; | ||
} | ||
} | ||
} | ||
}); | ||
// drop the event block and return | ||
delete atcbConfig.event; | ||
}); | ||
// drop the event block and return | ||
delete atcbConfig.event; | ||
} | ||
catch(err) { | ||
console.error("add-to-calendar button problem: it seems like you use the schema.org style, but did not define it properly"); | ||
} | ||
return atcbConfig; | ||
@@ -110,8 +117,20 @@ } | ||
} | ||
}); | ||
}); | ||
return atcbConfig; | ||
} | ||
function atcb_decorate_description(atcbConfig) { | ||
// if no description or already decorated, return early | ||
if (!atcbConfig.description || atcbConfig.description_iCal) return atcbConfig; | ||
// make a copy of the given argument rather than mutating in place | ||
const data = Object.assign({}, atcbConfig); | ||
// standardize any line breaks in the description and transform URLs (but keep a clean copy without the URL magic for iCal) | ||
data.description = data.description.replace(/<br\s*\/?>/gmi, '\n'); | ||
data.description_iCal = data.description.replace('[url]','').replace('[/url]',''); | ||
data.description = data.description.replace(/\[url\](.*?)\[\/url\]/g, "<a href='$1' target='_blank' rel='noopener'>$1</a>"); | ||
return data | ||
} | ||
// CHECK FOR REQUIRED FIELDS | ||
@@ -121,3 +140,3 @@ function atcb_check_required(data) { | ||
if (data['options'] == null || data['options'].length < 1) { | ||
console.log("add-to-calendar button generation failed: no options set"); | ||
console.error("add-to-calendar button generation failed: no options set"); | ||
return false; | ||
@@ -129,3 +148,3 @@ } | ||
if (data[field] == null || data[field] == "") { | ||
console.log("add-to-calendar button generation failed: required setting missing [" + field + "]"); | ||
console.error("add-to-calendar button generation failed: required setting missing [" + field + "]"); | ||
return false; | ||
@@ -168,3 +187,3 @@ } | ||
if (!options.includes(cleanOption[0])) { | ||
console.log("add-to-calendar button generation failed: invalid option [" + cleanOption[0] + "]"); | ||
console.error("add-to-calendar button generation failed: invalid option [" + cleanOption[0] + "]"); | ||
return false; | ||
@@ -182,3 +201,3 @@ } | ||
if (dateParts.length < 3 || dateParts.length > 3) { | ||
console.log("add-to-calendar button generation failed: date misspelled [" + date + ": " + data[date] + "]"); | ||
console.error("add-to-calendar button generation failed: date misspelled [" + date + ": " + data[date] + "]"); | ||
return false; | ||
@@ -250,3 +269,2 @@ } | ||
buttonTrigger.classList.add('atcb_button'); | ||
buttonTrigger.dataset.atcbtn = buttonId; | ||
buttonTriggerWrapper.appendChild(buttonTrigger); | ||
@@ -257,25 +275,29 @@ buttonTrigger.innerHTML = '<span class="atcb_icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><path d="M81.61 4.73c0-2.61 2.58-4.73 5.77-4.73s5.77 2.12 5.77 4.73v20.72c0 2.61-2.58 4.73-5.77 4.73s-5.77-2.12-5.77-4.73V4.73h0zm-3.65 76.03c1.83 0 3.32 1.49 3.32 3.32s-1.49 3.32-3.32 3.32l-12.95-.04-.04 12.93c0 1.83-1.49 3.32-3.32 3.32s-3.32-1.49-3.32-3.32l.04-12.94-12.93-.05c-1.83 0-3.32-1.49-3.32-3.32s1.49-3.32 3.32-3.32l12.94.04.04-12.93c0-1.83 1.49-3.32 3.32-3.32s3.32 1.49 3.32 3.32l-.04 12.95 12.94.04h0zM29.61 4.73c0-2.61 2.58-4.73 5.77-4.73s5.77 2.12 5.77 4.73v20.72c0 2.61-2.58 4.73-5.77 4.73s-5.77-2.12-5.77-4.73V4.73h0zM6.4 45.32h110.08V21.47c0-.8-.33-1.53-.86-2.07-.53-.53-1.26-.86-2.07-.86H103c-1.77 0-3.2-1.43-3.2-3.2s1.43-3.2 3.2-3.2h10.55c2.57 0 4.9 1.05 6.59 2.74s2.74 4.02 2.74 6.59v27.06 65.03c0 2.57-1.05 4.9-2.74 6.59s-4.02 2.74-6.59 2.74H9.33c-2.57 0-4.9-1.05-6.59-2.74-1.69-1.7-2.74-4.03-2.74-6.6V48.53 21.47c0-2.57 1.05-4.9 2.74-6.59s4.02-2.74 6.59-2.74H20.6c1.77 0 3.2 1.43 3.2 3.2s-1.43 3.2-3.2 3.2H9.33c-.8 0-1.53.33-2.07.86-.53.53-.86 1.26-.86 2.07v23.85h0zm110.08 6.41H6.4v61.82c0 .8.33 1.53.86 2.07.53.53 1.26.86 2.07.86h104.22c.8 0 1.53-.33 2.07-.86.53-.53.86-1.26.86-2.07V51.73h0zM50.43 18.54c-1.77 0-3.2-1.43-3.2-3.2s1.43-3.2 3.2-3.2h21.49c1.77 0 3.2 1.43 3.2 3.2s-1.43 3.2-3.2 3.2H50.43h0z"/></svg></span>'; | ||
if (data['trigger'] == 'click') { | ||
buttonTrigger.addEventListener('mousedown', atcb_toggle); | ||
buttonTrigger.addEventListener('mousedown', () => atcb_toggle(data, buttonTrigger, true, false)); | ||
} else { | ||
buttonTrigger.addEventListener('touchstart', atcb_toggle, {passive: true}); | ||
buttonTrigger.addEventListener('mouseenter', atcb_open); | ||
buttonTrigger.addEventListener('touchstart', () => atcb_toggle(data, buttonTrigger, true, false), {passive: true}); | ||
buttonTrigger.addEventListener('mouseenter', () => addToCalendar(data, buttonTrigger, true, false)); | ||
} | ||
buttonTrigger.addEventListener('focus', atcb_open); | ||
buttonTrigger.addEventListener('keydown', function(event) { // trigger click on enter as well | ||
if (event.key == 'Enter') { | ||
atcb_toggle.call(event.target); | ||
atcb_toggle(data, buttonTrigger, true); | ||
} | ||
}); | ||
// standardize any line breaks in the description and transform URLs (but keep a clean copy without the URL magic for iCal) | ||
if (data['description'] != null) { | ||
data['description'] = data['description'].replace(/<br\s*\/?>/gmi, '\n'); | ||
data['description_iCal'] = data['description'].replace('[url]','').replace('[/url]',''); | ||
data['description'] = data['description'].replace(/\[url\](.*?)\[\/url\]/g, "<a href='$1' target='_blank' rel='noopener'>$1</a>"); | ||
// update the placeholder class to prevent multiple initializations | ||
button.classList.remove('atcb'); | ||
button.classList.add('atcb_initialized'); | ||
// show the placeholder div | ||
if (data['inline']) { | ||
button.style.display = 'inline-block'; | ||
} else { | ||
button.style.display = 'block'; | ||
} | ||
// generate the options list | ||
let optionsList = document.createElement('div'); | ||
optionsList.id = 'atcb_list_' + buttonId; | ||
// console log | ||
console.log("add-to-calendar button #" + (buttonId + 1) + " created"); | ||
} | ||
function atcb_generate_dropdown_list(data) { | ||
const optionsList = document.createElement('div'); | ||
optionsList.classList.add('atcb_list'); | ||
optionsList.style.display = 'none'; | ||
buttonTriggerWrapper.appendChild(optionsList); | ||
// generate the list items | ||
@@ -296,3 +318,3 @@ data['options'].forEach(function(option) { | ||
atcb_generate_ical(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -307,3 +329,3 @@ break; | ||
atcb_generate_google(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -318,3 +340,3 @@ break; | ||
atcb_generate_ical(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -329,3 +351,3 @@ break; | ||
atcb_generate_teams(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -340,3 +362,3 @@ break; | ||
atcb_generate_microsoft(data, '365'); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -351,3 +373,3 @@ break; | ||
atcb_generate_microsoft(data, 'outlook'); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -362,3 +384,3 @@ break; | ||
atcb_generate_yahoo(data); | ||
atcb_close_all(); | ||
atcb_close(); | ||
}); | ||
@@ -373,24 +395,19 @@ break; | ||
}); | ||
// create the background overlay, which also acts as trigger to close any dropdowns | ||
let bgOverlay = document.createElement('div'); | ||
bgOverlay.id = 'atcb_bgoverlay_' + buttonId; | ||
return optionsList; | ||
} | ||
// create the background overlay, which also acts as trigger to close any dropdowns | ||
function atcb_generate_bg_overlay(data) { | ||
const bgOverlay = document.createElement('div'); | ||
bgOverlay.classList.add('atcb_bgoverlay'); | ||
bgOverlay.style.display = 'none'; | ||
bgOverlay.tabIndex = 0; | ||
button.appendChild(bgOverlay); | ||
bgOverlay.addEventListener('click', atcb_close_all); | ||
bgOverlay.addEventListener('touchstart', atcb_close_all, {passive: true}); | ||
bgOverlay.addEventListener('mousemove', atcb_close_all); | ||
bgOverlay.addEventListener('focus', atcb_close_all); | ||
// update the placeholder class to prevent multiple initializations | ||
button.classList.remove('atcb'); | ||
button.classList.add('atcb_initialized'); | ||
// show the placeholder div | ||
if (data['inline']) { | ||
button.style.display = 'inline-block'; | ||
} else { | ||
button.style.display = 'block'; | ||
bgOverlay.addEventListener('click', () => atcb_close(true)); | ||
bgOverlay.addEventListener('touchstart', () => atcb_close(true), {passive: true}); | ||
bgOverlay.addEventListener('focus', () => atcb_close(false)); | ||
if (data['trigger'] !== 'click') { | ||
bgOverlay.addEventListener('mousemove', () => atcb_close(true)); | ||
} else { | ||
bgOverlay.classList.add('atcb_click'); | ||
} | ||
// console log | ||
console.log("add-to-calendar button #" + (buttonId + 1) + " created"); | ||
return bgOverlay; | ||
} | ||
@@ -401,34 +418,74 @@ | ||
// FUNCTIONS TO CONTROL THE INTERACTION | ||
function atcb_toggle() { | ||
function atcb_toggle(data, button, buttonGenerated, keyboardTrigger = true) { | ||
// check for state and adjust accordingly | ||
if (this.classList.contains('active')) { | ||
atcb_close.call(this); | ||
if (button.classList.contains('atcb_active')) { | ||
atcb_close(); | ||
} else { | ||
atcb_open.call(this); | ||
atcb_addToCalendar(data, button, buttonGenerated, keyboardTrigger); | ||
} | ||
} | ||
function atcb_open() { | ||
// add open class and show the list (+ the background overlay) | ||
this.classList.add('active'); | ||
let list = document.getElementById('atcb_list_' + this.dataset.atcbtn); | ||
list.style.display = 'block'; | ||
document.getElementById('atcb_bgoverlay_' + this.dataset.atcbtn).style.display = 'block'; | ||
} | ||
// show the dropdown list + background overlay | ||
function atcb_addToCalendar(data, button, buttonGenerated = false, keyboardTrigger = true) { | ||
// abort early if an add-to-calendar dropdown already opened | ||
if (document.querySelector('.atcb_list')) return | ||
function atcb_close() { | ||
// remove the active class, hide the list and the background overlay | ||
this.classList.remove('active'); | ||
let list = document.getElementById('atcb_list_' + this.dataset.atcbtn); | ||
list.style.display = 'none'; | ||
document.getElementById('atcb_bgoverlay_' + this.dataset.atcbtn).style.display = 'none'; | ||
// validate & decorate data | ||
if (!atcb_check_required(data) || !atcb_validate(data)) { | ||
throw new Error("Invalid data; see logs") | ||
} | ||
data = atcb_decorate_description(data); | ||
// generate list | ||
const list = atcb_generate_dropdown_list(data, buttonGenerated); | ||
// set list styles, set possible button to atcb_active and go for modal mode if no button is set | ||
if (button) { | ||
button.classList.add('atcb_active'); | ||
const rect = button.getBoundingClientRect(); | ||
list.style.width = rect.width + 'px'; | ||
list.style.top = rect.bottom + window.scrollY + 'px'; | ||
list.style.right = document.body.offsetWidth - rect.right + 'px'; | ||
} else { | ||
list.classList.add('atcb_modal') | ||
} | ||
if (buttonGenerated) { | ||
list.classList.add('atcb_generated_button'); | ||
} | ||
// add list to DOM | ||
document.body.appendChild(list); | ||
// add background overlay right after, so tabbing past last option closes list | ||
document.body.appendChild(atcb_generate_bg_overlay(data)); | ||
// give keyboard focus to first item in list, if not blocked, because there is definitely no keyboard trigger | ||
if (keyboardTrigger) { | ||
list.firstChild.focus(); | ||
} | ||
} | ||
function atcb_close_all() { | ||
// get all buttons | ||
let atcButtons = document.querySelectorAll('.atcb_button'); | ||
// and close them one by one | ||
for (let i = 0; i < atcButtons.length; i++) { | ||
atcb_close.call(atcButtons[i]); | ||
function atcb_close(blockFocus = false) { | ||
// 1. Focus triggering button (if not existing, try a custom element) | ||
if (!blockFocus) { | ||
let newFocusEl = document.querySelector('.atcb_active'); | ||
if (newFocusEl) { | ||
newFocusEl.focus(); | ||
} else { | ||
newFocusEl = document.querySelector('.atcb_customTrigger'); | ||
if (newFocusEl) { | ||
newFocusEl.focus(); | ||
} | ||
} | ||
} | ||
// 2. Inactivate all buttons | ||
Array.from(document.querySelectorAll('.atcb_active')).forEach(button => { | ||
button.classList.remove('atcb_active'); | ||
}) | ||
// 3. Remove dropdowns & bg overlays (should only be one of each) | ||
Array.from(document.querySelectorAll('.atcb_list')).concat( | ||
Array.from(document.querySelectorAll('.atcb_bgoverlay')) | ||
).forEach(el => el.remove()); | ||
} | ||
@@ -546,3 +603,3 @@ | ||
let now = new Date(); | ||
now = now.toISOString().replace(/\-/g, '').replace(/\:/g, '').replace(/\..../g, ''); | ||
now = now.toISOString().replace(/\..../g, '').replace(/[^a-z0-9]/gi,''); | ||
let formattedDate = atcb_generate_time(data, 'clean', 'ical'); | ||
@@ -554,24 +611,24 @@ let timeslot = ''; | ||
let ics_lines = [ | ||
"BEGIN:VCALENDAR", | ||
"VERSION:2.0", | ||
"CALSCALE:GREGORIAN", | ||
"BEGIN:VEVENT", | ||
"DTSTAMP:" + formattedDate['start'], | ||
"DTSTART" + timeslot + ":" + formattedDate['start'], | ||
"DTEND" + timeslot + ":" + formattedDate['end'], | ||
"SUMMARY:" + data['name'] | ||
]; | ||
if (data['description_iCal'] != null && data['description_iCal'] != '') { | ||
ics_lines.push("DESCRIPTION:" + data['description_iCal'].replace(/\n/g, '\\n')); | ||
} | ||
if (data['location'] != null && data['location'] != '') { | ||
ics_lines.push("LOCATION:" + data['location']); | ||
} | ||
ics_lines.push( | ||
"STATUS:CONFIRMED", | ||
"LAST-MODIFIED:" + now, | ||
"SEQUENCE:0", | ||
"END:VEVENT", | ||
"END:VCALENDAR" | ||
); | ||
"BEGIN:VCALENDAR", | ||
"VERSION:2.0", | ||
"CALSCALE:GREGORIAN", | ||
"BEGIN:VEVENT", | ||
"DTSTAMP:" + formattedDate['start'], | ||
"DTSTART" + timeslot + ":" + formattedDate['start'], | ||
"DTEND" + timeslot + ":" + formattedDate['end'], | ||
"SUMMARY:" + data['name'] | ||
]; | ||
if (data['description_iCal'] != null && data['description_iCal'] != '') { | ||
ics_lines.push("DESCRIPTION:" + data['description_iCal'].replace(/\n/g, '\\n')); | ||
} | ||
if (data['location'] != null && data['location'] != '') { | ||
ics_lines.push("LOCATION:" + data['location']); | ||
} | ||
ics_lines.push( | ||
"STATUS:CONFIRMED", | ||
"LAST-MODIFIED:" + now, | ||
"SEQUENCE:0", | ||
"END:VEVENT", | ||
"END:VCALENDAR" | ||
); | ||
let dlurl = 'data:text/calendar;charset=utf-8,'+encodeURIComponent(ics_lines.join('\r\n')); | ||
@@ -665,2 +722,11 @@ try { | ||
export { atcb_init }; | ||
// Global listener to ESC key to close dropdown | ||
document.addEventListener('keydown', evt => { | ||
if (evt.key === 'Escape') { | ||
atcb_close(); | ||
} | ||
}); | ||
export { addToCalendar, atcb_init }; |
{ | ||
"name": "add-to-calendar-button", | ||
"version": "1.6.4", | ||
"version": "1.7.0", | ||
"description": "A convenient JavaScript snippet, which lets you create beautiful buttons, where people can add events to their calendars.", | ||
@@ -5,0 +5,0 @@ "main": "npm_dist/cjs/index.js", |
124
README.md
@@ -1,9 +0,12 @@ | ||
# Your next Add-to-Calendar Button | ||
# 📅 Your next Add-to-Calendar Button! | ||
![Add-to-Calendar Button](https://github.com/jekuer/add-to-calendar-button/blob/main/repo_image.png?raw=true) | ||
_A convenient JavaScript snippet, which lets you create beautiful buttons, where people can add events to their calendars._ | ||
_A convenient JavaScript snippet, which lets you create beautiful buttons, where people can add events to their calendars._ | ||
![#1 Product of the Day on ProductHunt](https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=319458&theme=dark&period=daily) | ||
## Use case // Who this is for | ||
<br /><br /> | ||
## 😎 Use case // Who this is for | ||
This is for everybody, who wants to include a button at his/her website or app, which enables users to easily add a specific event to their calendars. | ||
@@ -13,4 +16,5 @@ It's main goal is to keep this process as easy as possible. Simply define your button configuration via JSON and everything else is automatically generated by the script. | ||
<br /> | ||
## Background // Why this repo exists | ||
## 🔎 Background // Why this repo exists | ||
@@ -20,17 +24,21 @@ While building a personal wedding page, I was confronted with the task to include a button, where invited people could save the event to their calendars. | ||
Unfortunately, all I found where some extremely outdated code snippets, which did not really fit all the modern systems and calendar tools. | ||
Beside that, there was only the solution by AddEvent.com - all over the place. I was looking at CodePen and all I found where thousands of pens, which basically only included the AddEvent tool. | ||
Beside that, there was only the solution by AddEvent.com - all over the place. I was looking at CodePen and all I found where thousands of pens, which basically only included the AddEvent tool. | ||
The problems with AddEvent.com: | ||
* it holds tons of features, which I did not need. I do not want to track my button. I just want it to work. | ||
* it limits the free tier to 50 event adds per month (consider the wedding case - this is way too less). | ||
* it brings some data privacy issues, since you basically send your users to their service. | ||
* the UX/UI is not ideal (imho). | ||
*Bottom line:* Pay for features, which I do not need - at additional privacy "cost" - that made me create this solution (for you). | ||
* it brings some data privacy issues, since you basically send your users to their service. GDPR alert! | ||
* the UX/UI is not ideal (imho). | ||
**Bottom line:** Paying for features, which I did not need - at additional privacy concerns - that made me create this solution (for you). | ||
## DEMO | ||
<br /> | ||
## ▶️ Demo | ||
See [jekuer.github.io/add-to-calendar-button](https://jekuer.github.io/add-to-calendar-button/) for a live demo. | ||
<br /> | ||
## Features | ||
## ✨ Features | ||
@@ -46,9 +54,10 @@ * Simple and convenient integration of multiple buttons - configure them directly within the HTML code. | ||
* Timed and all-day events. | ||
* Translatable labels and dynamic. | ||
* Well documented code, to easily understand the processes. | ||
* Translatable labels and dynamic dates. | ||
* Well documented code, to easily understand the processes and build on top of it. | ||
![Demo Screenshot](https://github.com/jekuer/add-to-calendar-button/blob/main/demo.gif?raw=true) | ||
<br /> | ||
## Setup | ||
## 🌱 Setup | ||
@@ -65,6 +74,6 @@ ### Option 1: simple | ||
1. Requires Node.js, npm, and a project, which builds on it (e.g. React or Angular). | ||
1. Requires Node, npm, and a project, which builds on it (e.g. React or Angular). | ||
2. Run **`npm install add-to-calendar-button`**. | ||
3. Import the module into your project/component. For example with Angular/React: `import { atcb_init } from 'add-to-calendar-button';`. | ||
4. Init the js after the DOM has been loaded. To determine the right moment and execute, ... | ||
3. Import the module into your project/component. For example with Angular/React: `import { addToCalendar, atcb_init } from 'add-to-calendar-button';`. | ||
4. Either use `addToCalendar` with your own buttons/forms/etc, or run `atcb_init` after the DOM has been loaded. To determine the right moment and execute, ... | ||
1. with Angular, you would use `ngAfterViewInit()` with `atcb_init();` (mind that, depending on your app, other hooks might be better); | ||
@@ -76,4 +85,5 @@ 2. with React, you might want to include an event listener like `document.addEventListener('DOMContentLoaded', atcb_init, false);` or using hooks in a functional component like `useEffect(() => atcb_init());` | ||
<br /> | ||
## Configuration | ||
## 🎛️ Configuration | ||
@@ -171,21 +181,53 @@ A button can be easily created by placing a respective placeholder, wherever you want the button to appear. | ||
### React example | ||
### `addToCalendar` example with React | ||
A simpler method uses `JSON.stringify()` where you define your config in an external file or variable and then | ||
convert it to a string to be passed into the div. | ||
If you can't or don't want to use `atcb_init`, you can use the `addToCalendar` import with your own buttons or other elements/components. If you omit the second argument, the dropdown list will display as a modal in the middle of the viewport - in this case, add the "atcb_customTrigger" class to the submitting element for better keyboard support. | ||
This may work better with React and other frontend frameworks. | ||
```js | ||
import React from 'react' | ||
import { addToCalendar } from 'add-to-calendar-button' | ||
const AddToCalendar = () => { | ||
const [name, setName] = React.useState('Some event') | ||
return ( | ||
<form onSubmit={e => { | ||
e.preventDefault() | ||
addToCalendar({ | ||
name, | ||
startDate: "2022-01-14", | ||
endDate: "2022-01-18", | ||
options: ['Apple', 'Google', 'iCal', 'Microsoft365', 'Outlook.com', 'MicrosoftTeams', 'Yahoo'], | ||
trigger: "click", | ||
iCalFileName: "Reminder-Event", | ||
}) | ||
}}> | ||
<input value={name} onChange={setName} /> | ||
<input class="atcb_customTrigger" type="submit" value="save"> | ||
</form> | ||
); | ||
} | ||
``` | ||
useEffect(() => atcb_init()); | ||
Alternatively, you could use `atcb_init` with a `useEffect` hook: | ||
const event = { /* Event data */} | ||
```js | ||
import React from 'react' | ||
import { atcb_init } from 'add-to-calendar-button' | ||
return ( | ||
<div className='atcb'> | ||
<script type="application/ld+json"> | ||
{JSON.stringify(event)} | ||
</script> | ||
</div> | ||
); | ||
const AddToCalendar = () => { | ||
React.useEffect(atcb_init, []) | ||
return ( | ||
<div className='atcb' style={{display: 'none'}}> | ||
{JSON.stringify({ | ||
name: "Some event", | ||
startDate: "2022-01-14", | ||
endDate: "2022-01-18", | ||
options: ['Apple', 'Google', 'iCal', 'Microsoft365', 'Outlook.com', 'MicrosoftTeams', 'Yahoo'], | ||
trigger: "click", | ||
iCalFileName: "Reminder-Event", | ||
})} | ||
</div> | ||
); | ||
} | ||
@@ -198,4 +240,4 @@ ``` | ||
* Dates need to be formatted as YYYY-MM-DD ([ISO-8601](https://en.wikipedia.org/wiki/ISO_8601)). | ||
* You can also use the word "today" as date. It will then dynamically use the current day at click. | ||
* Add "+5" at the end of the date to dynamically add 5 days (or any other number). "2022-01-30+12" would generate the 11th of February 2022. This can be interesting, when combined with "today". | ||
* You can also use the word "today" as date. It will then dynamically use the current day at click (not supported with schema.org style). | ||
* Add "+5" at the end of the date to dynamically add 5 days (or any other number). "2022-01-30+12" would generate the 11th of February 2022. This can be interesting, when combined with "today". | ||
* Times need to be formatted as HH:MM. | ||
@@ -217,4 +259,5 @@ * Times are optional. If not set, the button generates all-day events. | ||
<br /> | ||
## Contributing | ||
## 🙌 Contributing | ||
@@ -227,12 +270,15 @@ Anyone is welcome to contribute, but mind the [guidelines](.github/CONTRIBUTING.md): | ||
IMPORTANT NOTE: Run `npm install` and `npm run build` to create the minified js and css file, its sourcemap files as well as the npm_dist/ folder and content! | ||
**IMPORTANT NOTE:** Run `npm install` and `npm run build` to create the minified js and css file, its sourcemap files as well as the npm_dist/ folder and content! | ||
<br /> | ||
## License | ||
## 📃 License | ||
The code is available under the [MIT license (with “Commons Clause” License Condition v1.0)](LICENSE.txt). | ||
<br /> | ||
## Changelog (without bug fixes) | ||
## ⚡ Changelog (without minor changes and fixes) | ||
* v1.7 : new code structure and options + tons of optimizations | ||
* v1.6 : supporting Microsoft Teams | ||
@@ -246,6 +292,8 @@ * v1.5 : update to date format and better accesibility | ||
<br /> | ||
## Kudos go to | ||
* [github.com/dudewheresmycode](https://github.com/dudewheresmycodee) | ||
## 💜 Kudos go to | ||
* [uxwing.com](https://uxwing.com) | ||
* all contributors! | ||
* [Brian R (dudewheresmycode)](https://github.com/dudewheresmycode) | ||
* [Chad Ostrowski (chadoh)](https://github.com/chadoh) | ||
* ... and all other contributors! |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
107717
1625
290