activity-grid
Version:
A customizable activity grid component similar to GitHub's contribution graph
465 lines (451 loc) • 14.4 kB
JavaScript
function x(e) {
return (t) => {
customElements.define(e, t);
};
}
function c(e = { type: String }) {
return function(t, s) {
const r = t.constructor;
r.observedAttributes || (r.observedAttributes = []);
const a = e.attribute || s.toLowerCase();
r.observedAttributes.includes(a) || r.observedAttributes.push(a);
const i = Symbol(s);
Object.defineProperty(t, s, {
get() {
return this[i];
},
set(n) {
const u = this[i];
e.type === Boolean ? this[i] = n === "" || n === "true" || n === !0 : this[i] = n, this.requestUpdate && this.requestUpdate(s, u, this[i]);
},
enumerable: !0,
configurable: !0
});
};
}
function A() {
return function(e, t) {
const s = Symbol(t);
Object.defineProperty(e, t, {
get() {
return this[s];
},
set(r) {
const a = this[s];
this[s] = r, this.requestUpdate && this.requestUpdate(t, a, r);
},
enumerable: !0,
configurable: !0
});
};
}
function T(e) {
if (!e || typeof e != "object" || !("date" in e) || !("count" in e))
return !1;
const t = e, s = new Date(t.date);
return !(isNaN(s.getTime()) || typeof t.count != "number" || t.count < 0);
}
const y = {
default: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
// Current green theme
green: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
// Same as default
red: ["#ebedf0", "#ffcdd2", "#ef5350", "#e53935", "#b71c1c"],
blue: ["#ebedf0", "#bbdefb", "#64b5f6", "#1e88e5", "#0d47a1"],
yellow: ["#ebedf0", "#fff9c4", "#ffee58", "#fdd835", "#f57f17"],
purple: ["#ebedf0", "#e1bee7", "#ab47bc", "#8e24aa", "#4a148c"]
}, $ = (e) => e in y && e !== "default", L = `
<style>
:host {
display: inline-block;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
/* Dark mode styles */
:host([dark-mode]) {
color: #c9d1d9;
background-color: transparent;
}
.container {
display: inline-grid;
grid-template-rows: auto 1fr;
}
.months {
display: flex;
padding-left: 32px;
font-size: 12px;
color: #767676;
height: 20px;
}
:host([dark-mode]) .months {
color: #8b949e;
}
.months-spacer {
width: 30px;
}
.months-container {
display: flex;
justify-content: space-between;
flex: 1;
}
.months span {
padding: 0 4px;
}
.grid-wrapper {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px;
}
.weekdays {
display: grid;
grid-template-rows: repeat(7, 1fr);
gap: 2px;
text-align: right;
padding-left: 6px;
padding-right: 2px;
font-size: 12px;
color: #767676;
margin-top: -1px;
height: calc(7 * 10px + 6 * 2px);
}
:host([dark-mode]) .weekdays {
color: #8b949e;
}
.weekdays div {
height: 10px;
line-height: 10px;
}
.grid {
display: grid;
grid-template-columns: repeat(var(--grid-columns), 1fr);
grid-template-rows: repeat(7, 1fr);
gap: 2px;
}
.cell {
width: 10px;
height: 10px;
border-radius: 2px;
}
</style>
`, G = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], S = {
mondayStart: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
sundayStart: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
}, g = {
light: "#ebedf0",
dark: "#161b22"
}, D = (e) => `${e.getFullYear()}-${(e.getMonth() + 1).toString().padStart(2, "0")}-${e.getDate().toString().padStart(2, "0")}`;
class E {
createMonthLabels(t, s) {
const r = [], a = new Date(t);
for (a.setDate(1); a <= s; )
r.push(G[a.getMonth()]), a.setMonth(a.getMonth() + 1);
return `
<div class="months-container">
${r.map((i) => `<span>${i}</span>`).join("")}
</div>
`;
}
createWeekLabels(t, s = [0, 1, 2, 3, 4, 5, 6]) {
return `
<div class="weekdays">
${(t ? S.mondayStart : S.sundayStart).map((a, i) => `<div>${s.includes(i) ? a : ""}</div>`).join(`
`)}
</div>
`;
}
getWeeksBetweenDates(t, s) {
const a = Math.abs(s.getTime() - t.getTime());
return Math.ceil(a / 6048e5);
}
createGridCells(t, s, r, a, i, n, u) {
let h = "";
const b = n ? 5 : 7, p = new Date(r), v = new Date(s);
v.setHours(0, 0, 0, 0);
let w = n ? 5 - r.getDay() : 7 - r.getDay();
u && (w += 1), p.setDate(p.getDate() + w);
const M = new Date(s), W = u ? (s.getDay() || 7) - 1 : s.getDay();
W !== 0 && M.setDate(s.getDate() - W);
const C = this.getWeeksBetweenDates(p, s);
for (let m = 0; m < b; m++) {
const O = m > 4;
if (!(n && O))
for (let k = 0; k < C; k++) {
const l = new Date(M);
l.setDate(l.getDate() + m + k * 7), l.setHours(0, 0, 0, 0);
const _ = D(l), f = t[_];
l < v ? h += `
<div class="cell"
style="background-color: transparent">
</div>` : l <= r ? f ? h += `
<div class="cell"
style="background-color: ${a[f.level] || i}"
title="${l.toDateString()}: ${f.count} activities"
data-date="${_}"
data-count="${f.count}"
${f.id ? `cell-id="${f.id}"` : ""}>
</div>` : h += `
<div class="cell"
style="background-color: ${i}"
title="${l.toDateString()}: 0 activities"
data-date="${_}"
data-count="0">
</div>` : l <= p && (h += `
<div class="cell"
style="background-color: transparent">
</div>`);
}
}
return h;
}
render(t, s, r, a) {
const i = new Date(r);
let n = a.skipWeekends ? 5 - r.getDay() : 7 - r.getDay();
a.startWeekOnMonday && (n += 1), i.setDate(i.getDate() + n);
const u = this.getWeeksBetweenDates(i, s), h = this.createMonthLabels(s, r), b = this.createWeekLabels(
a.startWeekOnMonday,
a.startWeekOnMonday ? [0, 2, 4] : [1, 3, 5]
), p = this.createGridCells(
t,
s,
r,
a.colors,
a.emptyColor,
a.skipWeekends,
a.startWeekOnMonday
);
return {
html: `
<div class="container">
<div class="months">${h}</div>
<div class="grid-wrapper">
${b}
<div class="grid">
${p}
</div>
</div>
</div>
`,
numOfWeeks: u
};
}
}
function U(e) {
return new CustomEvent("cell-click", {
detail: e,
bubbles: !0,
composed: !0
});
}
var I = Object.defineProperty, N = Object.getOwnPropertyDescriptor, d = (e, t, s, r) => {
for (var a = r > 1 ? void 0 : r ? N(t, s) : t, i = e.length - 1, n; i >= 0; i--)
(n = e[i]) && (a = (r ? n(t, s, a) : n(a)) || a);
return r && a && I(t, s, a), a;
};
let o = class extends HTMLElement {
constructor() {
super(), this.gridRenderer = new E(), this._data = [], this._colors = y.default, this._colorTheme = null, this._darkMode = !1, this._emptyColor = this._darkMode ? g.dark : g.light, this._skipWeekends = !1, this._startWeekOnMonday = !1, this._endDate = /* @__PURE__ */ new Date(), this.cells = {}, this.attachShadow({ mode: "open" }), this._startDate = null;
}
set data(e) {
this.validateActivityData(e) && (this._data = e, this.updateGrid());
}
get data() {
return this._data;
}
set colors(e) {
if (!e || !Array.isArray(e) || e.length === 0) {
console.warn("Invalid or empty colors array provided. Using default theme."), this._colors = y.default, this.updateGrid();
return;
}
const t = e.filter((s) => !this.validateColor(s));
if (t.length > 0) {
console.warn(`Invalid colors found when trying to set color theme: ${t.join(", ")}. Using default theme.`), this._colors = y.default, this.updateGrid();
return;
}
this._colorTheme || (this._colors = e, this.updateGrid());
}
get colors() {
return this._colorTheme ? y[this._colorTheme] : this._colors;
}
set colorTheme(e) {
e ? $(e) ? this._colorTheme = e : (console.warn(`Invalid color theme "${e}". Using default theme.`), this._colorTheme = null) : this._colorTheme = null, this.updateGrid();
}
get colorTheme() {
return this._colorTheme || "";
}
set darkMode(e) {
this._darkMode = e, this.emptyColor = e ? g.dark : g.light, this.updateGrid();
}
get darkMode() {
return this._darkMode;
}
set emptyColor(e) {
if (e === null || !this.validateColor(e)) {
this._emptyColor = this._darkMode ? g.dark : g.light;
return;
}
this._emptyColor = e, this.updateGrid();
}
get emptyColor() {
return this._emptyColor;
}
set skipWeekends(e) {
this._skipWeekends = e, e && (this.startWeekOnMonday = !0), this.updateGrid();
}
get skipWeekends() {
return this._skipWeekends;
}
set startWeekOnMonday(e) {
this._startWeekOnMonday = this.skipWeekends ? !0 : e, this.updateGrid();
}
get startWeekOnMonday() {
return this._startWeekOnMonday;
}
set endDate(e) {
const t = e ? new Date(e) : /* @__PURE__ */ new Date();
isNaN(t.getTime()) ? (console.warn("Invalid end-date provided. Using current date instead."), this._endDate = /* @__PURE__ */ new Date()) : this._endDate = t, this.updateGrid();
}
get endDate() {
return D(this._endDate);
}
set startDate(e) {
const t = e ? new Date(e) : null;
t && !isNaN(t.getTime()) ? t > this._endDate ? (console.warn("Start date cannot be after end date. Using one year before end date instead."), this._startDate = this.createDefaultStartDate()) : this._startDate = t : this._startDate || (console.warn("Invalid start-date provided. Using one year before end date instead."), this._startDate = this.createDefaultStartDate()), this.isConnected && this.updateGrid();
}
get startDate() {
return this._startDate ? D(this._startDate) : D(this.createDefaultStartDate());
}
connectedCallback() {
this._startDate || (this._startDate = this.createDefaultStartDate()), this.updateGrid();
}
attributeChangedCallback(e, t, s) {
const r = e.replace(/-([a-z])/g, (a) => a[1].toUpperCase());
if (this[r] !== void 0) {
const a = this[r];
if (typeof a == "boolean")
this[r] = s !== null;
else if (typeof a == "number")
this[r] = Number(s);
else if (Array.isArray(a))
try {
this[r] = JSON.parse(s || "[]");
} catch (i) {
console.warn(`Invalid array value for ${e}:`, i);
}
else
this[r] = s;
}
}
static get observedAttributes() {
return [
"start-week-on-monday",
"skip-weekends",
"data",
"colors",
"color-theme",
"empty-color",
"max-level",
"end-date",
"start-date",
"dark-mode"
];
}
// #endregion
// #region Private methods
createDefaultStartDate() {
const e = new Date(this._endDate);
e.setFullYear(e.getFullYear() - 1);
const t = this.startWeekOnMonday ? (e.getDay() || 7) - 1 : e.getDay();
return t !== 0 && e.setDate(e.getDate() - t), e;
}
generateGridCells() {
const e = {};
return this.data.forEach((t) => {
e[t.date] = {
date: new Date(t.date),
count: t.count,
level: this.calculateLevel(t.count),
ignore: !1,
id: t.id
};
}), e;
}
validateActivityData(e) {
if (!e || !Array.isArray(e))
return console.warn("Invalid activity data: must be an array. Using empty array instead."), this._data = [], !1;
const t = e.filter((s) => !T(s));
return t.length > 0 && console.warn(
"Invalid items found in activity data. They will be filtered out:",
t
), !0;
}
validateColor(e) {
return CSS.supports("color", e);
}
calculateLevel(e) {
if (e === 0) return 0;
const t = this.colors.length - 1, s = Math.max(...this.data.map((r) => r.count));
return Math.ceil(e / s * t);
}
updateGrid() {
if (!this.shadowRoot) return;
this.cells = this.generateGridCells(), this._startDate || (this._startDate = this.createDefaultStartDate());
const { html: e, numOfWeeks: t } = this.gridRenderer.render(
this.cells,
this._startDate,
this._endDate,
{
colors: this.colors,
emptyColor: this.emptyColor,
skipWeekends: this.skipWeekends,
startWeekOnMonday: this.startWeekOnMonday
}
);
this.style.setProperty("--grid-columns", t.toString()), this.shadowRoot.innerHTML = `${L}${e}`, this.attachEventListeners();
}
attachEventListeners() {
if (!this.shadowRoot) return;
this.shadowRoot.querySelectorAll(".cell[data-date]").forEach((t) => {
t.addEventListener("click", (s) => {
const r = t.getAttribute("data-date"), a = parseInt(t.getAttribute("data-count") || "0", 10), i = t.getAttribute("cell-id") || void 0;
r && this.dispatchEvent(U({ date: r, count: a, id: i }));
});
});
}
// #endregion
};
d([
c({ type: Array })
], o.prototype, "data", 1);
d([
c({ type: Array })
], o.prototype, "colors", 1);
d([
c({ type: String, attribute: "color-theme" })
], o.prototype, "colorTheme", 1);
d([
c({ type: Boolean, attribute: "dark-mode" })
], o.prototype, "darkMode", 1);
d([
c({ type: String, attribute: "empty-color" })
], o.prototype, "emptyColor", 1);
d([
c({ type: Boolean })
], o.prototype, "skipWeekends", 1);
d([
c({ type: Boolean })
], o.prototype, "startWeekOnMonday", 1);
d([
c({ type: String, attribute: "end-date" })
], o.prototype, "endDate", 1);
d([
c({ type: String, attribute: "start-date" })
], o.prototype, "startDate", 1);
d([
A()
], o.prototype, "cells", 2);
o = d([
x("activity-grid")
], o);
export {
o as ActivityGrid
};
//# sourceMappingURL=activity-grid.es.js.map