UNPKG

activity-grid

Version:

A customizable activity grid component similar to GitHub's contribution graph

488 lines (474 loc) 15.1 kB
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