UNPKG

activity-grid

Version:

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

465 lines (451 loc) 14.4 kB
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