activity-grid
Version:
A customizable activity grid component similar to GitHub's contribution graph
126 lines (112 loc) • 12.1 kB
JavaScript
(function(o,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(o=typeof globalThis<"u"?globalThis:o||self,g(o.ActivityGrid={}))})(this,function(o){"use strict";function g(i){return t=>{customElements.define(i,t)}}function l(i={type:String}){return function(t,e){const s=t.constructor;s.observedAttributes||(s.observedAttributes=[]);const a=i.attribute||e.toLowerCase();s.observedAttributes.includes(a)||s.observedAttributes.push(a);const r=Symbol(e);Object.defineProperty(t,e,{get(){return this[r]},set(n){const f=this[r];i.type===Boolean?this[r]=n===""||n==="true"||n===!0:this[r]=n,this.requestUpdate&&this.requestUpdate(e,f,this[r])},enumerable:!0,configurable:!0})}}function G(){return function(i,t){const e=Symbol(t);Object.defineProperty(i,t,{get(){return this[e]},set(s){const a=this[e];this[e]=s,this.requestUpdate&&this.requestUpdate(t,a,s)},enumerable:!0,configurable:!0})}}function O(i){if(!i||typeof i!="object"||!("date"in i)||!("count"in i))return!1;const t=i,e=new Date(t.date);return!(isNaN(e.getTime())||typeof t.count!="number"||t.count<0)}const m={default:["#ebedf0","#9be9a8","#40c463","#30a14e","#216e39"],green:["#ebedf0","#9be9a8","#40c463","#30a14e","#216e39"],red:["#ebedf0","#ffcdd2","#ef5350","#e53935","#b71c1c"],blue:["#ebedf0","#bbdefb","#64b5f6","#1e88e5","#0d47a1"],yellow:["#ebedf0","#fff9c4","#ffee58","#fdd835","#f57f17"],purple:["#ebedf0","#e1bee7","#ab47bc","#8e24aa","#4a148c"]},T=i=>i in m&&i!=="default",C=`
<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>
`,$=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],w={mondayStart:["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],sundayStart:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},u={light:"#ebedf0",dark:"#161b22"},b=i=>`${i.getFullYear()}-${(i.getMonth()+1).toString().padStart(2,"0")}-${i.getDate().toString().padStart(2,"0")}`;class L{createMonthLabels(t,e){const s=[],a=new Date(t);for(a.setDate(1);a<=e;)s.push($[a.getMonth()]),a.setMonth(a.getMonth()+1);return`
<div class="months-container">
${s.map(r=>`<span>${r}</span>`).join("")}
</div>
`}createWeekLabels(t,e=[0,1,2,3,4,5,6]){return`
<div class="weekdays">
${(t?w.mondayStart:w.sundayStart).map((a,r)=>`<div>${e.includes(r)?a:""}</div>`).join(`
`)}
</div>
`}getWeeksBetweenDates(t,e){const a=Math.abs(e.getTime()-t.getTime());return Math.ceil(a/6048e5)}createGridCells(t,e,s,a,r,n,f){let h="";const k=n?5:7,p=new Date(s),M=new Date(e);M.setHours(0,0,0,0);let A=n?5-s.getDay():7-s.getDay();f&&(A+=1),p.setDate(p.getDate()+A);const S=new Date(e),W=f?(e.getDay()||7)-1:e.getDay();W!==0&&S.setDate(e.getDate()-W);const I=this.getWeeksBetweenDates(p,e);for(let D=0;D<k;D++){const N=D>4;if(!(n&&N))for(let v=0;v<I;v++){const c=new Date(S);c.setDate(c.getDate()+D+v*7),c.setHours(0,0,0,0);const _=b(c),y=t[_];c<M?h+=`
<div class="cell"
style="background-color: transparent">
</div>`:c<=s?y?h+=`
<div class="cell"
style="background-color: ${a[y.level]||r}"
title="${c.toDateString()}: ${y.count} activities"
data-date="${_}"
data-count="${y.count}"
${y.id?`cell-id="${y.id}"`:""}>
</div>`:h+=`
<div class="cell"
style="background-color: ${r}"
title="${c.toDateString()}: 0 activities"
data-date="${_}"
data-count="0">
</div>`:c<=p&&(h+=`
<div class="cell"
style="background-color: transparent">
</div>`)}}return h}render(t,e,s,a){const r=new Date(s);let n=a.skipWeekends?5-s.getDay():7-s.getDay();a.startWeekOnMonday&&(n+=1),r.setDate(r.getDate()+n);const f=this.getWeeksBetweenDates(r,e),h=this.createMonthLabels(e,s),k=this.createWeekLabels(a.startWeekOnMonday,a.startWeekOnMonday?[0,2,4]:[1,3,5]),p=this.createGridCells(t,e,s,a.colors,a.emptyColor,a.skipWeekends,a.startWeekOnMonday);return{html:`
<div class="container">
<div class="months">${h}</div>
<div class="grid-wrapper">
${k}
<div class="grid">
${p}
</div>
</div>
</div>
`,numOfWeeks:f}}}function x(i){return new CustomEvent("cell-click",{detail:i,bubbles:!0,composed:!0})}var E=Object.defineProperty,U=Object.getOwnPropertyDescriptor,d=(i,t,e,s)=>{for(var a=s>1?void 0:s?U(t,e):t,r=i.length-1,n;r>=0;r--)(n=i[r])&&(a=(s?n(t,e,a):n(a))||a);return s&&a&&E(t,e,a),a};o.ActivityGrid=class extends HTMLElement{constructor(){super(),this.gridRenderer=new L,this._data=[],this._colors=m.default,this._colorTheme=null,this._darkMode=!1,this._emptyColor=this._darkMode?u.dark:u.light,this._skipWeekends=!1,this._startWeekOnMonday=!1,this._endDate=new Date,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=m.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=m.default,this.updateGrid();return}this._colorTheme||(this._colors=t,this.updateGrid())}get colors(){return this._colorTheme?m[this._colorTheme]:this._colors}set colorTheme(t){t?T(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?u.dark:u.light,this.updateGrid()}get darkMode(){return this._darkMode}set emptyColor(t){if(t===null||!this.validateColor(t)){this._emptyColor=this._darkMode?u.dark:u.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):new Date;isNaN(e.getTime())?(console.warn("Invalid end-date provided. Using current date instead."),this._endDate=new Date):this._endDate=e,this.updateGrid()}get endDate(){return b(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?b(this._startDate):b(this.createDefaultStartDate())}connectedCallback(){this._startDate||(this._startDate=this.createDefaultStartDate()),this.updateGrid()}attributeChangedCallback(t,e,s){const a=t.replace(/-([a-z])/g,r=>r[1].toUpperCase());if(this[a]!==void 0){const r=this[a];if(typeof r=="boolean")this[a]=s!==null;else if(typeof r=="number")this[a]=Number(s);else if(Array.isArray(r))try{this[a]=JSON.parse(s||"[]")}catch(n){console.warn(`Invalid array value for ${t}:`,n)}else this[a]=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"]}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=>!O(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(a=>a.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});this.style.setProperty("--grid-columns",e.toString()),this.shadowRoot.innerHTML=`${C}${t}`,this.attachEventListeners()}attachEventListeners(){if(!this.shadowRoot)return;this.shadowRoot.querySelectorAll(".cell[data-date]").forEach(e=>{e.addEventListener("click",s=>{const a=e.getAttribute("data-date"),r=parseInt(e.getAttribute("data-count")||"0",10),n=e.getAttribute("cell-id")||void 0;a&&this.dispatchEvent(x({date:a,count:r,id:n}))})})}},d([l({type:Array})],o.ActivityGrid.prototype,"data",1),d([l({type:Array})],o.ActivityGrid.prototype,"colors",1),d([l({type:String,attribute:"color-theme"})],o.ActivityGrid.prototype,"colorTheme",1),d([l({type:Boolean,attribute:"dark-mode"})],o.ActivityGrid.prototype,"darkMode",1),d([l({type:String,attribute:"empty-color"})],o.ActivityGrid.prototype,"emptyColor",1),d([l({type:Boolean})],o.ActivityGrid.prototype,"skipWeekends",1),d([l({type:Boolean})],o.ActivityGrid.prototype,"startWeekOnMonday",1),d([l({type:String,attribute:"end-date"})],o.ActivityGrid.prototype,"endDate",1),d([l({type:String,attribute:"start-date"})],o.ActivityGrid.prototype,"startDate",1),d([G()],o.ActivityGrid.prototype,"cells",2),o.ActivityGrid=d([g("activity-grid")],o.ActivityGrid),Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})});
//# sourceMappingURL=activity-grid.umd.js.map