Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bitovi/calendar-events-component

Package Overview
Dependencies
Maintainers
15
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bitovi/calendar-events-component - npm Package Compare versions

Comparing version 0.0.11 to 0.1.0

dist/global/calendar-events.js

14

build.js

@@ -10,10 +10,12 @@ var stealTools = require("steal-tools");

"+amd": {},
"+global-js": {}
"+global-js": {
dest: __dirname + "/dist/global/calendar-events.js"
}
}
}).catch(function(e){
setTimeout(function(){
}).catch(function (e) {
setTimeout(function () {
throw e;
},1);
}, 1);
});

@@ -0,8 +1,609 @@

// jshint ignore: start
var QUnit = require('steal-qunit');
var CalendarEvents = require('./calendar-events');
QUnit.module('calendar-events');
const fixture = document.getElementById('qunit-fixture')
const select = fixture.querySelector.bind(fixture)
const selectAll = fixture.querySelectorAll.bind(fixture)
QUnit.test('Initialized the plugin', function(){
const globalFetch = (typeof fetch !== "undefined") && fetch
QUnit.module('calendar-events', {
beforeEach: function () {
globalThis.fetch = () => Promise.resolve({ json: () => googleCalendarAPIResponse })
},
afterEach: function (assert) {
globalThis.fetch = globalFetch
}
})
const dateShapeRx = {
"Apr 18, 2023, 6:30 PM": /^[^\s\d]+ \d{1,2}, \d{4}(?:,| at) \d{1,2}:\d{2} ..$/, // safari says "...2023 at 6..."
"Tue, Apr 04": /^[a-z]{3}, [a-z]{3} \d\d$/i,
}
QUnit.test('Initialized the plugin', function () {
QUnit.equal(typeof CalendarEvents, 'function');
});
})
QUnit.test('Component mounts, requests, renders, and uses default event template', async assert => {
let lastFetchedUrl = ''
globalThis.fetch = (url) => {
lastFetchedUrl = url
return Promise.resolve({ json: () => googleCalendarAPIResponse })
}
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
></calendar-events>
`
const el = select("calendar-events")
assert.ok(el, "el inserted")
assert.ok(el.promise, "el promises")
assert.equal(
lastFetchedUrl,
"https://www.googleapis.com/calendar/v3/calendars/" +
"jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com" +
"/events?key=AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns",
"requests correct url based on the custom element attributes"
)
assert.equal(await el.promise.then(() => "READY"), "READY", "Response rendered")
assert.equal(el.childElementCount, 3, "3 events")
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 3, "default template applied")
assert.equal(eventEls[2], el.lastElementChild, "correct depth: calendar-events > event-instances")
})
QUnit.test('Component correctly uses default pending and resolved templates', async assert => {
globalThis.fetch = (url) => {
return Promise.resolve({ json: () => ({ items: [] }) }) // resolve with no items
}
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
></calendar-events>
`
const el = select("calendar-events")
assert.ok(el, "el inserted")
assert.ok(el.promise, "el promises")
assert.ok(select(".calendar-events-pending"), "pending exists before resolved")
assert.equal(selectAll("*").length, 2, "1 element inside custom element only")
assert.equal(await el.promise.then(() => "READY"), "READY", "Response rendered")
assert.equal(selectAll("*").length, 3, "2 elements total inside custom element after resolved")
assert.equal(el.textContent, "There are no events to show.", "correct resolved but empty template used")
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 0, "no events rendered")
})
QUnit.test('Component correctly uses default rejected template', async assert => {
globalThis.fetch = (url) => {
return Promise.reject({})
}
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
></calendar-events>
`
const el = select("calendar-events")
assert.ok(el, "el inserted")
assert.ok(el.promise, "el promises")
assert.ok(select(".calendar-events-pending"), "pending exists before rejected")
assert.equal(await el.promise.catch(() => "READY"), "READY", "Response rendered")
assert.equal(selectAll("*").length, 3, "3 elements total after rejected")
assert.equal(el.textContent, "Sorry, events can't load right now.", "correct rejected template used")
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 0, "no events rendered")
})
QUnit.test('Component correctly parses and injects event data into the default event template', async assert => {
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
></calendar-events>
`
await select("calendar-events").promise
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 3, "default template applied")
assert.equal(
eventEls[1].querySelector(".event-title").innerHTML,
"JS.Chi April Meetup - JavaScript Lightning Talks",
"title information correctly parsed & inserted"
)
assert.equal(
eventEls[1].querySelector(".event-group").innerHTML,
"Bitovi Community",
"group information correctly parsed & inserted"
)
// TODO: this assertion might fail outside of the US because locales isn't specified
assert.equal(
dateShapeRx["Apr 18, 2023, 6:30 PM"].test(
eventEls[1].querySelector(".event-date").innerHTML
),
true,
`date information (${eventEls[1].querySelector(".event-date").innerHTML}) correctly parsed & inserted`
)
assert.equal(
eventEls[1].querySelector(".event-location").innerHTML,
"29 N Upper Wacker Dr, Chicago, IL 60606, USA",
"location information correctly parsed & inserted"
)
assert.equal(
eventEls[1].querySelector(".event-body").innerHTML,
"IN PERSON ChicagoJS Meetup<br><br>See all the details and RSVP here:&nbsp;<a href=\"https://www.google.com/url?q=https://www.meetup.com/js-chi/events/288515502/&amp;sa=D&amp;source=calendar&amp;ust=1680538571058455&amp;usg=AOvVaw1Uqtg1pPsvsyv6sfG4NFK7\" target=\"_blank\">https://www.meetup.com/js-chi/events/288515502/</a>",
"body information correctly parsed & inserted"
)
assert.equal(
eventEls[1].querySelector("a.event-url").href,
"https://www.google.com/calendar/event?eid=N2ZjbHY2NTJmdTlydTg2NnZhbmpsOG1vaTEganVwaXRlcmpzLmNvbV9nMjd2Y2szNm5pZmJucXJna2N0a29hbnFiNEBn",
"url information correctly parsed & inserted"
)
})
/* custom templates */
QUnit.test('Component correctly uses custom pending and resolved templates', async assert => {
globalThis.fetch = (url) => {
return Promise.resolve({ json: () => ({ items: [] }) }) // resolve with no items
}
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
>
<template>
<span class="calendar-events-pending custom">Hi, ily.</span>
<span class="calendar-events-resolved custom">Ily2.</span>
</template>
</calendar-events>
`
const el = select("calendar-events")
assert.ok(select("span.calendar-events-pending.custom"), "custom pending exists before resolved")
assert.equal(el.textContent, "Hi, ily.", "custom pending template used")
assert.equal(selectAll("*").length, 2)
assert.equal(await el.promise.then(() => "READY"), "READY", "Response rendered")
assert.ok(select("span.calendar-events-resolved.custom"), "custom resolved used")
assert.equal(el.textContent, "Ily2.", "custom pending template used")
assert.equal(selectAll("*").length, 2)
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 0, "no events rendered")
})
QUnit.test('Component correctly uses custom rejected template', async assert => {
globalThis.fetch = (url) => {
return Promise.reject({})
}
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
>
<template>
<span class="calendar-events-event custom"><b>event</b></span>
<span class="calendar-events-rejected custom">oh no.</span>
</template>
</calendar-events>
`
const el = select("calendar-events")
assert.ok(select(".calendar-events-pending"), "default pending exists before rejected")
assert.equal(await el.promise.catch(() => "READY"), "READY", "Response rendered")
assert.equal(el.textContent, "oh no.", "correct custom rejected template used")
assert.equal(selectAll("*").length, 2)
const eventEls = selectAll(".calendar-events-event")
assert.equal(eventEls.length, 0, "no events rendered")
})
QUnit.test('Component correctly uses custom template for events when parts aren\'t specified', async assert => {
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
>
<template>
<span class="event-title custom"></span>
<span class="event-group custom"></span>
<span class="event-title custom duplicate">duplicate</span>
</template>
</calendar-events>
`
assert.ok(select(".calendar-events-pending"), "default pending exists before resolved")
const el = select("calendar-events")
await el.promise
assert.equal(
el.querySelectorAll(".event-title.custom")[2].innerHTML,
"JS.Chi April Meetup - JavaScript Lightning Talks",
"title information correctly parsed & inserted"
)
assert.equal(
el.querySelectorAll(".event-title.custom")[3].innerHTML,
"JS.Chi April Meetup - JavaScript Lightning Talks",
"title information correctly parsed & inserted"
)
assert.equal(
el.querySelectorAll(".event-group.custom")[1].innerHTML,
"Bitovi Community",
"group information correctly parsed & inserted"
)
assert.equal(selectAll("calendar-events > *").length, 9)
})
QUnit.test('Component correctly uses custom template part for events', async assert => {
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
>
<template>
<div class="calendar-events-event">
<span class="event-title"></span>
<span class="event-group"></span>
<span class="event-title duplicate">duplicate</span>
</div>
<span class="calendar-events-pending">Hi, ily.</span>
</template>
</calendar-events>
`
const el = select("calendar-events")
assert.ok(select("span.calendar-events-pending"), "custom pending exists before resolved")
assert.equal(el.textContent, "Hi, ily.", "custom pending template used")
await el.promise
assert.equal(
el.querySelectorAll(".event-title")[2].innerHTML,
"JS.Chi April Meetup - JavaScript Lightning Talks",
"title information correctly parsed & inserted"
)
assert.equal(
el.querySelectorAll(".event-title")[3].innerHTML,
"JS.Chi April Meetup - JavaScript Lightning Talks",
"title information correctly parsed & inserted"
)
assert.equal(
el.querySelectorAll(".event-group")[1].innerHTML,
"Bitovi Community",
"group information correctly parsed & inserted"
)
assert.equal(selectAll("calendar-events > *").length, 3)
})
QUnit.test('event-all-day class works', async assert => {
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="100"
show-recurring
>
<template>
<div class="calendar-events-event event-all-day">
<span class="event-title"></span>
<span class="event-group"></span>
</div>
</template>
</calendar-events>
`
const el = select("calendar-events")
await el.promise
assert.equal(selectAll(".event-all-day[data-all-day=true]").length, 9, "data-all-day appeneded with correct value")
assert.equal(selectAll(".event-all-day[data-all-day=false]").length, 91, "data-all-day appeneded with correct value")
})
const eventSkel = {
kind: "calendar#event",
etag: '"3031026261682000"',
id: "2mag0ad2crcv9rmujah0hvek6q",
status: "confirmed",
htmlLink: "https://www.google.com/calendar/event?eid=Mm1hZzBhZDJjcmN2OXJtdWphaDBodmVrNnEganVwaXRlcmpzLmNvbV9nMjd2Y2szNm5pZmJucXJna2N0a29hbnFiNEBn",
created: "2021-03-05T00:54:10.000Z",
updated: "2021-03-05T00:54:53.841Z",
summary: "Love, try to love.",
creator: { email: "in@bit.testdata", displayName: "Jane Ori" },
organizer: { email: "b4@gro.testdata", self: true, displayName: "title goes here" },
start: { date: new Date().toISOString() },
end: { date: new Date().toISOString() },
description: "https://www.youtube.com/watch?v=cGgVoqr78gk",
transparency: "transparent",
iCalUID: "6q@goo.testdata",
sequence: 0,
eventType: "default"
}
const createEvent = (title, description, start) => {
return Object.assign({}, eventSkel, {
organizer: { email: "b4@gro.testdata", self: true, displayName: title },
description,
start: start || eventSkel.start
})
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
QUnit.test('Component correctly uses custom event-date field and its variants', async assert => {
globalThis.fetch = () => Promise.resolve({
json: () => ({
items: [
createEvent("test event-date", `Test Description 1`, { dateTime: new Date("Apr 7, 2023, 6:30 PM").toISOString() }),
createEvent("test event-date", `Test Description 2`, { date: new Date("Tue, Apr 18, 2023").toISOString() })
]
})
})
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="1"
show-recurring
>
<template>
<div class="calendar-events-event">
<span class="event-date" data-locales="en-US"></span>
<span class="event-date" data-locales="en-US" data-options="month:short day:2-digit weekday:short"></span>
<span class="event-date" data-locales="en-US" data-options="weekday"></span>
<span class="event-date" data-locales="en-US" data-options="year"></span>
<span class="event-date" data-locales="en-US" data-options="month"></span>
<span class="event-date" data-locales="en-US" data-options="day"></span>
<span class="event-date" data-locales="en-US" data-options="hour"></span>
<span class="event-date" data-locales="en-US" data-options="hour hourCycle:h24"></span>
<span class="event-date" data-locales="en-US" data-options="hour hour12:false"></span>
<span class="event-date" data-locales="en-US" data-options="hour minute"></span>
<span class="event-date" data-locales="en-US" data-options="dayPeriod"></span>
<span class="event-date" data-locales="en-US" data-options="minute"></span>
<span class="event-date" data-locales="en-US" data-options="second"></span>
<span class="event-date" data-locales="en-US" data-options="weekday:long"></span>
<span class="event-date" data-locales="en-US" data-options="weekday:short"></span>
<span class="event-date" data-locales="en-US" data-options="weekday:narrow"></span>
<span class="event-date" data-locales="en-US" data-options="year:numeric"></span>
<span class="event-date" data-locales="en-US" data-options="year:2-digit"></span>
<span class="event-date" data-locales="en-US" data-options="month:numeric"></span>
<span class="event-date" data-locales="en-US" data-options="month:2-digit"></span>
<span class="event-date" data-locales="en-US" data-options="month:long"></span>
<span class="event-date" data-locales="en-US" data-options="month:short"></span>
<span class="event-date" data-locales="en-US" data-options="month:narrow"></span>
<span class="event-date" data-locales="en-US" data-options="day:numeric"></span>
<span class="event-date" data-locales="en-US" data-options="day:2-digit"></span>
</div>
</template>
</calendar-events>
`
const el = select("calendar-events")
await el.promise
const dateEls = selectAll(".event-date")
assert.equal(
dateShapeRx["Apr 18, 2023, 6:30 PM"].test(
dateEls[0].innerHTML
),
true,
`date information (${dateEls[0].innerHTML}) correctly parsed & inserted`
)
assert.equal(
dateShapeRx["Tue, Apr 04"].test(
dateEls[1].innerHTML
),
true,
`date information (${dateEls[1].innerHTML}) correctly parsed & inserted`
)
assert.equal(el.querySelector('[data-options="weekday"]').innerHTML, "Fri", "weekday by itself works")
assert.equal(el.querySelector('[data-options="year"]').innerHTML, "2023", "year by itself works")
assert.equal(el.querySelector('[data-options="month"]').innerHTML, "Apr", "month by itself works")
assert.equal(el.querySelector('[data-options="day"]').innerHTML, "7", "day by itself works")
assert.equal(el.querySelector('[data-options="hour"]').innerHTML, "6 PM", "hour by itself works by auto appending AM/PM")
assert.equal(el.querySelector('[data-options="hour hourCycle:h24"]').innerHTML, "18", "hour hourCycle:h24 works")
assert.equal(el.querySelector('[data-options="hour hour12:false"]').innerHTML, "18", "hour hour12:false works")
assert.equal(el.querySelector('[data-options="hour minute"]').innerHTML, "6:30 PM", "hour minute works")
// None of these work well when used alone:
// const dayPeriod = dateEls[10].innerHTML
// const minute = dateEls[11].innerHTML
// const second = dateEls[12].innerHTML
// assert.equal(dayPeriod, "PM", "dayPeriod (AM/PM) works")
// assert.equal(minute, "00", "minute by itself works")
// assert.equal(second, "00", "second by itself works")
// we might want to consider hacking in our own homebrew variants like:
// hour without dayPeriod auto attached, dayPaeriod (AM/PM) by itself, minute:2-digit by itself, timeZoneName alone
assert.equal(el.querySelector('[data-options="weekday:long"]').innerHTML, "Friday", "weekday:long by itself works")
assert.equal(el.querySelector('[data-options="weekday:short"]').innerHTML, "Fri", "weekday:short by itself works")
assert.equal(el.querySelector('[data-options="weekday:narrow"]').innerHTML, "F", "weekday:narrow by itself works")
assert.equal(el.querySelector('[data-options="year:numeric"]').innerHTML, "2023", "year:numeric by itself works")
assert.equal(el.querySelector('[data-options="year:2-digit"]').innerHTML, "23", "year:2-digit by itself works")
assert.equal(el.querySelector('[data-options="month:numeric"]').innerHTML, "4", "month:numeric by itself works")
assert.equal(el.querySelector('[data-options="month:2-digit"]').innerHTML, "04", "month:2-digit by itself works")
assert.equal(el.querySelector('[data-options="month:long"]').innerHTML, "April", "month:long by itself works")
assert.equal(el.querySelector('[data-options="month:short"]').innerHTML, "Apr", "month:short by itself works")
assert.equal(el.querySelector('[data-options="month:narrow"]').innerHTML, "A", "month:narrow by itself works")
assert.equal(el.querySelector('[data-options="day:numeric"]').innerHTML, "7", "day:numeric by itself works")
assert.equal(el.querySelector('[data-options="day:2-digit"]').innerHTML, "07", "day:2-digit by itself works")
})
QUnit.test('Component correctly handles html descriptions', async assert => {
const onlyHTMLEvents = googleCalendarAPIResponse.items.filter(ev => (ev.description || '').indexOf("<") > -1)
globalThis.fetch = () => { return Promise.resolve({ json: () => ({ items: onlyHTMLEvents }) }) }
let tagCount = 0
const descriptions = onlyHTMLEvents.map(x => {
tagCount += [...x.description.matchAll(/<\w/g)].length
return x.description
})
// console.log(descriptions)
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="30"
show-recurring
>
<template>
<div class="calendar-events-event event-body"></div>
</template>
</calendar-events>
`
const el = select("calendar-events")
await el.promise
const eventEls = selectAll(".calendar-events-event")
assert.equal(descriptions.length, 23, "expected number (23) of html descriptions in the data")
assert.equal(eventEls.length, descriptions.length, `${descriptions.length} events printed with html embeded`)
assert.equal(tagCount, 129, "expected number (129) of embeded html tags in the data")
assert.equal(selectAll(".calendar-events-event *").length, tagCount, `all ${tagCount} embeded html elements rendered`)
})
QUnit.test('Component correctly handles data-find', async assert => {
globalThis.fetch = () => Promise.resolve({
json: () => ({
items: [
createEvent("test data-find", `
Test Description 1\u003cbr\u003e
\u003ca href="http://bitovi.com/join-our-event?invitedBy=Heather"\u003eRegister for the event here.\u003c/a\u003e
\u003ca href="http://bitovi.com/services/augmented-ui-consulting?leadGenTrackingFromEventId=222077"\u003e
Join the queue for augmented-ui consulting!
\u003c/a\u003e
\u003ca href="https://i.imgur.com/i7eZZ5X.jpg"\u003eAndrew's meme of the event.\u003c/a\u003e
\u003ca href="https://i.imgur.com/i7eZZ5X.jpg"\u003eThis is-found.\u003c/a\u003e
\u003ca href="about:404"\u003eBAD register LINK\u003c/a\u003e
`),
createEvent("test data-find", `
Test Description 2
`)
]
})
})
fixture.innerHTML = `
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="30"
show-recurring
>
<template>
<div class="calendar-events-event">
<h1 class="event-title"></h1>
<a href="http://bitovi.com/" data-find="Register" data-cut></a>
<img data-find="meme" data-cut>
<div class="event-body"></div>
<a data-find="augmented-ui">High Tech, Low Effort. The future is augmented.</a>
<a data-find="register" data-cut>Only empty tags get textContent updated, so this text won't change.</a>
<a data-find="not-found is-found" data-cut>multiple terms work</a>
<img data-find="not-found">
<img src="#" data-find="not-found" alt="data-find els with default values stick around when not found">
<a data-find="not-found">like tears in the rain</a>
<a class="event-url" data-find="not-found">Still Here, saved by event url</a>
<a class="event-url" data-find="meme">Found url overrides event url</a>
</div>
</template>
</calendar-events>
`
const el = select("calendar-events")
await el.promise
const eventEls = selectAll(".calendar-events-event")
const registerURL = "http://bitovi.com/join-our-event?invitedBy=Heather"
const registerFound = eventEls[0].querySelectorAll("[data-find='register' i]")
assert.equal(registerFound.length, 2, "find register links still present")
assert.equal(registerFound[0].href, registerURL, "register was found and the href was copied correctly")
assert.equal(registerFound[0].textContent, "Register for the event here.", "register textContent was copied correctly")
assert.equal(registerFound[1].href, registerURL, "register was found and the href was copied correctly twice")
assert.equal(registerFound[1].textContent, "Only empty tags get textContent updated, so this text won't change.", "textContent was correctly NOT copied")
assert.equal(eventEls[0].querySelector(".event-body a[href='" + registerURL + "']"), null, "register link removed from event-body")
const augURL = "http://bitovi.com/services/augmented-ui-consulting?leadGenTrackingFromEventId=222077"
const augFound = eventEls[0].querySelectorAll("[data-find='augmented-ui' i]")
assert.equal(augFound.length, 1, "find augmented-ui link still present")
assert.equal(augFound[0].href, augURL, "augmented-ui link was found and the href was copied correctly")
assert.equal(augFound[0].textContent, "High Tech, Low Effort. The future is augmented.", "augmented-ui link text correctly NOT updated")
assert.equal(eventEls[0].querySelectorAll(".event-body a[href='" + augURL + "']").length, 1, "augmented-ui link not removed from event-body because it was found by an element without data-cut")
const memeURL = "https://i.imgur.com/i7eZZ5X.jpg"
const memeFound = eventEls[0].querySelectorAll("[data-find='meme' i]")
assert.equal(memeFound[0].src, memeURL, "meme link was found and the src was updated correctly")
assert.equal(memeFound[0].alt, "Andrew's meme of the event.", "meme alt updated correctly")
assert.equal(memeFound[1].href, memeURL, "meme link was found and the href overrode event-url correctly")
const multiFind = eventEls[0].querySelectorAll("[data-find~='is-found' i]")
assert.equal(multiFind[0].href, memeURL, "multi data-find works")
assert.equal(eventEls[0].querySelectorAll("img[data-find='not-found' i]").length, 1, "only one data-find not-found img remains")
assert.equal(eventEls[0].querySelectorAll("a[data-find='not-found' i]").length, 1, "only one data-find not-found link remains")
assert.equal(eventEls[0].querySelector("a[data-find='not-found' i]").href, eventSkel.htmlLink, "data-find not-found link remains because it is also event-url")
assert.equal(eventEls[1].querySelectorAll("[data-find].event-url").length, 2, "only 2 of 4 remaining data-find els exist because they fell back to event-urls in the template; event supplied no matches")
assert.equal(eventEls[1].querySelectorAll("[data-find]").length, 4, "only 2 of 4 remaining data-find els exist because of default urls in the template; event supplied no matches")
})
/*
.event-location // needs tests, does link substitution stuff, had a bug
.event-body // need to test plaintext too
*/

@@ -1,261 +0,376 @@

function safeCustomElement(tag, constructor, prototype){
prototype = prototype || constructor.prototype;
var Element = function(){
var result;
if(typeof Reflect !== "undefined") {
result = Reflect.construct(HTMLElement, [], new.target);
} else {
result = HTMLElement.apply(this, arguments);
}
constructor.apply(result, arguments);
return result;
};
if(typeof HTMLElement !== undefined) {
Element.prototype = Object.create(HTMLElement.prototype);
}
Object.getOwnPropertyNames(prototype).forEach(function(property){
Object.defineProperty(Element.prototype, property,
Object.getOwnPropertyDescriptor(prototype, property));
});
if(typeof customElements !== "undefined") {
customElements.define(tag, Element);
}
function safeCustomElement(tag, constructor, prototype) {
prototype = prototype || constructor.prototype;
var Element = function () {
var result;
if (typeof Reflect !== "undefined") {
result = Reflect.construct(HTMLElement, [], new.target);
} else {
result = HTMLElement.apply(this, arguments);
}
constructor.apply(result, arguments);
return result;
};
if (typeof HTMLElement !== undefined) {
Element.prototype = Object.create(HTMLElement.prototype);
}
Object.getOwnPropertyNames(prototype).forEach(function (property) {
Object.defineProperty(Element.prototype, property,
Object.getOwnPropertyDescriptor(prototype, property));
});
if (typeof customElements !== "undefined") {
customElements.define(tag, Element);
}
return Element;
return Element;
}
safeCustomElement.supported = (typeof Reflect !== "undefined") &&
(typeof HTMLElement !== undefined) &&
(typeof customElements !== "undefined");
(typeof HTMLElement !== undefined) &&
(typeof customElements !== "undefined");
function todayDate(){
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setMilliseconds(0);
date.setMilliseconds(0);
return date;
function getSortedEvents(eventsData) {
return eventsData.items.filter(function (event) {
return event.status !== 'cancelled';
}).map(function (event) {
var clone = Object.assign({}, event);
var dateStr = event.start.dateTime || event.start.date;
var date = new Date(dateStr);
clone.start.time = date;
return clone;
}).sort(function (eventA, eventB) {
return eventA.start.time - eventB.start.time;
});
}
function getSortedEvents(eventsData){
return eventsData.items.filter(function(event) {
return event.status !== 'cancelled';
}).map(function(event){
var clone = Object.assign({}, event);
var dateStr = event.start.dateTime || event.start.date;
var date = new Date(dateStr);
clone.start.time = date;
return clone;
}).sort(function(eventA, eventB){
return eventA.start.time - eventB.start.time;
});
function getFirstEventIndexFromToday(sortedEvents) {
var today = new Date().setHours(0, 0, 0, 0);
return sortedEvents.findIndex(function (event) {
return event.start.time > today;
});
}
function getFirstEventIndexFromToday(sortedEvents){
var today = todayDate();
return sortedEvents.findIndex(function(event){
return event.start.time > today;
});
}
function getPastAndFutureEvents(sortedEvents) {
var index = getFirstEventIndexFromToday(sortedEvents);
if(index !== -1) {
return {
future: sortedEvents.slice(index),
past: sortedEvents.slice(0,index)
};
} else {
return {
future: [],
past: sortedEvents
};
}
var index = getFirstEventIndexFromToday(sortedEvents);
if (index !== -1) {
return {
future: sortedEvents.slice(index),
past: sortedEvents.slice(0, index)
};
} else {
return {
future: [],
past: sortedEvents
};
}
}
function filterRecurringEvents(sortedEvents){
return sortedEvents.filter(function(event){
return !event.recurringEventId && event.status !== "cancelled";
});
function filterRecurringEvents(sortedEvents) {
return sortedEvents.filter(function (event) {
return !event.recurringEventId && event.status !== "cancelled";
});
}
function getEvents(pastAndFutureEvents, count) {
var futureEvents = pastAndFutureEvents.future.length,
pastEventsNeeded = count - futureEvents,
past = pastAndFutureEvents.past;
// older event start dates are at the start of both arrays
var futureEvents = pastAndFutureEvents.future,
pastEventsNeeded = count - futureEvents.length,
past = pastAndFutureEvents.past;
if(pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded)
.concat(pastAndFutureEvents.future);
} else {
return pastAndFutureEvents.future.slice(0, count);
}
if (pastEventsNeeded >= past.length) {
return past.concat(futureEvents);
} else if (pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded)
.concat(futureEvents);
} else {
return futureEvents.slice(0, count);
}
}
function eventDescriptionHTMLGroupAndUrl(event) {
var description = (event.description || '').trim();
var lines, last;
var isHTML = description.includes("<br>");
if( isHTML ) {
lines = description.split(/<br\/?>/);
} else {
lines = description.split(/\r?\n/);
}
last = lines.pop() || '';
var url = event.htmlLink;
var description = (event.description || '').trim();
// var lines, last;
// var isHTML = description.includes("<br>");
// if (isHTML) {
// lines = description.split(/<br\/?>/);
// } else {
// lines = description.split(/\r?\n/);
// }
// last = lines.pop() || '';
var url = event.htmlLink;
return {
descriptionHTML : description,
group: event.organizer.displayName,
url: url
};
return {
descriptionHTML: description,
group: event.organizer.displayName,
url: url
};
}
// TODO: Detect user Locale and use other Locale functions to set key-without-value defaults
const defaultDateValuesWhenOnlyKeySpecified = {
dateStyle: "full",
timeStyle: "short",
calendar: "gregory",
dayPeriod: "short", // uses "long" if specified at all
numberingSystem: "latn",
hour12: true,
hourCycle: "h12",
weekday: "short",
year: "numeric",
month: "short",
day: "numeric",
hour: "numeric", // includes dayPeriod:short if 12 hour
minute: "2-digit", // uses "numeric" when by itself
second: "2-digit",
timeZoneName: "short" // forces full date if used
};
// localesStr = "en-US" etc or space speparated list of valid tags
// optionsStr = "day month:short year:2-digit" space sepearted list of what the date output should include
function eventDate(eventStart, localesStr, optionsStr) {
const date = new Date(eventStart.dateTime || eventStart.date);
const locales = localesStr && localesStr.replace(/[^a-z0-9- ]/gi, "").split(" ");
const options = optionsStr ? {} : (
eventStart.dateTime ? {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit"
} : {
month: "short",
day: "numeric",
year: "numeric"
}
);
// "day month:short year:2-digit" -> { day: 'numeric', month: 'short', year: '2-digit' }
optionsStr && optionsStr.replace(
/\b([a-z0-9]+)(?::([a-z0-9-_/]+))?(\s|$)/gi,
(_, key, val) => options[key] = val || defaultDateValuesWhenOnlyKeySpecified[key]
);
function eventDate(event) {
var startDate = event.start.date;
var startDateTime = event.start.dateTime;
var date;
if (startDateTime) {
date = new Date(startDateTime);
return date.toLocaleString(undefined, {month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit"});
if (options.hour12) {
options.hour12 = options.hour12 !== "false"; // to bool if set, favor true
}
//return datetime.format('MMM Do, YYYY — h:mma');
} else if (startDate) {
date = new Date(startDate);
return date.toLocaleString(undefined, {month: "short", day: "numeric", year: "numeric"});
}
return date.toLocaleString(locales || undefined, options);
}
function selectAllIncludeSelf(container, query) {
const qsa = [...container.querySelectorAll(query)];
if (container.matches && container.matches(query)) {
qsa.push(container);
}
return qsa;
}
function setDateContent(container, query, value) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = eventDate(
value,
el.getAttribute("data-locales"),
el.getAttribute("data-options")
);
});
}
function setTextContent(container, query, value) {
container.querySelectorAll(query).forEach(function(el){
el.textContent = value;
});
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = value;
});
}
function setHtmlContent(container, query, value) {
container.querySelectorAll(query).forEach(function(el){
el.innerHTML = value;
});
selectAllIncludeSelf(container, query).forEach(function (el) {
el.innerHTML = value;
});
}
function shorten(text){
if(text.length > 30) {
return text.slice(0,40)+"&mldr;";
} else {
return text;
}
function dataFindThenCutOrCopy(container, dataFindRegEx) {
selectAllIncludeSelf(container, "a.event-body, .event-body a").forEach(function (link) {
const found = link.textContent.match(dataFindRegEx);
if (found) {
// Template links populated by data-find
selectAllIncludeSelf(container, `a[data-find~="${found[0]}" i]:not([data-found])`).forEach(function (finder) {
finder.href = link.href;
finder.textContent = finder.textContent || link.textContent.trim(); // copy link's text if template doesn't have text
if (finder.hasAttribute("data-cut")) { // default behavior is to keep the link. data-cut flags it for removal
link.remove(); // remove source link from event-body since it was moved to finder element
}
finder.setAttribute("data-found", found[0]);
});
// Template images populated by data-find
selectAllIncludeSelf(container, `img[data-find~="${found[0]}" i]:not([data-found])`).forEach(function (finder) {
finder.src = link.href; // copy link's href as the img.src
finder.alt = link.textContent.trim(); // copy link's text as image's alt
if (finder.hasAttribute("data-cut")) { // default behavior is to keep the link. data-cut flags it for removal
link.remove(); // remove source link from event-body since it was moved to finder element
}
finder.setAttribute("data-found", found[0]);
});
}
});
// removing data-found info, was used above for avoiding problems from duplicate description sources
selectAllIncludeSelf(container, `[data-find][data-found]`).forEach(function (finder) {
finder.removeAttribute("data-found");
});
// any data-find elements who wound up without a match are removed because they're invalid
// TODO: put data-not-found on root element populated with data-find keys that weren't found to help with styling
selectAllIncludeSelf(container, "a:not([href]), a[href=''], img:not([src]), img[src='']").forEach(
function (lostNotFound) {
lostNotFound.remove();
}
);
}
function shorten(text) {
if (text.length > 30) {
return text.slice(0, 40) + "&mldr;";
} else {
return text;
}
}
// from https://stackoverflow.com/questions/37684/how-to-replace-plain-urls-with-links
function linkify(inputText) {
var replacedText, replacePattern1, replacePattern2, replacePattern3;
var replacedText, replacePattern1, replacePattern2, replacePattern3;
//URLs starting with http://, https://, or ftp://
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, function(all){
return '<a href="'+all+'" target="_blank">'+shorten(all)+'</a>';
});
// URLs starting with http://, https://, or ftp://
replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, function (all) {
return '<a href="' + all + '" target="_blank">' + shorten(all) + '</a>';
});
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
// URLs starting with "www." (without // before it, or it'd re-link the ones done above).
replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
//Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
// Change email addresses to mailto:: links.
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
return replacedText;
return replacedText;
}
function defaultTemplate(){
var container = document.createElement("div");
container.innerHTML = "<div class='event-header'>"+
"<div class='event-summary'><a class='event-url event-title'></a></div>"+
"<div class='event-group'></div>"+
"<div class='event-date'></div>"+
"<div class='event-location'></div>"+
"<div class='event-body'></div>"+
"</div>"+
"<div class='event-footer'><a class='event-url'>View Event</a></div>";
var frag = document.createDocumentFragment();
frag.appendChild(container);
return frag;
}
module.exports = safeCustomElement("calendar-events", function () {
}, {
get apiKey() {
return this.getAttribute("api-key");
},
get calendarId() {
return this.getAttribute("calendar-id");
},
get showRecurring() {
return this.hasAttribute("show-recurring");
},
get eventCount() {
return parseInt(this.getAttribute("event-count"), 10) || 10;
},
readTemplates() {
const defaultHTML = {
pending: "<div class='calendar-events-pending'></div>",
rejected: "<div class='calendar-events-rejected'><p>Sorry, events can't load right now.</p></div>",
resolved: "<div class='calendar-events-resolved'><p>There are no events to show.</p></div>",
event: `
<div class="calendar-events-event">
<div class="event-header">
<div class="event-summary"><a class="event-url event-title"></a></div>
<div class="event-group"></div>
<div class="event-date"></div>
<div class="event-location"></div>
<div class="event-body"></div>
</div>
<div class="event-footer"><a class="event-url">View Event</a></div>
</div>
`
};
const templateChild = this.querySelector("template");
const selectorPrefix = ".calendar-events-";
const allParts = Object.keys(defaultHTML);
const templates = {};
allParts.forEach(part => {
const customizedPart = templateChild && templateChild.content.querySelector(`${selectorPrefix}${part}`);
templates[part] = customizedPart || (
Object.assign(document.createElement("div"), { innerHTML: defaultHTML[part] }).firstElementChild
);
});
module.exports = safeCustomElement("calendar-events", function(){
// if there is a custom template but not specifying parts, assume all content is the "event" part
const anyPartSelector = allParts.map(part => `${selectorPrefix}${part}`).join(", ");
const anyPartCustomized = (templateChild && templateChild.content.querySelector(anyPartSelector));
if (templateChild && !anyPartCustomized) {
templates.event = templateChild.content;
}
}, {
get apiKey(){
return this.getAttribute("api-key");
},
get calendarId(){
return this.getAttribute("calendar-id");
},
get showRecurring(){
return this.hasAttribute("show-recurring");
},
get eventCount(){
return parseInt( this.getAttribute("event-count"), 10) || 10;
},
connectedCallback: function(){
var template = this.querySelector("template");
this.template = template ? template.content : defaultTemplate();
this.innerHTML = "<div class='calendar-events-pending'></div>";
var url = "https://www.googleapis.com/calendar/v3/calendars/"+
this.calendarId+"/events?key="+this.apiKey;
fetch( url ).then(function(response){
return response.json();
})
.then(getSortedEvents)
.then(function(sortedEvents){
if(this.showRecurring) {
return sortedEvents;
} else {
return filterRecurringEvents( sortedEvents );
}
}.bind(this))
.then(getPastAndFutureEvents)
.then(this.showEvents.bind(this))
.catch(function(err){
this.innerHTML = "<div class='calendar-events-rejected'>"+
"<p>Sorry, events can't load right now.</p>"+
"</div>";
throw err;
}.bind(this));
return templates;
},
showTemplate(part) {
this.innerHTML = ""; // this.replaceChildren() not supported well enough yet
this.appendChild(this.templates[part].cloneNode(true));
},
connectedCallback: function () {
this.templates = this.readTemplates();
},
showEvents: function(getPastAndFutureEvents){
if(!getPastAndFutureEvents.future.length &&
!getPastAndFutureEvents.past.length) {
this.showTemplate("pending");
this.innerHTML = "<div class='calendar-events-resolved'>"+
"<p>There are no events to show.</p>"+
"</div>";
return;
}
var events = getEvents(getPastAndFutureEvents, this.eventCount);
var url = `https://www.googleapis.com/calendar/v3/calendars/${this.calendarId}/events?key=${this.apiKey}`;
this.promise = fetch(url).then(function (response) {
return response.json();
})
.then(getSortedEvents)
.then(function (sortedEvents) {
if (this.showRecurring) {
return sortedEvents;
} else {
return filterRecurringEvents(sortedEvents);
}
}.bind(this))
.then(getPastAndFutureEvents)
.then(this.showEvents.bind(this))
.catch(function (err) {
this.showTemplate("rejected");
throw err;
}.bind(this));
},
showEvents: function (pastAndFutureEvents) {
if (!pastAndFutureEvents.future.length && !pastAndFutureEvents.past.length) {
this.showTemplate("resolved");
return;
}
const events = getEvents(pastAndFutureEvents, this.eventCount);
var elements = events.map( function( event ) {
var container = this.template.cloneNode(true);
container.firstElementChild.classList.add("calendar-events-event");
const eventTemplate = this.templates.event;
// Create regex to search link text for terms specified within the event template
const findTerms = selectAllIncludeSelf(eventTemplate, "[data-find]").map(
finder => finder.getAttribute("data-find").trim().replace(/\s+/g, "|")
).join("|");
const dataFindRegEx = findTerms && new RegExp(`\\b(?:${findTerms})\\b`, "i");
var metaData = eventDescriptionHTMLGroupAndUrl(event);
const elements = events.map((event) => {
var container = eventTemplate.cloneNode(true);
container.querySelectorAll("a.event-url").forEach(function(a){
a.href = metaData.url;
});
setTextContent(container, ".event-title", event.summary);
setTextContent(container, ".event-group", metaData.group);
setTextContent(container, ".event-date", eventDate(event) );
setHtmlContent(container, ".event-location", linkify(event.location || event.hangoutLink) );
var metaData = eventDescriptionHTMLGroupAndUrl(event);
setHtmlContent(container, ".event-body", metaData.descriptionHTML );
selectAllIncludeSelf(container, "a.event-url").forEach(function (a) {
a.href = metaData.url;
});
selectAllIncludeSelf(container, ".event-all-day").forEach(function (el) {
el.setAttribute("data-all-day", !event.start.dateTime);
});
setTextContent(container, ".event-title", event.summary);
setTextContent(container, ".event-group", metaData.group);
setDateContent(container, ".event-date", event.start);
return container;
}.bind(this) );
elements.forEach(function(element){
this.appendChild(element);
}.bind(this));
}
const locatable = event.location || event.hangoutLink;
locatable && setHtmlContent(container, ".event-location", linkify(locatable));
setHtmlContent(container, ".event-body", metaData.descriptionHTML);
findTerms && dataFindThenCutOrCopy(container, dataFindRegEx); // requires .event-body to be populated first
return container;
});
// this.replaceChildren(...elements)
this.innerHTML = ""; // delete pending state element
elements.forEach(function (element) {
this.appendChild(element);
}.bind(this));
}
});

@@ -1,2 +0,2 @@

/*@bitovi/calendar-events-component@0.0.10#calendar-events*/
/*@bitovi/calendar-events-component@0.1.0#calendar-events*/
define(function (require, exports, module) {

@@ -27,10 +27,2 @@ function safeCustomElement(tag, constructor, prototype) {

safeCustomElement.supported = typeof Reflect !== 'undefined' && typeof HTMLElement !== undefined && typeof customElements !== 'undefined';
function todayDate() {
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setMilliseconds(0);
date.setMilliseconds(0);
return date;
}
function getSortedEvents(eventsData) {

@@ -50,3 +42,3 @@ return eventsData.items.filter(function (event) {

function getFirstEventIndexFromToday(sortedEvents) {
var today = todayDate();
var today = new Date().setHours(0, 0, 0, 0);
return sortedEvents.findIndex(function (event) {

@@ -76,7 +68,9 @@ return event.start.time > today;

function getEvents(pastAndFutureEvents, count) {
var futureEvents = pastAndFutureEvents.future.length, pastEventsNeeded = count - futureEvents, past = pastAndFutureEvents.past;
if (pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded).concat(pastAndFutureEvents.future);
var futureEvents = pastAndFutureEvents.future, pastEventsNeeded = count - futureEvents.length, past = pastAndFutureEvents.past;
if (pastEventsNeeded >= past.length) {
return past.concat(futureEvents);
} else if (pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded).concat(futureEvents);
} else {
return pastAndFutureEvents.future.slice(0, count);
return futureEvents.slice(0, count);
}

@@ -86,10 +80,2 @@ }

var description = (event.description || '').trim();
var lines, last;
var isHTML = description.includes('<br>');
if (isHTML) {
lines = description.split(/<br\/?>/);
} else {
lines = description.split(/\r?\n/);
}
last = lines.pop() || '';
var url = event.htmlLink;

@@ -102,26 +88,53 @@ return {

}
function eventDate(event) {
var startDate = event.start.date;
var startDateTime = event.start.dateTime;
var date;
if (startDateTime) {
date = new Date(startDateTime);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
} else if (startDate) {
date = new Date(startDate);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
});
const defaultDateValuesWhenOnlyKeySpecified = {
dateStyle: 'full',
timeStyle: 'short',
calendar: 'gregory',
dayPeriod: 'short',
numberingSystem: 'latn',
hour12: true,
hourCycle: 'h12',
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
};
function eventDate(eventStart, localesStr, optionsStr) {
const date = new Date(eventStart.dateTime || eventStart.date);
const locales = localesStr && localesStr.replace(/[^a-z0-9- ]/gi, '').split(' ');
const options = optionsStr ? {} : eventStart.dateTime ? {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
} : {
month: 'short',
day: 'numeric',
year: 'numeric'
};
optionsStr && optionsStr.replace(/\b([a-z0-9]+)(?::([a-z0-9-_/]+))?(\s|$)/gi, (_, key, val) => options[key] = val || defaultDateValuesWhenOnlyKeySpecified[key]);
if (options.hour12) {
options.hour12 = options.hour12 !== 'false';
}
return date.toLocaleString(locales || undefined, options);
}
function selectAllIncludeSelf(container, query) {
const qsa = [...container.querySelectorAll(query)];
if (container.matches && container.matches(query)) {
qsa.push(container);
}
return qsa;
}
function setDateContent(container, query, value) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = eventDate(value, el.getAttribute('data-locales'), el.getAttribute('data-options'));
});
}
function setTextContent(container, query, value) {
container.querySelectorAll(query).forEach(function (el) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = value;

@@ -131,6 +144,35 @@ });

function setHtmlContent(container, query, value) {
container.querySelectorAll(query).forEach(function (el) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.innerHTML = value;
});
}
function dataFindThenCutOrCopy(container, dataFindRegEx) {
selectAllIncludeSelf(container, 'a.event-body, .event-body a').forEach(function (link) {
const found = link.textContent.match(dataFindRegEx);
if (found) {
selectAllIncludeSelf(container, `a[data-find~="${ found[0] }" i]:not([data-found])`).forEach(function (finder) {
finder.href = link.href;
finder.textContent = finder.textContent || link.textContent.trim();
if (finder.hasAttribute('data-cut')) {
link.remove();
}
finder.setAttribute('data-found', found[0]);
});
selectAllIncludeSelf(container, `img[data-find~="${ found[0] }" i]:not([data-found])`).forEach(function (finder) {
finder.src = link.href;
finder.alt = link.textContent.trim();
if (finder.hasAttribute('data-cut')) {
link.remove();
}
finder.setAttribute('data-found', found[0]);
});
}
});
selectAllIncludeSelf(container, `[data-find][data-found]`).forEach(function (finder) {
finder.removeAttribute('data-found');
});
selectAllIncludeSelf(container, 'a:not([href]), a[href=\'\'], img:not([src]), img[src=\'\']').forEach(function (lostNotFound) {
lostNotFound.remove();
});
}
function shorten(text) {

@@ -155,9 +197,2 @@ if (text.length > 30) {

}
function defaultTemplate() {
var container = document.createElement('div');
container.innerHTML = '<div class=\'event-header\'>' + '<div class=\'event-summary\'><a class=\'event-url event-title\'></a></div>' + '<div class=\'event-group\'></div>' + '<div class=\'event-date\'></div>' + '<div class=\'event-location\'></div>' + '<div class=\'event-body\'></div>' + '</div>' + '<div class=\'event-footer\'><a class=\'event-url\'>View Event</a></div>';
var frag = document.createDocumentFragment();
frag.appendChild(container);
return frag;
}
module.exports = safeCustomElement('calendar-events', function () {

@@ -177,8 +212,44 @@ }, {

},
readTemplates() {
const defaultHTML = {
pending: '<div class=\'calendar-events-pending\'></div>',
rejected: '<div class=\'calendar-events-rejected\'><p>Sorry, events can\'t load right now.</p></div>',
resolved: '<div class=\'calendar-events-resolved\'><p>There are no events to show.</p></div>',
event: `
<div class="calendar-events-event">
<div class="event-header">
<div class="event-summary"><a class="event-url event-title"></a></div>
<div class="event-group"></div>
<div class="event-date"></div>
<div class="event-location"></div>
<div class="event-body"></div>
</div>
<div class="event-footer"><a class="event-url">View Event</a></div>
</div>
`
};
const templateChild = this.querySelector('template');
const selectorPrefix = '.calendar-events-';
const allParts = Object.keys(defaultHTML);
const templates = {};
allParts.forEach(part => {
const customizedPart = templateChild && templateChild.content.querySelector(`${ selectorPrefix }${ part }`);
templates[part] = customizedPart || Object.assign(document.createElement('div'), { innerHTML: defaultHTML[part] }).firstElementChild;
});
const anyPartSelector = allParts.map(part => `${ selectorPrefix }${ part }`).join(', ');
const anyPartCustomized = templateChild && templateChild.content.querySelector(anyPartSelector);
if (templateChild && !anyPartCustomized) {
templates.event = templateChild.content;
}
return templates;
},
showTemplate(part) {
this.innerHTML = '';
this.appendChild(this.templates[part].cloneNode(true));
},
connectedCallback: function () {
var template = this.querySelector('template');
this.template = template ? template.content : defaultTemplate();
this.innerHTML = '<div class=\'calendar-events-pending\'></div>';
var url = 'https://www.googleapis.com/calendar/v3/calendars/' + this.calendarId + '/events?key=' + this.apiKey;
fetch(url).then(function (response) {
this.templates = this.readTemplates();
this.showTemplate('pending');
var url = `https://www.googleapis.com/calendar/v3/calendars/${ this.calendarId }/events?key=${ this.apiKey }`;
this.promise = fetch(url).then(function (response) {
return response.json();

@@ -192,26 +263,34 @@ }).then(getSortedEvents).then(function (sortedEvents) {

}.bind(this)).then(getPastAndFutureEvents).then(this.showEvents.bind(this)).catch(function (err) {
this.innerHTML = '<div class=\'calendar-events-rejected\'>' + '<p>Sorry, events can\'t load right now.</p>' + '</div>';
this.showTemplate('rejected');
throw err;
}.bind(this));
},
showEvents: function (getPastAndFutureEvents) {
if (!getPastAndFutureEvents.future.length && !getPastAndFutureEvents.past.length) {
this.innerHTML = '<div class=\'calendar-events-resolved\'>' + '<p>There are no events to show.</p>' + '</div>';
showEvents: function (pastAndFutureEvents) {
if (!pastAndFutureEvents.future.length && !pastAndFutureEvents.past.length) {
this.showTemplate('resolved');
return;
}
var events = getEvents(getPastAndFutureEvents, this.eventCount);
var elements = events.map(function (event) {
var container = this.template.cloneNode(true);
container.firstElementChild.classList.add('calendar-events-event');
const events = getEvents(pastAndFutureEvents, this.eventCount);
const eventTemplate = this.templates.event;
const findTerms = selectAllIncludeSelf(eventTemplate, '[data-find]').map(finder => finder.getAttribute('data-find').trim().replace(/\s+/g, '|')).join('|');
const dataFindRegEx = findTerms && new RegExp(`\\b(?:${ findTerms })\\b`, 'i');
const elements = events.map(event => {
var container = eventTemplate.cloneNode(true);
var metaData = eventDescriptionHTMLGroupAndUrl(event);
container.querySelectorAll('a.event-url').forEach(function (a) {
selectAllIncludeSelf(container, 'a.event-url').forEach(function (a) {
a.href = metaData.url;
});
selectAllIncludeSelf(container, '.event-all-day').forEach(function (el) {
el.setAttribute('data-all-day', !event.start.dateTime);
});
setTextContent(container, '.event-title', event.summary);
setTextContent(container, '.event-group', metaData.group);
setTextContent(container, '.event-date', eventDate(event));
setHtmlContent(container, '.event-location', linkify(event.location || event.hangoutLink));
setDateContent(container, '.event-date', event.start);
const locatable = event.location || event.hangoutLink;
locatable && setHtmlContent(container, '.event-location', linkify(locatable));
setHtmlContent(container, '.event-body', metaData.descriptionHTML);
findTerms && dataFindThenCutOrCopy(container, dataFindRegEx);
return container;
}.bind(this));
});
this.innerHTML = '';
elements.forEach(function (element) {

@@ -218,0 +297,0 @@ this.appendChild(element);

@@ -1,2 +0,2 @@

/*@bitovi/calendar-events-component@0.0.10#calendar-events*/
/*@bitovi/calendar-events-component@0.1.0#calendar-events*/
function safeCustomElement(tag, constructor, prototype) {

@@ -26,10 +26,2 @@ prototype = prototype || constructor.prototype;

safeCustomElement.supported = typeof Reflect !== 'undefined' && typeof HTMLElement !== undefined && typeof customElements !== 'undefined';
function todayDate() {
var date = new Date();
date.setHours(0);
date.setMinutes(0);
date.setMilliseconds(0);
date.setMilliseconds(0);
return date;
}
function getSortedEvents(eventsData) {

@@ -49,3 +41,3 @@ return eventsData.items.filter(function (event) {

function getFirstEventIndexFromToday(sortedEvents) {
var today = todayDate();
var today = new Date().setHours(0, 0, 0, 0);
return sortedEvents.findIndex(function (event) {

@@ -75,7 +67,9 @@ return event.start.time > today;

function getEvents(pastAndFutureEvents, count) {
var futureEvents = pastAndFutureEvents.future.length, pastEventsNeeded = count - futureEvents, past = pastAndFutureEvents.past;
if (pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded).concat(pastAndFutureEvents.future);
var futureEvents = pastAndFutureEvents.future, pastEventsNeeded = count - futureEvents.length, past = pastAndFutureEvents.past;
if (pastEventsNeeded >= past.length) {
return past.concat(futureEvents);
} else if (pastEventsNeeded > 0) {
return past.slice(past.length - pastEventsNeeded).concat(futureEvents);
} else {
return pastAndFutureEvents.future.slice(0, count);
return futureEvents.slice(0, count);
}

@@ -85,10 +79,2 @@ }

var description = (event.description || '').trim();
var lines, last;
var isHTML = description.includes('<br>');
if (isHTML) {
lines = description.split(/<br\/?>/);
} else {
lines = description.split(/\r?\n/);
}
last = lines.pop() || '';
var url = event.htmlLink;

@@ -101,26 +87,53 @@ return {

}
function eventDate(event) {
var startDate = event.start.date;
var startDateTime = event.start.dateTime;
var date;
if (startDateTime) {
date = new Date(startDateTime);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
});
} else if (startDate) {
date = new Date(startDate);
return date.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric'
});
const defaultDateValuesWhenOnlyKeySpecified = {
dateStyle: 'full',
timeStyle: 'short',
calendar: 'gregory',
dayPeriod: 'short',
numberingSystem: 'latn',
hour12: true,
hourCycle: 'h12',
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
};
function eventDate(eventStart, localesStr, optionsStr) {
const date = new Date(eventStart.dateTime || eventStart.date);
const locales = localesStr && localesStr.replace(/[^a-z0-9- ]/gi, '').split(' ');
const options = optionsStr ? {} : eventStart.dateTime ? {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
} : {
month: 'short',
day: 'numeric',
year: 'numeric'
};
optionsStr && optionsStr.replace(/\b([a-z0-9]+)(?::([a-z0-9-_/]+))?(\s|$)/gi, (_, key, val) => options[key] = val || defaultDateValuesWhenOnlyKeySpecified[key]);
if (options.hour12) {
options.hour12 = options.hour12 !== 'false';
}
return date.toLocaleString(locales || undefined, options);
}
function selectAllIncludeSelf(container, query) {
const qsa = [...container.querySelectorAll(query)];
if (container.matches && container.matches(query)) {
qsa.push(container);
}
return qsa;
}
function setDateContent(container, query, value) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = eventDate(value, el.getAttribute('data-locales'), el.getAttribute('data-options'));
});
}
function setTextContent(container, query, value) {
container.querySelectorAll(query).forEach(function (el) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.textContent = value;

@@ -130,6 +143,35 @@ });

function setHtmlContent(container, query, value) {
container.querySelectorAll(query).forEach(function (el) {
selectAllIncludeSelf(container, query).forEach(function (el) {
el.innerHTML = value;
});
}
function dataFindThenCutOrCopy(container, dataFindRegEx) {
selectAllIncludeSelf(container, 'a.event-body, .event-body a').forEach(function (link) {
const found = link.textContent.match(dataFindRegEx);
if (found) {
selectAllIncludeSelf(container, `a[data-find~="${ found[0] }" i]:not([data-found])`).forEach(function (finder) {
finder.href = link.href;
finder.textContent = finder.textContent || link.textContent.trim();
if (finder.hasAttribute('data-cut')) {
link.remove();
}
finder.setAttribute('data-found', found[0]);
});
selectAllIncludeSelf(container, `img[data-find~="${ found[0] }" i]:not([data-found])`).forEach(function (finder) {
finder.src = link.href;
finder.alt = link.textContent.trim();
if (finder.hasAttribute('data-cut')) {
link.remove();
}
finder.setAttribute('data-found', found[0]);
});
}
});
selectAllIncludeSelf(container, `[data-find][data-found]`).forEach(function (finder) {
finder.removeAttribute('data-found');
});
selectAllIncludeSelf(container, 'a:not([href]), a[href=\'\'], img:not([src]), img[src=\'\']').forEach(function (lostNotFound) {
lostNotFound.remove();
});
}
function shorten(text) {

@@ -154,9 +196,2 @@ if (text.length > 30) {

}
function defaultTemplate() {
var container = document.createElement('div');
container.innerHTML = '<div class=\'event-header\'>' + '<div class=\'event-summary\'><a class=\'event-url event-title\'></a></div>' + '<div class=\'event-group\'></div>' + '<div class=\'event-date\'></div>' + '<div class=\'event-location\'></div>' + '<div class=\'event-body\'></div>' + '</div>' + '<div class=\'event-footer\'><a class=\'event-url\'>View Event</a></div>';
var frag = document.createDocumentFragment();
frag.appendChild(container);
return frag;
}
module.exports = safeCustomElement('calendar-events', function () {

@@ -176,8 +211,44 @@ }, {

},
readTemplates() {
const defaultHTML = {
pending: '<div class=\'calendar-events-pending\'></div>',
rejected: '<div class=\'calendar-events-rejected\'><p>Sorry, events can\'t load right now.</p></div>',
resolved: '<div class=\'calendar-events-resolved\'><p>There are no events to show.</p></div>',
event: `
<div class="calendar-events-event">
<div class="event-header">
<div class="event-summary"><a class="event-url event-title"></a></div>
<div class="event-group"></div>
<div class="event-date"></div>
<div class="event-location"></div>
<div class="event-body"></div>
</div>
<div class="event-footer"><a class="event-url">View Event</a></div>
</div>
`
};
const templateChild = this.querySelector('template');
const selectorPrefix = '.calendar-events-';
const allParts = Object.keys(defaultHTML);
const templates = {};
allParts.forEach(part => {
const customizedPart = templateChild && templateChild.content.querySelector(`${ selectorPrefix }${ part }`);
templates[part] = customizedPart || Object.assign(document.createElement('div'), { innerHTML: defaultHTML[part] }).firstElementChild;
});
const anyPartSelector = allParts.map(part => `${ selectorPrefix }${ part }`).join(', ');
const anyPartCustomized = templateChild && templateChild.content.querySelector(anyPartSelector);
if (templateChild && !anyPartCustomized) {
templates.event = templateChild.content;
}
return templates;
},
showTemplate(part) {
this.innerHTML = '';
this.appendChild(this.templates[part].cloneNode(true));
},
connectedCallback: function () {
var template = this.querySelector('template');
this.template = template ? template.content : defaultTemplate();
this.innerHTML = '<div class=\'calendar-events-pending\'></div>';
var url = 'https://www.googleapis.com/calendar/v3/calendars/' + this.calendarId + '/events?key=' + this.apiKey;
fetch(url).then(function (response) {
this.templates = this.readTemplates();
this.showTemplate('pending');
var url = `https://www.googleapis.com/calendar/v3/calendars/${ this.calendarId }/events?key=${ this.apiKey }`;
this.promise = fetch(url).then(function (response) {
return response.json();

@@ -191,26 +262,34 @@ }).then(getSortedEvents).then(function (sortedEvents) {

}.bind(this)).then(getPastAndFutureEvents).then(this.showEvents.bind(this)).catch(function (err) {
this.innerHTML = '<div class=\'calendar-events-rejected\'>' + '<p>Sorry, events can\'t load right now.</p>' + '</div>';
this.showTemplate('rejected');
throw err;
}.bind(this));
},
showEvents: function (getPastAndFutureEvents) {
if (!getPastAndFutureEvents.future.length && !getPastAndFutureEvents.past.length) {
this.innerHTML = '<div class=\'calendar-events-resolved\'>' + '<p>There are no events to show.</p>' + '</div>';
showEvents: function (pastAndFutureEvents) {
if (!pastAndFutureEvents.future.length && !pastAndFutureEvents.past.length) {
this.showTemplate('resolved');
return;
}
var events = getEvents(getPastAndFutureEvents, this.eventCount);
var elements = events.map(function (event) {
var container = this.template.cloneNode(true);
container.firstElementChild.classList.add('calendar-events-event');
const events = getEvents(pastAndFutureEvents, this.eventCount);
const eventTemplate = this.templates.event;
const findTerms = selectAllIncludeSelf(eventTemplate, '[data-find]').map(finder => finder.getAttribute('data-find').trim().replace(/\s+/g, '|')).join('|');
const dataFindRegEx = findTerms && new RegExp(`\\b(?:${ findTerms })\\b`, 'i');
const elements = events.map(event => {
var container = eventTemplate.cloneNode(true);
var metaData = eventDescriptionHTMLGroupAndUrl(event);
container.querySelectorAll('a.event-url').forEach(function (a) {
selectAllIncludeSelf(container, 'a.event-url').forEach(function (a) {
a.href = metaData.url;
});
selectAllIncludeSelf(container, '.event-all-day').forEach(function (el) {
el.setAttribute('data-all-day', !event.start.dateTime);
});
setTextContent(container, '.event-title', event.summary);
setTextContent(container, '.event-group', metaData.group);
setTextContent(container, '.event-date', eventDate(event));
setHtmlContent(container, '.event-location', linkify(event.location || event.hangoutLink));
setDateContent(container, '.event-date', event.start);
const locatable = event.location || event.hangoutLink;
locatable && setHtmlContent(container, '.event-location', linkify(locatable));
setHtmlContent(container, '.event-body', metaData.descriptionHTML);
findTerms && dataFindThenCutOrCopy(container, dataFindRegEx);
return container;
}.bind(this));
});
this.innerHTML = '';
elements.forEach(function (element) {

@@ -217,0 +296,0 @@ this.appendChild(element);

{
"name": "@bitovi/calendar-events-component",
"version": "0.0.11",
"version": "0.1.0",
"description": "custom element that shows a google calendar",

@@ -18,3 +18,3 @@ "homepage": "http://bitovi.com",

"version": "git commit -am \"Update version number\" && git checkout -b release && git add -f dist/",
"postpublish": "git push --tags && git checkout master && git branch -D release && git push",
"postpublish": "git push --tags && git checkout main && git branch -D release && git push",
"testee": "testee test.html --browsers firefox",

@@ -45,2 +45,2 @@ "test": "npm run jshint && npm run testee",

"license": "MIT"
}
}

@@ -6,10 +6,12 @@ ## @bitovi/calendar-events

Use it like:
Import the `calendar-events.js` file from `/dist/amd/`, `/dist/cjs/`, or `/dist/global/`
Use it by adding the custom element to your page:
```html
<calendar-events
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
api-key="AIzaSyBsNpdGbkTsqn1BCSPQrjO9OaMySjK5Sns"
calendar-id="jupiterjs.com_g27vck36nifbnqrgkctkoanqb4@group.calendar.google.com"
event-count="3"
show-recurring
></calendar-events>

@@ -43,15 +45,17 @@ ```

The default html output looks like the following:
The default html output for an event looks like the following:
```html
<calendar-events>
<div class='event-header'>
<div class='event-summary'><a class='event-url event-title'></a></div>
<div class='event-group'></div>
<div class='event-date'></div>
<div class='event-location'></div>
<div class='event-body'></div>
</div>
<div class='event-footer'><a class='event-url'>View Event</a></div>
...
<div class="calendar-events-event">
<div class='event-header'>
<div class='event-summary'><a class='event-url event-title'></a></div>
<div class='event-group'></div>
<div class='event-date'></div>
<div class='event-location'></div>
<div class='event-body'></div>
</div>
<div class='event-footer'><a class='event-url'>View Event</a></div>
</div>
...
</calendar-events>

@@ -64,9 +68,237 @@ ```

<calendar-events>
<template>
<a class='event-url'>
<h1 class='event-title'></h1>
<p class='event-body'></p>
</a>
</template>
<template>
<a class='event-url'>
<h1 class='event-title'></h1>
<p class='event-body'></p>
</a>
</template>
</calendar-events>
```
If you wrap your custom event template in a container with class `calendar-events-event`, you can specify custom templates for other states as well:
```html
<calendar-events>
<template>
<div class="calendar-events-event">
<a class='event-url'>
<h1 class='event-title'></h1>
<p class='event-body'></p>
</a>
</div>
<div class='calendar-events-pending'>Appears when the calendar API is fetching</div>
<div class='calendar-events-rejected'><p>Appears when an error occured.</p></div>
<div class='calendar-events-resolved'><p>Appears when there are no events to display.</p></div>
</template>
</calendar-events>
```
Within the event template (`.calendar-events-event`), the following classes will allow the element to be updated with corresponding information about the event. The textContent of every tag matching the class is replaced.
`.event-title` - The title of the event (`event.summary` from the API response)
`.event-group` - The name of the Calendar the event was created under (`event.organizer.displayName`)
`.event-location` - The location field or hangout link, urls become links. (`event.location || event.hangoutLink`)
`.event-body` - The content of the description field. Linebreaks in plaintext converted into br tags. (`event.description`)
`a.event-url` - Updates the href (must be an `a` tag in your template) as a link to the event itself. (`event.htmlLink`)
`.event-all-day` - adds a `data-all-day` attribute to the element with a value of true or false for styling purposes like hiding the time.
`.event-date` - The event's start date and time. (`event.start.dateTime || event.start.date`)
### .event-date locales and options
The replaced value of `.event-date` uses [date.toLocaleString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString). The `locales` and `options` parameters are best documented under the [Intl.DateTimeFormat() constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat).
By default, if the calendar start date does not include a time ("All Day" is checked) then the date shown in `.event-date` elements will be the result of calling `.toLocaleString(undefined, { month: "short", day: "numeric", year: "numeric" })`
In the US, that might produce the string `Apr 7, 2023`. In Japan, it may return `2023年4月7日`. Order of the options does not matter.
By default, if the calendar start date also includes a time, the default options are:
```
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit"
```
which may produce `Apr 7, 2023, 9:21 PM` in the US or `2023年4月7日 21:21` in Japan.
#### data-locales
To set a specific locale, pass a space separated list of locale tags like so:
`<span class="event-date" data-locales="en-US"></span>`
or
`<span class="event-date" data-locales="iu-Cans-CA es-PR"></span>`
the first tag in the list that's suported by the browser will be chosen.
#### data-options
Browsers do not support all combinations of [the possible options options](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#datestyle) but many work as expected.
You can use the `data-options` attribute to customize the output.
Options in the mdn documentation linked above can be placed in the attribute in a space separated list to include them in the output.
```html
<span class="event-date" data-locales="en-US" data-options="month day weekday"></span>
<span class="event-date" data-locales="en-US" data-options="weekday"></span>
<span class="event-date" data-locales="en-US" data-options="year"></span>
<span class="event-date" data-locales="en-US" data-options="month"></span>
<span class="event-date" data-locales="en-US" data-options="day"></span>
<span class="event-date" data-locales="en-US" data-options="hour"></span>
<span class="event-date" data-locales="en-US" data-options="hour hourCycle:h24"></span>
<span class="event-date" data-locales="en-US" data-options="hour hour12:false"></span>
```
These are the most common combinations:
```
data-options="weekday year month day hour minute second"
data-options="weekday year month day"
data-options="year month day"
data-options="year month"
data-options="month day"
data-options="hour minute second"
data-options="hour minute"
```
Defaults are used for each option. To choose specifc variants of an option, add a colon immediately after the option property and its desired value imediately after that.
```html
<span class="event-date" data-options="month:short day:2-digit weekday:short"></span>
<span class="event-date" data-options="hour hourCycle:h24"></span>
<span class="event-date" data-options="hour hour12:false"></span>
<span class="event-date" data-options="weekday:long"></span>
<span class="event-date" data-options="weekday:short"></span>
<span class="event-date" data-options="weekday:narrow"></span>
<span class="event-date" data-options="year:numeric"></span>
<span class="event-date" data-options="year:2-digit"></span>
<span class="event-date" data-options="month:numeric"></span>
<span class="event-date" data-options="month:2-digit"></span>
<span class="event-date" data-options="month:long"></span>
<span class="event-date" data-options="month:short"></span>
<span class="event-date" data-options="month:narrow"></span>
<span class="event-date" data-options="day:numeric"></span>
<span class="event-date" data-options="day:2-digit"></span>
```
### data-find
Copy text and urls from links in the event's description (.event-body) into other parts of the template.
Given this event description:
QA Con 2023 [register here](https://bitovi.com) if you wish.
[Check out Andrew's meme of the day here](https://i.imgur.com/i7eZZ5X.jpg).
the following event template
```html
<calendar-events>
<template>
<div class="calendar-events-event">
<a data-find="register"></a>
<img data-find="meme">
<p class='event-body'></p>
<a data-find="register">Register Here!</a>
</div>
</template>
</calendar-events>
```
would produce:
```html
<div class="calendar-events-event">
<a data-find="register" href="https://bitovi.com">register here</a>
<img data-find="meme" src="https://i.imgur.com/i7eZZ5X.jpg" alt="Check out Andrew's meme of the day here">
<p class='event-body'>
QA Con 2023 <a href="https://bitovi.com">register here</a> if you wish.<br>
<a href="https://i.imgur.com/i7eZZ5X.jpg">Check out Andrew's meme of the day here</a>.
</p>
<a data-find="register" href="https://bitovi.com">Register Here!</a>
</div>
```
if you want the copied data to be removed from the event-body, add `data-cut` flag to the `data-find` elements:
```html
<calendar-events>
<template>
<div class="calendar-events-event">
<a data-find="register" data-cut></a>
<img data-find="meme" data-cut>
<p class='event-body'></p>
<a data-find="register">Register Here!</a>
</div>
</template>
</calendar-events>
```
becomes:
```html
<div class="calendar-events-event">
<a data-find="register" href="https://bitovi.com">register here</a>
<img data-find="meme" src="https://i.imgur.com/i7eZZ5X.jpg" alt="Check out Andrew's meme of the day here">
<p class='event-body'>
QA Con 2023 if you wish.<br>
.
</p>
<a data-find="register" href="https://bitovi.com">Register Here!</a>
</div>
```
Coordinate with your marketing team on what keywords your templates can expect to find in the link text within your event descriptions.
Using either above template, if the event description was instead:
"2077 DLC marketing campaign starts in June!"
the result would be:
```html
<div class="calendar-events-event">
<p class='event-body'>
2077 DLC marketing campaign starts in June!
</p>
</div>
```
(any links without an `href` or images without a `src` are removed from the output)
You can specify default `href` / `src` attributes in the template, `data-find` will override them if found, otherwise the defaults will remain in the final output.
If a link in the template uses the `.event-url` class AND `data-find`, the event url will act as a default href value and only be overwritten if data-find matched. If not matched, the link will remain in the output with the event-url.
Default textContent of a link will NOT be overwritten by `data-find`.
Default alt text of an image WILL be overwritten by `data-find`.
Finally, if your marketing team wants to be more flexible with their link text in the event descriptions, you can specify multiple find terms in your template with a space-separated-list of terms:
```html
...
<a data-find="register registration"></a>
...
```
There should not be more than one link in the event's description whose text contains a `data-find` query term (case insensitive), but if there is, only the first one is used.

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc