BBTimeline/bbtimeline.min.js

13 lines
14 KiB
JavaScript

/**
* BBTimeline
* @version v0.1.1.277 apha (2023/11/12 0106)
*/
class BBTimeline{constructor(e){const t=this;t.Container=document.getElementById(e),t.Padding={Left:20,Top:20,Right:20,Bottom:0},t.Size={W:t.Container.innerWidth||t.Container.clientWidth,H:t.Container.innerHeight||t.Container.clientHeight},t.Layer={Background:null,Flourish:null,Markers:null},t.DateParsePattern="yyyy-MM-dd",t.HotTrack={Colour:"#F57C00",Width:3},t.Events=[],t.StartDate=t.DateToInternalString(new Date),t.ShowDate=t.StartDate,t.ShowMarkerLabel=!0,t.Enabled=!1,t.Debug=!1,t.EnableHotTracking=!0,t.initialiseComponents()}initialiseComponents(){const e=this;e.Container.innerHTML="<canvas></canvas><canvas></canvas><canvas></canvas>";const t=e.Container.getElementsByTagName("canvas");e.Layer.Background=new BBTimelineBackgroundCanvas(e,t[0]),e.Layer.Flourish=new BBTimelineFlourishCanvas(e,t[1]),e.Layer.Markers=new BBTimelineForegroundCanvas(e,t[2])}get CTX(){return a.Layer.Markers.CTX}get NewEvent(){return{Date:"",Label:"",Position:{X:0,Y:0},Events:[],HitBox:null,BorderColour:this.Layer.Markers.Options.Marker.BorderColour,BackColour:this.Layer.Markers.Options.Marker.BackColour}}get NewEventItem(){return{Title:"",Description:"",Link:"",Tag:null}}get VisibleDays(){const e=this,t=e.Size.W-(e.Padding.Left+e.Padding.Right);return Math.floor(t/(e.XAxis.NoPartPerDay*e.XAxis.HourLineSpace))}get VisibleStartDate(){return this.ConvertToDate(this.ShowDate)}get VisibleEndDate(){const e=this;let t=e.ConvertToDate(e.ShowDate);return t.setDate(t.getDate()+e.VisibleDays),t.setDate(t.getDate()-1),e.DateToString(t,e.DateParsePattern)}get VisibleEvents(){const e=this;let t=[];return e.Layer.Background.XAxisPositions.forEach((function(a){const r=e.FindEvent(a.Date);null!=r&&(r.Position.X=a.X,t.push(r))})),t}get XAxis(){return this.Layer.Background.Options.XAxis}AddEvent(e,t,a){const r=this,n=Object.assign(r.NewEventItem,a);let i=r.FindEvent(e);if(null==i){let t=r.NewEvent;t.Date=e,r.Events.push(t),i=r.FindEvent(e)}null!=t&&(i.Label=t),i.Events.push(n)}Clear(){const e=this;e.Layer.Background.Clear(),e.Layer.Flourish.Clear(),e.Layer.Markers.Clear(),e.StartDate=e.DateToInternalString(new Date),e.ShowDate=e.StartDate,e.Enabled=!1,e.Events=[]}DeleteMarker(e){const t=this;for(let a=0;a<t.Events.length;a++)t.Events[a].Date==e&&t.Events.splice(a,1)}FindDatePosition(e){const t=this.Layer.Background.XAxisPositions;for(let a=0;a<t.length;a++)if(t[a].Date==e)return t[a];return null}FindEvent(e){const t=this;for(let a=0;a<t.Events.length;a++)if(t.Events[a].Date==e)return t.Events[a];return null}FindEventsByCoords(e,t,a){const r=this;for(let n=0;n<r.Events.length;n++){const i=r.Events[n].HitBox;if(null==r.Events[n].HitBox)continue;let o=i.X+i.W,l=i.Y+i.H;if(a&&(l+=r.Layer.Markers.Options.Label.Margin.Y),e>=i.X&&e<=o&&t>=i.Y&&t<=l)return r.Events[n]}return null}Load(e){this.StartDate=e,this.Show(e)}Show(e){const t=this;t.ConvertToDate(e)<t.ConvertToDate(t.StartDate)&&(e=t.StartDate),t.ShowDate=e,t.Enabled=!0,t.Invalidate(!0,!0)}ShowNext(){const e=this;let t=e.VisibleStartDate;t.setDate(t.getDate()+(e.VisibleDays-1)),e.Show(e.DateToInternalString(t))}ShowPrevious(){const e=this;let t=e.VisibleStartDate;t.setDate(t.getDate()-(e.VisibleDays-1)),e.Show(e.DateToInternalString(t))}UpdateLabel(e,t){let a=this.FindEvent(e);null!=a&&(a.Label=t,this.Invalidate(!1,!0))}UpdateMarker(e,t,a){let r=this.FindEvent(e);null!=r&&(r.BorderColour=t,r.BackColour=a,this.Invalidate(!1,!0))}Invalidate(e,t){const a=this;e&&a.Layer.Background.Invalidate(),a.Layer.Flourish.Clear(),t&&a.Layer.Markers.Invalidate()}DateToString(e,t){let a=t;return a=a.replace("fffffff",e.getMilliseconds().toString().padStart(7,"0")),a=a.replace("ffffff",e.getMilliseconds().toString().padStart(6,"0")),a=a.replace("fffff",e.getMilliseconds().toString().padStart(5,"0")),a=a.replace("yyyy",e.getFullYear().toString().padStart(4,"0")),a=a.replace("MMMM","{1}"),a=a.replace("dddd","{2}"),a=a.replace("ffff",e.getMilliseconds().toString().padStart(4,"0")),a=a.replace("yyy",e.getFullYear().toString().padStart(3,"0")),a=a.replace("MMM","{3}"),a=a.replace("ddd","{4}"),a=a.replace("fff",e.getMilliseconds().toString().padStart(3,"0")),a=a.replace("zzz",""),a=a.replace("yy",e.getFullYear().toString().slice(-2)),a=a.replace("MM",(e.getMonth()+1).toString().padStart(2,"0")),a=a.replace("dd",e.getDate().toString().padStart(2,"0")),a=a.replace("HH",e.getHours().toString().padStart(2,"0")),a=a.replace("hh",(e.getHours()>12?e.getHours()-12:e.getHours()).toString().padStart(2,"0")),a=a.replace("mm",e.getMinutes().toString().padStart(2,"0")),a=a.replace("ss",e.getSeconds().toString().padStart(2,"0")),a=a.replace("ff",e.getMilliseconds().toString().padStart(2,"0")),a=a.replace("tt","{5}"),a=a.replace("zz",""),a=a.replace("y",e.getFullYear().toString()),a=a.replace("M",(e.getMonth()+1).toString()),a=a.replace("d",e.getDate().toString()),a=a.replace("H",e.getHours().toString()),a=a.replace("h",(e.getHours()>12?e.getHours()-12:e.getHours()).toString()),a=a.replace("m",e.getMinutes().toString()),a=a.replace("s",e.getSeconds().toString()),a=a.replace("z",""),a=a.replace("t","{6}"),a=a.replace("Z",""),a=a.replace("{1}",e.toLocaleString("default",{month:"long"})),a=a.replace("{2}",e.toLocaleString("default",{weekday:"long"})),a=a.replace("{3}",e.toLocaleString("default",{month:"short"})),a=a.replace("{4}",e.toLocaleString("default",{weekday:"short"})),a=a.replace("{5}",e.getHours()>=12?"PM":"AM"),a=a.replace("{6}",e.getHours()>=12?"P":"A"),a}DateToInternalString(e){return this.DateToString(e,this.DateParsePattern)}ConvertToDate(e){return new Date(Date.parse(e))}OnMouseDown(e,t,a){}OnMouseMove(e,t,a){}OnClick(e,t,a){}OnDblClick(e,t,a){}}
class BBTimelineCanvas{constructor(t,e){const n=this;n.Parent=t,n.Container=e,n.CTX=n.Container.getContext("2d"),n.initialiseOptions(),n.initialiseComponents()}initialiseOptions(){}initialiseComponents(){const t=this;t.Container.style.width=t.Parent.Size.W+"px",t.Container.style.height=t.Parent.Size.H+"px",t.Container.style.position="absolute",t.Container.style.border="none",t.CTX.canvas.width=t.Parent.Size.W,t.CTX.canvas.height=t.Parent.Size.H,t.Clear()}get ClientRectangle(){const t=this;return{X:t.Parent.Padding.Left,Y:t.Parent.Padding.Top,W:t.Parent.Size.W-(t.Parent.Padding.Left+t.Parent.Padding.Right),H:t.Parent.Size.H-(t.Parent.Padding.Top+t.Parent.Padding.Bottom)}}Clear(){const t=this;t.CTX.clearRect(0,0,t.CTX.canvas.width,t.CTX.canvas.height)}Invalidate(){}drawCircle(t,e,n,i,a,r){const s=this,o=2*i,X=n-o,l=s.half(n);s.CTX.beginPath(),s.CTX.arc(t,e,X,0,2*Math.PI,!1),s.CTX.fillStyle=r,s.CTX.fill(),s.CTX.lineWidth=i,s.CTX.strokeStyle=a,s.CTX.stroke();return{X:t-(l+i),Y:e-(l+i),W:n+o,H:n+o}}drawRectangle(t,e){const n=this;return n.CTX.beginPath(),n.CTX.rect(t.X,t.Y,t.W,t.H),n.CTX.lineWidth=1,n.CTX.strokeStyle=e,n.CTX.stroke(),t}drawText(t,e,n,i,a,r){const s=this;s.CTX.font=i,s.CTX.fillStyle=a;let o=s.measureText(i,n);switch(o.Y=e,r){case"center":o.X=t-o.X;break;case"right":o.X=t-o.W;break;default:o.X=t}return s.CTX.fillText(n,o.X,o.Y+o.H),o}drawVerticalLine(t,e,n,i,a){const r=this;r.CTX.beginPath(),r.CTX.moveTo(t,e),r.CTX.lineTo(t,n-i),r.CTX.lineWidth=i,r.CTX.strokeStyle=a,r.CTX.stroke();return{X:t,Y:e,W:i,H:n-e}}half(t){return t/2}measureText(t,e){const n=this;n.CTX.font=t;const i=n.CTX.measureText(e);return{W:i.width,H:i.fontBoundingBoxAscent,X:n.half(i.width),Y:n.half(i.fontBoundingBoxAscent)}}isPointInRectangle(t,e){const n=t.X+t.W,i=t.Y+t.H;return e.X>=t.X&&e.X<=n&&e.Y>=t.Y&&e.Y<=i}combineRectangle(t,e){const n=Math.max(t.X+t.W,e.X+e.W),i=Math.max(t.Y+t.H,e.Y+e.H),a={X:Math.min(t.X,e.X),Y:Math.min(t.Y,e.Y),W:0,H:0};return a.W=n-a.X,a.H=i-a.Y,a}}
class BBTimelineBackgroundCanvas extends BBTimelineCanvas{constructor(e,t){super(e,t)}initialiseOptions(){super.initialiseOptions();this.Options={Axis:{LineColour1:"#000000",LineWidth:1,Font:"8pt Arial",LabelColour:"#000000",LabelSpacing:6},XAxis:{NoPartPerDay:4,HourLineSpace:6,HourLineHeight:10,HourLineColour:"#EAEAEA",DayLineHeight:20,DayLineColour:"#9E9E9E",Position:"bottom"}}}initialiseComponents(){super.initialiseComponents();this.Invalidate()}get GraphRectangle(){const e=this;return"top"==e.Options.XAxis.Position?{X:e.ClientRectangle.X,Y:e.ClientRectangle.Y+e.XAxisHeight,W:e.ClientRectangle.W,H:e.ClientRectangle.H-e.XAxisHeight}:{X:e.ClientRectangle.X,Y:e.ClientRectangle.Y,W:e.ClientRectangle.W,H:e.ClientRectangle.H-e.XAxisHeight}}get XAxisHeight(){const e=this;return e.measureText(e.Options.Axis.Font,"0").H+e.Options.Axis.LabelSpacing+2*e.Options.XAxis.DayLineHeight}get XAxisPositions(){const e=this,t=e.GraphRectangle.X+e.GraphRectangle.W;let i=[],n=e.GraphRectangle.X,a=e.Parent.ConvertToDate(e.Parent.ShowDate);for(a.setDate(a.getDate()-1);!(n>=t);)i.push({Date:e.Parent.DateToInternalString(a),X:n}),n+=e.Options.XAxis.HourLineSpace*e.Options.XAxis.NoPartPerDay,a.setDate(a.getDate()+1);return i}Invalidate(){const e=this;e.Clear(),e.drawAxis(),e.drawXAxisTicks(),e.drawXAxisLabels(),e.Parent.Debug&&e.drawRectangle(e.ClientRectangle,"red"),e.Parent.Debug&&e.drawRectangle(e.GraphRectangle,"red")}drawAxis(){const e=this;e.CTX.beginPath(),"top"==e.Options.XAxis.Position?(e.CTX.moveTo(e.GraphRectangle.X,e.GraphRectangle.Y+e.GraphRectangle.H),e.CTX.lineTo(e.GraphRectangle.X,e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.X+e.GraphRectangle.W,e.GraphRectangle.Y)):(e.CTX.moveTo(e.GraphRectangle.X,e.GraphRectangle.Y),e.CTX.lineTo(e.GraphRectangle.X,e.GraphRectangle.Y+e.GraphRectangle.H),e.CTX.lineTo(e.GraphRectangle.X+e.GraphRectangle.W,e.GraphRectangle.Y+e.GraphRectangle.H)),e.CTX.lineWidth=e.Options.Axis.LineWidth,e.CTX.strokeStyle=e.Options.Axis.LineColour1,e.CTX.stroke()}drawXAxisLabels(){const e=this,t=e.measureText(e.Options.Axis.Font,"0");let i=0,n=0,a=0;"top"==e.Options.XAxis.Position?(i=e.GraphRectangle.Y-e.Options.Axis.LineWidth-2,n=i-(t.H+e.Options.XAxis.DayLineHeight),a=n-(t.H+e.Options.Axis.LabelSpacing)):(i=e.GraphRectangle.Y+e.GraphRectangle.H+e.Options.Axis.LineWidth,n=i+e.Options.XAxis.DayLineHeight,a=n+t.H+e.Options.Axis.LabelSpacing),e.XAxisPositions.forEach((function(t,i){const s=e.Parent.ConvertToDate(t.Date);let o=!1;0==i?s.getDate()<25&&(o=!0):1==s.getDate()&&(o=!0),e.drawText(t.X,n,e.Parent.DateToString(s,"dd"),e.Options.Axis.Font,e.Options.Axis.LabelColour,"center"),o&&e.drawText(t.X,a,e.Parent.DateToString(s,"MMMM yyyy"),e.Options.Axis.Font,e.Options.Axis.LabelColour,"left")}))}drawXAxisTicks(){const e=this;let t=e.GraphRectangle.X;const i=e.GraphRectangle.X+e.GraphRectangle.W;let n=0,a=0,s=0;"top"==e.Options.XAxis.Position?(n=e.GraphRectangle.Y-e.Options.Axis.LineWidth,a=n-e.Options.XAxis.DayLineHeight,s=n-e.Options.XAxis.HourLineHeight):(n=e.GraphRectangle.Y+e.GraphRectangle.H+e.Options.Axis.LineWidth,a=n+e.Options.XAxis.DayLineHeight,s=n+e.Options.XAxis.HourLineHeight);let o=0;for(;!(t>=i);)e.CTX.beginPath(),e.CTX.moveTo(t,n),o%e.Options.XAxis.NoPartPerDay==0?(e.CTX.lineTo(t,a),e.CTX.strokeStyle=e.Options.XAxis.DayLineColour):(e.CTX.lineTo(t,s),e.CTX.strokeStyle=e.Options.XAxis.HourLineColour),e.CTX.lineWidth=e.Options.Axis.LineWidth,e.CTX.stroke(),t+=e.Options.XAxis.HourLineSpace,o++}}
class BBTimelineFlourishCanvas extends BBTimelineCanvas{constructor(a,e){super(a,e)}initialiseOptions(){super.initialiseOptions();this.XPos=-1}Invalidate(){const a=this;if(a.Clear(),a.XPos<0)a.Clear();else{let e=0;e="top"==a.Parent.XAxis.Position?a.Parent.Layer.Background.GraphRectangle.Y:a.Parent.Layer.Background.GraphRectangle.Y+a.Parent.Layer.Background.GraphRectangle.H,a.drawCircle(a.XPos,e,a.Parent.HotTrack.Width,0,a.Parent.HotTrack.Colour,a.Parent.HotTrack.Colour)}}}
class BBTimelineForegroundCanvas extends BBTimelineCanvas{constructor(e,n){super(e,n)}initialiseOptions(){super.initialiseOptions();this.Options={Marker:{BorderColour:"#3A5D9C",BorderWidth:2,BackColour:"#D4DEEF",Width:10},Label:{Colour:"#3A5D9C",Font:"9pt Arial",Margin:{X:2,Y:10},Line:{Colour:"#A6A6A6",Width:1}}}}initialiseComponents(){super.initialiseComponents();const e=this;e.CTX.canvas.addEventListener("mousedown",(function(n){if(e.Parent.Enabled){var t=e.Parent.FindEventsByCoords(n.offsetX,n.offsetY,!1);null!=t&&(e.Parent.Debug&&console.log(t),e.Parent.OnMouseDown(this,n,t))}})),e.CTX.canvas.addEventListener("click",(function(n){if(e.Parent.Enabled){var t=e.Parent.FindEventsByCoords(n.offsetX,n.offsetY,!1);null!=t&&(e.Parent.Debug&&console.log(t),e.Parent.OnClick(this,n,t))}})),e.CTX.canvas.addEventListener("dblclick",(function(n){if(e.Parent.Enabled){var t=e.Parent.FindEventsByCoords(n.offsetX,n.offsetY,!1);null!=t&&(e.Parent.Debug&&console.log(t),e.Parent.OnDblClick(this,n,t))}})),e.Parent.EnableHotTracking&&e.CTX.canvas.addEventListener("mousemove",(function(n){if(e.Parent.Enabled){var t=e.Parent.FindEventsByCoords(n.offsetX,n.offsetY,!1);if(e.Container.style.cursor=null!=t?"pointer":"default",e.Parent.EnableHotTracking){const o={X:n.offsetX,Y:n.offsetY};e.isPointInRectangle(e.Parent.Layer.Background.GraphRectangle,o)?(e.Parent.Debug&&console.log(o),e.Parent.Layer.Flourish.XPos=o.X,e.Parent.OnMouseMove(this,n,t)):e.Parent.Layer.Flourish.XPos=-1,e.Parent.Layer.Flourish.Invalidate()}}})),e.Invalidate()}Invalidate(){const e=this,n=e.Parent.Layer.Background.GraphRectangle;e.Clear();let t=0;const o=e.Parent.VisibleEvents;t="top"==e.Parent.XAxis.Position?n.Y+e.Options.Marker.Width+20:n.Y+e.Options.Marker.Width,o.forEach((function(e,n){e.HitBox={X:0,Y:0,W:0,H:0}})),o.forEach((function(n,o){let a=e.calcMarkerPosition(n.Position.X,t),r=0;r="top"==e.Parent.XAxis.Position?e.Parent.Layer.Background.GraphRectangle.Y:e.Parent.Layer.Background.GraphRectangle.Y+e.Parent.Layer.Background.GraphRectangle.H,e.Options.Label.Line.Width>0&&e.drawVerticalLine(n.Position.X,a,r,e.Options.Label.Line.Width,e.Options.Label.Line.Colour);const i=e.drawCircle(n.Position.X,a,e.Options.Marker.Width,e.Options.Marker.BorderWidth,n.BorderColour,n.BackColour);if(n.Position={X:n.Position.X,Y:a},e.Parent.ShowMarkerLabel){const t=e.drawText(i.X+i.W+e.Options.Label.Margin.X,i.Y,n.Label,e.Options.Label.Font,e.Options.Label.Colour,"left");n.HitBox=e.combineRectangle(i,t)}else n.HitBox=i;e.Parent.Debug&&e.drawRectangle(n.HitBox,"red"),e.Parent.Debug&&console.log(n)}))}OnMouseDown(e){if(a.Parent.Enabled){var n=a.Parent.FindEventsByCoords(e.offsetX,e.offsetY,!1);null!=n&&(a.Parent.Debug&&console.log(n),a.Parent.OnMouseDown(this,e,n))}}calcMarkerPosition(e,n){const t=this,o=t.Parent.Layer.Background.GraphRectangle;let a=!1,r=n;for(let n=o.Y;n<o.Y+o.H;n++){if(r=n,null==t.Parent.FindEventsByCoords(e,r,!0)){a=!0;break}}return a||(r=n),r}}