activity-grid
Version:
A customizable activity grid component similar to GitHub's contribution graph
488 lines (474 loc) • 15.1 kB
JavaScript
function A(t) {
return (e) => {
customElements.define(t, e);
};
}
function h(t = { type: String }) {
return function(e, s) {
const r = e.constructor;
r.observedAttributes || (r.observedAttributes = []);
const a = t.attribute || s.toLowerCase();
r.observedAttributes.includes(a) || r.observedAttributes.push(a);
const i = Symbol(s);
Object.defineProperty(e, s, {
get() {
return this[i];
},
set(n) {
const p = this[i];
t.type === Boolean ? this[i] = n === "" || n === "true" || n === !0 : this[i] = n, this.requestUpdate && this.requestUpdate(s, p, this[i]);
},
enumerable: !0,
configurable: !0
});
};
}
function $() {
return function(t, e) {
const s = Symbol(e);
Object.defineProperty(t, e, {
get() {
return this[s];
},
set(r) {
const a = this[s];
this[s] = r, this.requestUpdate && this.requestUpdate(e, a, r);
},
enumerable: !0,
configurable: !0
});
};
}
function L(t) {
if (!t || typeof t != "object" || !("date" in t) || !("count" in t))
return !1;
const e = t, s = new Date(e.date);
return !(isNaN(s.getTime()) || typeof e.count != "number" || e.count < 0);
}
const g = {
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"]
}, G = (t) => t in g && t !== "default", F = `
<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>
`, E = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], C = {
mondayStart: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
sundayStart: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
}, y = {
light: "#ebedf0",
dark: "#161b22"
}, k = (t) => `${t.getFullYear()}-${(t.getMonth() + 1).toString().padStart(2, "0")}-${t.getDate().toString().padStart(2, "0")}`;
class U {
createMonthLabels(e, s) {
const r = [], a = new Date(e);
for (a.setDate(1); a <= s; )
r.push(E[a.getMonth()]), a.setMonth(a.getMonth() + 1);
return `
<div class="months-container">
${r.map((i) => `<span>${i}</span>`).join("")}
</div>
`;
}
createWeekLabels(e, s = [0, 1, 2, 3, 4, 5, 6]) {
return `
<div class="weekdays">
${(e ? C.mondayStart : C.sundayStart).map((a, i) => `<div>${s.includes(i) ? a : ""}</div>`).join(`
`)}
</div>
`;
}
getWeeksBetweenDates(e, s) {
const a = Math.abs(s.getTime() - e.getTime());
return Math.ceil(a / 6048e5);
}
defaultTitleFormatter(e, s, r) {
const a = s === 1 ? "activity" : "activities";
return `${e.toDateString()}: ${s} ${a}`;
}
createGridCells(e, s, r, a, i, n, p, f) {
let u = "";
const D = n ? 5 : 7, m = new Date(r), M = new Date(s);
M.setHours(0, 0, 0, 0);
let W = n ? 5 - r.getDay() : 7 - r.getDay();
p && (W += 1), m.setDate(m.getDate() + W);
const S = new Date(s), T = p ? (s.getDay() || 7) - 1 : s.getDay();
T !== 0 && S.setDate(s.getDate() - T);
const O = this.getWeeksBetweenDates(m, s);
for (let b = 0; b < D; b++) {
const x = b > 4;
if (!(n && x))
for (let _ = 0; _ < O; _++) {
const d = new Date(S);
d.setDate(d.getDate() + b + _ * 7), d.setHours(0, 0, 0, 0);
const v = k(d), c = e[v];
if (d < M)
u += `
<div class="cell"
style="background-color: transparent">
</div>`;
else if (d <= r)
if (c) {
const w = f ? f(d, c.count, c.id) : this.defaultTitleFormatter(d, c.count, c.id);
u += `
<div class="cell"
style="background-color: ${a[c.level] || i}"
title="${w}"
data-date="${v}"
data-count="${c.count}"
${c.id ? `cell-id="${c.id}"` : ""}>
</div>`;
} else {
const w = f ? f(d, 0) : this.defaultTitleFormatter(d, 0);
u += `
<div class="cell"
style="background-color: ${i}"
title="${w}"
data-date="${v}"
data-count="0">
</div>`;
}
else d <= m && (u += `
<div class="cell"
style="background-color: transparent">
</div>`);
}
}
return u;
}
render(e, 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 p = this.getWeeksBetweenDates(i, s), f = this.createMonthLabels(s, r), u = this.createWeekLabels(
a.startWeekOnMonday,
a.startWeekOnMonday ? [0, 2, 4] : [1, 3, 5]
), D = this.createGridCells(
e,
s,
r,
a.colors,
a.emptyColor,
a.skipWeekends,
a.startWeekOnMonday,
a.titleFormatter
);
return {
html: `
<div class="container">
<div class="months">${f}</div>
<div class="grid-wrapper">
${u}
<div class="grid">
${D}
</div>
</div>
</div>
`,
numOfWeeks: p
};
}
}
function I(t) {
return new CustomEvent("cell-click", {
detail: t,
bubbles: !0,
composed: !0
});
}
var N = Object.defineProperty, B = Object.getOwnPropertyDescriptor, l = (t, e, s, r) => {
for (var a = r > 1 ? void 0 : r ? B(e, s) : e, i = t.length - 1, n; i >= 0; i--)
(n = t[i]) && (a = (r ? n(e, s, a) : n(a)) || a);
return r && a && N(e, s, a), a;
};
let o = class extends HTMLElement {
constructor() {
super(), this.gridRenderer = new U(), this._data = [], this._colors = g.default, this._colorTheme = null, this._darkMode = !1, this._emptyColor = this._darkMode ? y.dark : y.light, this._skipWeekends = !1, this._startWeekOnMonday = !1, this._endDate = /* @__PURE__ */ new Date(), this._titleFormatter = null, this.cells = {}, this.attachShadow({ mode: "open" }), this._startDate = null;
}
set data(t) {
this.validateActivityData(t) && (this._data = t, this.updateGrid());
}
get data() {
return this._data;
}
set colors(t) {
if (!t || !Array.isArray(t) || t.length === 0) {
console.warn("Invalid or empty colors array provided. Using default theme."), this._colors = g.default, this.updateGrid();
return;
}
const e = t.filter((s) => !this.validateColor(s));
if (e.length > 0) {
console.warn(`Invalid colors found when trying to set color theme: ${e.join(", ")}. Using default theme.`), this._colors = g.default, this.updateGrid();
return;
}
this._colorTheme || (this._colors = t, this.updateGrid());
}
get colors() {
return this._colorTheme ? g[this._colorTheme] : this._colors;
}
set colorTheme(t) {
t ? G(t) ? this._colorTheme = t : (console.warn(`Invalid color theme "${t}". Using default theme.`), this._colorTheme = null) : this._colorTheme = null, this.updateGrid();
}
get colorTheme() {
return this._colorTheme || "";
}
set darkMode(t) {
this._darkMode = t, this.emptyColor = t ? y.dark : y.light, this.updateGrid();
}
get darkMode() {
return this._darkMode;
}
set emptyColor(t) {
if (t === null || !this.validateColor(t)) {
this._emptyColor = this._darkMode ? y.dark : y.light;
return;
}
this._emptyColor = t, this.updateGrid();
}
get emptyColor() {
return this._emptyColor;
}
set skipWeekends(t) {
this._skipWeekends = t, t && (this.startWeekOnMonday = !0), this.updateGrid();
}
get skipWeekends() {
return this._skipWeekends;
}
set startWeekOnMonday(t) {
this._startWeekOnMonday = this.skipWeekends ? !0 : t, this.updateGrid();
}
get startWeekOnMonday() {
return this._startWeekOnMonday;
}
set endDate(t) {
const e = t ? new Date(t) : /* @__PURE__ */ new Date();
isNaN(e.getTime()) ? (console.warn("Invalid end-date provided. Using current date instead."), this._endDate = /* @__PURE__ */ new Date()) : this._endDate = e, this.updateGrid();
}
get endDate() {
return k(this._endDate);
}
set startDate(t) {
const e = t ? new Date(t) : null;
e && !isNaN(e.getTime()) ? e > this._endDate ? (console.warn("Start date cannot be after end date. Using one year before end date instead."), this._startDate = this.createDefaultStartDate()) : this._startDate = e : 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 ? k(this._startDate) : k(this.createDefaultStartDate());
}
// Title formatter property (not an HTML attribute, only accessible via JavaScript)
set titleFormatter(t) {
this._titleFormatter = t, this.updateGrid();
}
get titleFormatter() {
return this._titleFormatter;
}
connectedCallback() {
this._startDate || (this._startDate = this.createDefaultStartDate()), this.updateGrid();
}
attributeChangedCallback(t, e, s) {
const r = t.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 ${t}:`, 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 t = new Date(this._endDate);
t.setFullYear(t.getFullYear() - 1);
const e = this.startWeekOnMonday ? (t.getDay() || 7) - 1 : t.getDay();
return e !== 0 && t.setDate(t.getDate() - e), t;
}
generateGridCells() {
const t = {};
return this.data.forEach((e) => {
t[e.date] = {
date: new Date(e.date),
count: e.count,
level: this.calculateLevel(e.count),
ignore: !1,
id: e.id
};
}), t;
}
validateActivityData(t) {
if (!t || !Array.isArray(t))
return console.warn("Invalid activity data: must be an array. Using empty array instead."), this._data = [], !1;
const e = t.filter((s) => !L(s));
return e.length > 0 && console.warn(
"Invalid items found in activity data. They will be filtered out:",
e
), !0;
}
validateColor(t) {
return CSS.supports("color", t);
}
calculateLevel(t) {
if (t === 0) return 0;
const e = this.colors.length - 1, s = Math.max(...this.data.map((r) => r.count));
return Math.ceil(t / s * e);
}
updateGrid() {
if (!this.shadowRoot) return;
this.cells = this.generateGridCells(), this._startDate || (this._startDate = this.createDefaultStartDate());
const { html: t, numOfWeeks: e } = this.gridRenderer.render(
this.cells,
this._startDate,
this._endDate,
{
colors: this.colors,
emptyColor: this.emptyColor,
skipWeekends: this.skipWeekends,
startWeekOnMonday: this.startWeekOnMonday,
titleFormatter: this.titleFormatter
}
);
this.style.setProperty("--grid-columns", e.toString()), this.shadowRoot.innerHTML = `${F}${t}`, this.attachEventListeners();
}
attachEventListeners() {
if (!this.shadowRoot) return;
this.shadowRoot.querySelectorAll(".cell[data-date]").forEach((e) => {
e.addEventListener("click", (s) => {
const r = e.getAttribute("data-date"), a = parseInt(e.getAttribute("data-count") || "0", 10), i = e.getAttribute("cell-id") || void 0;
r && this.dispatchEvent(I({ date: r, count: a, id: i }));
});
});
}
// #endregion
};
l([
h({ type: Array })
], o.prototype, "data", 1);
l([
h({ type: Array })
], o.prototype, "colors", 1);
l([
h({ type: String, attribute: "color-theme" })
], o.prototype, "colorTheme", 1);
l([
h({ type: Boolean, attribute: "dark-mode" })
], o.prototype, "darkMode", 1);
l([
h({ type: String, attribute: "empty-color" })
], o.prototype, "emptyColor", 1);
l([
h({ type: Boolean })
], o.prototype, "skipWeekends", 1);
l([
h({ type: Boolean })
], o.prototype, "startWeekOnMonday", 1);
l([
h({ type: String, attribute: "end-date" })
], o.prototype, "endDate", 1);
l([
h({ type: String, attribute: "start-date" })
], o.prototype, "startDate", 1);
l([
$()
], o.prototype, "cells", 2);
o = l([
A("activity-grid")
], o);
export {
o as ActivityGrid
};
//# sourceMappingURL=activity-grid.es.js.map