diff --git a/dist/frappe-gantt.css b/dist/frappe-gantt.css index 452db33..5f727d9 100644 --- a/dist/frappe-gantt.css +++ b/dist/frappe-gantt.css @@ -68,12 +68,16 @@ .gantt .bar-wrapper { cursor: pointer; } .gantt .bar-wrapper:hover .bar { - stroke-width: 2; } + fill: #a9b5c1; } + .gantt .bar-wrapper:hover .bar-progress { + fill: #8a8aff; } .gantt .bar-wrapper:hover .handle { visibility: visible; opacity: 1; } .gantt .bar-wrapper.active .bar { - stroke-width: 2; } + fill: #a9b5c1; } + .gantt .bar-wrapper.active .bar-progress { + fill: #8a8aff; } .gantt .lower-text, .gantt .upper-text { font-size: 12px; diff --git a/dist/frappe-gantt.js b/dist/frappe-gantt.js index c20a887..bb1bbc9 100644 --- a/dist/frappe-gantt.js +++ b/dist/frappe-gantt.js @@ -245,7 +245,64 @@ function createSVG(tag, attrs) { return elem; } +function animateSVG(svgElement, attr, from, to) { + const animatedSvgElement = getAnimationElement(svgElement, attr, from, to); + if (animatedSvgElement === svgElement) { + // triggered 2nd time programmatically + // trigger artificial click event + const event = document.createEvent('HTMLEvents'); + event.initEvent('click', true, true); + event.eventName = 'click'; + animatedSvgElement.dispatchEvent(event); + } +} + +function getAnimationElement( + svgElement, + attr, + from, + to, + dur = '0.4s', + begin = '0.1s' +) { + const animEl = svgElement.querySelector('animate'); + if (animEl) { + $.attr(animEl, { + attributeName: attr, + from, + to, + dur, + begin: 'click + ' + begin // artificial click + }); + return svgElement; + } + + const animateElement = createSVG('animate', { + attributeName: attr, + from, + to, + dur, + begin, + calcMode: 'spline', + values: from + ';' + to, + keyTimes: '0; 1', + keySplines: cubic_bezier('ease-out') + }); + svgElement.appendChild(animateElement); + + return svgElement; +} + +function cubic_bezier(name) { + return { + ease: '.25 .1 .25 1', + linear: '0 0 1 1', + 'ease-in': '.42 0 1 1', + 'ease-out': '0 0 .58 1', + 'ease-in-out': '.42 0 .58 1' + }[name]; +} $.on = (element, event, selector, callback) => { if (!callback) { @@ -385,6 +442,8 @@ class Bar { append_to: this.bar_group }); + animateSVG(this.$bar, 'width', 0, this.width); + if (this.invalid) { this.$bar.classList.add('bar-invalid'); } @@ -402,6 +461,8 @@ class Bar { class: 'bar-progress', append_to: this.bar_group }); + + animateSVG(this.$bar_progress, 'width', 0, this.progress_width); } draw_label() { @@ -412,7 +473,8 @@ class Bar { class: 'bar-label', append_to: this.bar_group }); - this.update_label_position(); + // labels get BBox in the next tick + requestAnimationFrame(() => this.update_label_position()); } draw_resize_handles() { @@ -467,27 +529,35 @@ class Bar { bind() { if (this.invalid) return; this.setup_click_event(); - this.show_details(); - // this.bind_resize_progress(); } - show_details() { - this.group.onclick = e => { + setup_click_event() { + $.on(this.group, 'click', e => { if (this.action_completed) { // just finished a move action, wait for a few seconds return; } - const start_date = date_utils.format(this.task._start, 'MMM D'); - const end_date = date_utils.format(this.task._end, 'MMM D'); - const subtitle = start_date + ' - ' + end_date; + if (this.group.classList.contains('active')) { + this.gantt.trigger_event('click', [this.task]); + } + this.gantt.unselect_all(); + this.group.classList.toggle('active'); - this.gantt.show_popup({ - target_element: this.$bar, - title: this.task.name, - subtitle: subtitle - }); - }; + this.show_popup(); + }); + } + + show_popup() { + const start_date = date_utils.format(this.task._start, 'MMM D'); + const end_date = date_utils.format(this.task._end, 'MMM D'); + const subtitle = start_date + ' - ' + end_date; + + this.gantt.show_popup({ + target_element: this.$bar, + title: this.task.name, + subtitle: subtitle + }); } update_bar_position({ x = null, width = null }) { @@ -517,20 +587,6 @@ class Bar { // this.update_details_position(); } - setup_click_event() { - this.group.onclick = () => { - if (this.action_completed) { - // just finished a move action, wait for a few seconds - return; - } - if (this.group.classList.contains('active')) { - this.gantt.trigger_event('click', [this.task]); - } - this.gantt.unselect_all(); - this.group.classList.toggle('active'); - }; - } - date_changed() { const { new_start_date, new_end_date } = this.compute_start_end_date(); this.task._start = new_start_date; @@ -662,6 +718,7 @@ class Bar { update_label_position() { const bar = this.$bar, label = this.group.querySelector('.bar-label'); + if (label.getBBox().width > bar.getWidth()) { label.classList.add('big'); label.setAttribute('x', bar.getX() + bar.getWidth() + 5); @@ -849,6 +906,7 @@ class Popup { this.pointer.style.top = this.title.clientHeight / 2 - this.pointer.getBoundingClientRect().height + + 2 + 'px'; } @@ -1421,7 +1479,7 @@ class Gantt { bind_grid_click() { this.layers.grid.onclick = () => { this.unselect_all(); - this.popup && this.popup.hide(); + this.hide_popup(); }; } @@ -1661,6 +1719,10 @@ class Gantt { this.popup.show(options); } + hide_popup() { + this.popup && this.popup.hide(); + } + trigger_event(event, args) { if (this.options['on_' + event]) { this.options['on_' + event].apply(null, args); diff --git a/dist/frappe-gantt.min.js b/dist/frappe-gantt.min.js index b09b496..8986777 100644 --- a/dist/frappe-gantt.min.js +++ b/dist/frappe-gantt.min.js @@ -1 +1 @@ -var Gantt=function(){"use strict";const t=["January","February","March","April","May","June","July","August","September","October","November","December"];var e={parse(t,e="-",s=":"){if(t instanceof Date)return t;if("string"==typeof t){let i,n;const a=t.split(" ");i=a[0].split(e).map(t=>parseInt(t,10)),n=a[1]&&a[1].split(s),i[1]=i[1]-1;let o=i;return n&&n.length&&(o=o.concat(n)),new Date(...o)}},to_string(t,e=!1){if(!(t instanceof Date))throw new TypeError("Invalid argument type");const i=this.get_date_values(t).map((t,e)=>(1===e&&(t+=1),s(t+"",2,"0"))),n=`${i[0]}-${i[1]}-${i[2]}`,a=`${i[3]}:${i[4]}:${i[5]}`;return n+(e?" "+a:"")},format(e,i="YYYY-MM-DD HH:mm:ss"){const n=this.get_date_values(e).map(t=>s(t,2,0)),a={YYYY:n[0],MM:s(+n[1]+1,2,0),DD:n[2],HH:n[3],mm:n[4],ss:n[5],D:n[2],MMMM:t[+n[1]],MMM:t[+n[1]]};let o=i;return Object.keys(a).sort((t,e)=>e.length-t.length).forEach(t=>{o=o.replace(t,a[t])}),o},diff(t,e,s="day"){let i,n,a,o,r,h,d;return d=(h=(r=(a=(o=(n=(i=t-e)/1e3)/60)/60)/24)/30)/12,s.endsWith("s")||(s+="s"),Math.floor({milliseconds:i,seconds:n,minutes:o,hours:a,days:r,months:h,years:d}[s])},today(){const t=this.get_date_values(new Date).slice(0,3);return new Date(...t)},now:()=>new Date,add(t,e,s){e=parseInt(e,10);const i=[t.getFullYear()+("year"===s?e:0),t.getMonth()+("month"===s?e:0),t.getDate()+("day"===s?e:0),t.getHours()+("hour"===s?e:0),t.getMinutes()+("minute"===s?e:0),t.getSeconds()+("second"===s?e:0),t.getMilliseconds()+("millisecond"===s?e:0)];return new Date(...i)},start_of(t,e){const s={year:6,month:5,day:4,hour:3,minute:2,second:1,millisecond:0};function i(t){const i=s[e];return s[t]<=i}const n=[t.getFullYear(),i("year")?0:t.getMonth(),i("month")?1:t.getDate(),i("day")?0:t.getHours(),i("hour")?0:t.getMinutes(),i("minute")?0:t.getSeconds(),i("second")?0:t.getMilliseconds()];return new Date(...n)},clone(t){return new Date(...this.get_date_values(t))},get_date_values:t=>[t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()],get_days_in_month(t){const e=[31,28,31,30,31,30,31,31,30,31,30,31],s=t.getMonth();if(1!==s)return e[s];const i=t.getFullYear();return i%4==0&&i%100!=0||i%400==0?29:28}};function s(t,e,s){return t+="",e>>=0,s=String(void 0!==s?s:" "),t.length>e?String(t):((e-=t.length)>s.length&&(s+=s.repeat(e/s.length)),s.slice(0,e)+String(t))}function i(t,e){return"string"==typeof t?(e||document).querySelector(t):t||null}function n(t,e){const s=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t in e)if("append_to"===t){e.append_to.appendChild(s)}else"innerHTML"===t?s.innerHTML=e.innerHTML:s.setAttribute(t,e[t]);return s}i.on=((t,e,s,n)=>{n?i.delegate(t,e,s,n):(n=s,i.bind(t,e,n))}),i.off=((t,e,s)=>{t.removeEventListener(e,s)}),i.bind=((t,e,s)=>{e.split(/\s+/).forEach(function(e){t.addEventListener(e,s)})}),i.delegate=((t,e,s,i)=>{t.addEventListener(e,function(t){const e=t.target.closest(s);e&&(t.delegatedTarget=e,i.call(this,t,e))})}),i.closest=((t,e)=>e?e.matches(t)?e:i.closest(t,e.parentNode):null),i.attr=((t,e,s)=>{if(!s&&"string"==typeof e)return t.getAttribute(e);if("object"!=typeof e)t.setAttribute(e,s);else for(let s in e)i.attr(t,s,e[s])});class a{constructor(t,e){this.set_defaults(t,e),this.prepare(),this.draw(),this.bind()}set_defaults(t,e){this.action_completed=!1,this.gantt=t,this.task=e}prepare(){this.prepare_values(),this.prepare_helpers()}prepare_values(){this.invalid=this.task.invalid,this.height=this.gantt.options.bar.height,this.x=this.compute_x(),this.y=this.compute_y(),this.corner_radius=3,this.duration=(e.diff(this.task._end,this.task._start,"hour")+24)/this.gantt.options.step,this.width=this.gantt.options.column_width*this.duration,this.progress_width=this.gantt.options.column_width*this.duration*(this.task.progress/100)||0,this.group=n("g",{class:"bar-wrapper "+(this.task.custom_class||""),"data-id":this.task.id}),this.bar_group=n("g",{class:"bar-group",append_to:this.group}),this.handle_group=n("g",{class:"handle-group",append_to:this.group})}prepare_helpers(){SVGElement.prototype.getX=function(){return+this.getAttribute("x")},SVGElement.prototype.getY=function(){return+this.getAttribute("y")},SVGElement.prototype.getWidth=function(){return+this.getAttribute("width")},SVGElement.prototype.getHeight=function(){return+this.getAttribute("height")},SVGElement.prototype.getEndX=function(){return this.getX()+this.getWidth()}}draw(){this.draw_bar(),this.draw_progress_bar(),this.draw_label(),this.draw_resize_handles()}draw_bar(){this.$bar=n("rect",{x:this.x,y:this.y,width:this.width,height:this.height,rx:this.corner_radius,ry:this.corner_radius,class:"bar",append_to:this.bar_group}),this.invalid&&this.$bar.classList.add("bar-invalid")}draw_progress_bar(){this.invalid||(this.$bar_progress=n("rect",{x:this.x,y:this.y,width:this.progress_width,height:this.height,rx:this.corner_radius,ry:this.corner_radius,class:"bar-progress",append_to:this.bar_group}))}draw_label(){n("text",{x:this.x+this.width/2,y:this.y+this.height/2,innerHTML:this.task.name,class:"bar-label",append_to:this.bar_group}),this.update_label_position()}draw_resize_handles(){if(this.invalid)return;const t=this.$bar;n("rect",{x:t.getX()+t.getWidth()-9,y:t.getY()+1,width:8,height:this.height-2,rx:this.corner_radius,ry:this.corner_radius,class:"handle right",append_to:this.handle_group}),n("rect",{x:t.getX()+1,y:t.getY()+1,width:8,height:this.height-2,rx:this.corner_radius,ry:this.corner_radius,class:"handle left",append_to:this.handle_group}),this.task.progress&&this.task.progress<100&&(this.$handle_progress=n("polygon",{points:this.get_progress_polygon_points().join(","),class:"handle progress",append_to:this.handle_group}))}get_progress_polygon_points(){const t=this.$bar_progress;return[t.getEndX()-5,t.getY()+t.getHeight(),t.getEndX()+5,t.getY()+t.getHeight(),t.getEndX(),t.getY()+t.getHeight()-8.66]}bind(){this.invalid||(this.setup_click_event(),this.show_details())}show_details(){this.group.onclick=(t=>{if(this.action_completed)return;const s=e.format(this.task._start,"MMM D")+" - "+e.format(this.task._end,"MMM D");this.gantt.show_popup({target_element:this.$bar,title:this.task.name,subtitle:s})})}update_bar_position({x:t=null,width:e=null}){const s=this.$bar;if(t){if(!this.task.dependencies.map(t=>this.gantt.get_bar(t).$bar.getX()).reduce((e,s)=>t>=s,t))return void(e=null);this.update_attr(s,"x",t)}e&&e>=this.gantt.options.column_width&&this.update_attr(s,"width",e),this.update_label_position(),this.update_handle_position(),this.update_progressbar_position(),this.update_arrow_position()}setup_click_event(){this.group.onclick=(()=>{this.action_completed||(this.group.classList.contains("active")&&this.gantt.trigger_event("click",[this.task]),this.gantt.unselect_all(),this.group.classList.toggle("active"))})}date_changed(){const{new_start_date:t,new_end_date:e}=this.compute_start_end_date();this.task._start=t,this.task._end=e,this.gantt.trigger_event("date_change",[this.task,t,e])}progress_changed(){const t=this.compute_progress();this.task.progress=t,this.gantt.trigger_event("progress_change",[this.task,t])}set_action_completed(){this.action_completed=!0,setTimeout(()=>this.action_completed=!1,2e3)}compute_start_end_date(){const t=this.$bar,s=t.getX()/this.gantt.options.column_width,i=e.add(this.gantt.gantt_start,s*this.gantt.options.step,"hours"),n=t.getWidth()/this.gantt.options.column_width,a=e.add(i,n*this.gantt.options.step,"hours");return e.add(a,-1,"second"),{new_start_date:i,new_end_date:a}}compute_progress(){const t=this.$bar_progress.getWidth()/this.$bar.getWidth()*100;return parseInt(t,10)}compute_x(){let t=e.diff(this.task._start,this.gantt.gantt_start,"hour")/this.gantt.options.step*this.gantt.options.column_width;return this.gantt.view_is("Month")&&(t=e.diff(this.task._start,this.gantt.gantt_start,"day")*this.gantt.options.column_width/30),t}compute_y(){return this.gantt.options.header_height+this.gantt.options.padding+this.task._index*(this.height+this.gantt.options.padding)}get_snap_position(t){let e,s,i=t;return s=this.gantt.view_is("Week")?i-(e=t%(this.gantt.options.column_width/7))+(et.getWidth()?(e.classList.add("big"),e.setAttribute("x",t.getX()+t.getWidth()+5)):(e.classList.remove("big"),e.setAttribute("x",t.getX()+t.getWidth()/2))}update_handle_position(){const t=this.$bar;this.handle_group.querySelector(".handle.left").setAttribute("x",t.getX()+1),this.handle_group.querySelector(".handle.right").setAttribute("x",t.getEndX()-9);const e=this.group.querySelector(".handle.progress");e&&e.setAttribute("points",this.get_progress_polygon_points())}update_arrow_position(){this.arrows=this.arrows||[];for(let t of this.arrows)t.update()}update_details_position(){const{x:t,y:e}=get_details_position();this.details_box&&this.details_box.transform(`t${t},${e}`)}}class o{constructor(t,e,s){this.gantt=t,this.from_task=e,this.to_task=s,this.calculate_path(),this.draw()}calculate_path(){let t=this.from_task.$bar.getX()+this.from_task.$bar.getWidth()/2;const e=()=>this.to_task.$bar.getX()this.from_task.$bar.getX()+this.gantt.options.padding;for(;e();)t-=10;const s=this.gantt.options.header_height+this.gantt.options.bar.height+(this.gantt.options.padding+this.gantt.options.bar.height)*this.from_task.task._index+this.gantt.options.padding,i=this.to_task.$bar.getX()-this.gantt.options.padding/2,n=this.gantt.options.header_height+this.gantt.options.bar.height/2+(this.gantt.options.padding+this.gantt.options.bar.height)*this.to_task.task._index+this.gantt.options.padding,a=this.from_task.task._index>this.to_task.task._index,o=this.gantt.options.arrow.curve,r=a?1:0,h=a?-o:o,d=a?n+this.gantt.options.arrow.curve:n-this.gantt.options.arrow.curve;if(this.path=`\n M ${t} ${s}\n V ${d}\n a ${o} ${o} 0 0 ${r} ${o} ${h}\n L ${i} ${n}\n m -5 -5\n l 5 5\n l -5 5`,this.to_task.$bar.getX()\n
\n
\n ',this.hide(),this.title=this.parent.querySelector(".title"),this.subtitle=this.parent.querySelector(".subtitle"),this.pointer=this.parent.querySelector(".pointer")}show(t){if(!t.target_element)throw new Error("target_element is required to show popup");t.position||(t.position="left");const e=t.target_element;let s;this.title.innerHTML=t.title,this.subtitle.innerHTML=t.subtitle,this.parent.style.width=this.parent.clientWidth+"px",e instanceof HTMLElement?s=e.getBoundingClientRect():e instanceof SVGElement&&(s=t.target_element.getBBox()),"left"===t.position&&(this.parent.style.left=s.x+(s.width+10)+"px",this.parent.style.top=s.y-this.title.clientHeight/2+s.height/2+"px",this.pointer.style.transform="rotateZ(90deg)",this.pointer.style.left="-7px",this.pointer.style.top=this.title.clientHeight/2-this.pointer.getBoundingClientRect().height+"px"),this.parent.style.opacity=1}hide(){this.parent.style.opacity=0}}return class{constructor(t,e,s){this.setup_wrapper(t),this.setup_options(s),this.setup_tasks(e),this.change_view_mode(),this.bind_events()}setup_wrapper(t){if("string"==typeof t&&(t=document.querySelector(t)),!(t instanceof HTMLElement))throw new Error("Invalid argument passed for element");this.$container=document.createElement("div"),this.$container.classList.add("gantt-container"),t.appendChild(this.$container),this.$svg=n("svg",{append_to:this.$container,class:"gantt"}),this.popup_wrapper=document.createElement("div"),this.popup_wrapper.classList.add("popup-wrapper"),this.$svg.parentElement.appendChild(this.popup_wrapper)}setup_options(t){this.options=Object.assign({},{header_height:50,column_width:30,step:24,view_modes:["Quarter Day","Half Day","Day","Week","Month"],bar:{height:20},arrow:{curve:5},padding:18,view_mode:"Day",date_format:"YYYY-MM-DD",custom_popup_html:null},t)}setup_tasks(t){this.tasks=t.map((t,s)=>{if(t._start=e.parse(t.start),t._end=e.parse(t.end),e.diff(t._end,t._start,"year")>10&&(t.end=null),t._index=s,!t.start&&!t.end){const s=e.today();t._start=s,t._end=e.add(s,2,"day")}if(!t.start&&t.end&&(t._start=e.add(t._end,-2,"day")),t.start&&!t.end&&(t._end=e.add(t._start,2,"day")),t.start&&t.end||(t.invalid=!0),"string"==typeof t.dependencies||!t.dependencies){let e=[];t.dependencies&&(e=t.dependencies.split(",").map(t=>t.trim()).filter(t=>t)),t.dependencies=e}return t.id||(t.id=function(t){return t.name+"_"+Math.random().toString(36).slice(2,12)}(t)),t}),this.setup_dependencies()}setup_dependencies(){this.dependency_map={};for(let t of this.tasks)for(let e of t.dependencies)this.dependency_map[e]=this.dependency_map[e]||[],this.dependency_map[e].push(t.id)}refresh(t){this.setup_tasks(t),this.change_view_mode()}change_view_mode(t=this.options.view_mode){this.update_view_scale(t),this.setup_dates(),this.render(),this.trigger_event("view_change",[t])}update_view_scale(t){this.options.view_mode=t,"Day"===t?(this.options.step=24,this.options.column_width=38):"Half Day"===t?(this.options.step=12,this.options.column_width=38):"Quarter Day"===t?(this.options.step=6,this.options.column_width=38):"Week"===t?(this.options.step=168,this.options.column_width=140):"Month"===t&&(this.options.step=720,this.options.column_width=120)}setup_dates(){this.setup_gantt_dates(),this.setup_date_values()}setup_gantt_dates(){this.gantt_start=this.gantt_end=null;for(let t of this.tasks)(!this.gantt_start||t._startthis.gantt_end)&&(this.gantt_end=t._end);this.view_is(["Quarter Day","Half Day"])?(this.gantt_start=e.add(this.gantt_start,-7,"day"),this.gantt_end=e.add(this.gantt_end,7,"day")):this.view_is("Month")?(this.gantt_start=e.start_of(this.gantt_start,"year"),this.gantt_end=e.add(this.gantt_end,1,"year")):(this.gantt_start=e.add(this.gantt_start,-1,"month"),this.gantt_end=e.add(this.gantt_end,1,"month"))}setup_date_values(){this.dates=[];let t=null;for(;null===t||t=1&&a.getDate()<8&&(o+=" thick"),this.view_is("Month")&&(a.getMonth()+1)%3==0&&(o+=" thick"),n("path",{d:`M ${t} ${s} v ${i}`,class:o,append_to:this.layers.grid}),this.view_is("Month")?t+=e.get_days_in_month(a)*this.options.column_width/30:t+=this.options.column_width}}make_grid_highlights(){this.view_is("Day")&&n("rect",{x:e.diff(e.today(),this.gantt_start,"hour")/this.options.step*this.options.column_width,y:0,width:this.options.column_width,height:(this.options.bar.height+this.options.padding)*this.tasks.length+this.options.header_height+this.options.padding/2,class:"today-highlight",append_to:this.layers.grid})}make_dates(){for(let t of this.get_dates_to_draw())if(n("text",{x:t.lower_x,y:t.lower_y,innerHTML:t.lower_text,class:"lower-text",append_to:this.layers.date}),t.upper_text){const e=n("text",{x:t.upper_x,y:t.upper_y,innerHTML:t.upper_text,class:"upper-text",append_to:this.layers.date});e.getBBox().x2>this.layers.grid.getBBox().width&&e.remove()}}get_dates_to_draw(){let t=null;return this.dates.map((e,s)=>{const i=this.get_date_info(e,t,s);return t=e,i})}get_date_info(t,s,i){s||(s=e.add(t,1,"year"));const n={"Quarter Day_lower":e.format(t,"HH"),"Half Day_lower":e.format(t,"HH"),Day_lower:t.getDate()!==s.getDate()?e.format(t,"D"):"",Week_lower:t.getMonth()!==s.getMonth()?e.format(t,"D MMM"):e.format(t,"D"),Month_lower:e.format(t,"MMMM"),"Quarter Day_upper":t.getDate()!==s.getDate()?e.format(t,"D MMM"):"","Half Day_upper":t.getDate()!==s.getDate()?t.getMonth()!==s.getMonth()?e.format(t,"D MMM"):e.format(t,"D"):"",Day_upper:t.getMonth()!==s.getMonth()?e.format(t,"MMMM"):"",Week_upper:t.getMonth()!==s.getMonth()?e.format(t,"MMMM"):"",Month_upper:t.getFullYear()!==s.getFullYear()?e.format(t,"YYYY"):""},a={x:i*this.options.column_width,lower_y:this.options.header_height,upper_y:this.options.header_height-25},o={"Quarter Day_lower":4*this.options.column_width/2,"Quarter Day_upper":0,"Half Day_lower":2*this.options.column_width/2,"Half Day_upper":0,Day_lower:this.options.column_width/2,Day_upper:30*this.options.column_width/2,Week_lower:0,Week_upper:4*this.options.column_width/2,Month_lower:this.options.column_width/2,Month_upper:12*this.options.column_width/2};return{upper_text:n[`${this.options.view_mode}_upper`],lower_text:n[`${this.options.view_mode}_lower`],upper_x:a.x+o[`${this.options.view_mode}_upper`],upper_y:a.upper_y,lower_x:a.x+o[`${this.options.view_mode}_lower`],lower_y:a.lower_y}}make_bars(){this.bars=this.tasks.map(t=>{const e=new a(this,t);return this.layers.bar.appendChild(e.group),e})}make_arrows(){this.arrows=[];for(let t of this.tasks){let e=[];e=t.dependencies.map(e=>{const s=this.get_task(e);if(!s)return;const i=new o(this,this.bars[s._index],this.bars[t._index]);return this.layers.arrow.appendChild(i.element),i}).filter(Boolean),this.arrows=this.arrows.concat(e)}}map_arrows_on_bars(){for(let t of this.bars)t.arrows=this.arrows.filter(e=>e.from_task.task.id===t.task.id||e.to_task.task.id===t.task.id)}set_width(){const t=this.$svg.getBoundingClientRect().width,e=this.$svg.querySelector(".grid .grid-row").getAttribute("width");t{this.unselect_all(),this.popup&&this.popup.hide()})}bind_bar_events(){let t=!1,e=0,s=0,n=!1,a=!1,o=null,r=[];i.on(this.layers.bar,"mousedown",".bar-wrapper, .handle",(h,d)=>{const p=i.closest(".bar-wrapper",d);d.classList.contains("left")?n=!0:d.classList.contains("right")?a=!0:d.classList.contains("bar-wrapper")&&(t=!0),e=h.offsetX,s=h.offsetY;const _=[o=p.getAttribute("data-id"),...this.get_all_dependent_tasks(o)];(r=_.map(t=>this.get_bar(t))).forEach(t=>{const e=t.$bar;e.ox=e.getX(),e.oy=e.getY(),e.owidth=e.getWidth(),e.finaldx=0})}),i.on(this.$svg,"mousemove",s=>{if(!(t||n||a))return;const i=s.offsetX-e;s.offsetY,r.forEach(e=>{const s=e.$bar;s.finaldx=this.get_snap_position(i),n?o===e.task.id?e.update_bar_position({x:s.ox+s.finaldx,width:s.owidth-s.finaldx}):e.update_bar_position({x:s.ox+s.finaldx}):a?o===e.task.id&&e.update_bar_position({width:s.owidth+s.finaldx}):t&&e.update_bar_position({x:s.ox+s.finaldx})})}),document.addEventListener("mouseup",e=>{t=!1,n=!1,a=!1}),i.on(this.$svg,"mouseup",t=>{r.forEach(t=>{t.$bar.finaldx&&(t.date_changed(),t.set_action_completed())})}),this.bind_bar_progress()}bind_bar_progress(){let t=0,e=0,s=null,n=null,a=null,o=null;i.on(this.$svg,"mousedown",".handle.progress",(r,h)=>{s=!0,t=r.offsetX,e=r.offsetY;const d=i.closest(".bar-wrapper",h).getAttribute("data-id");n=this.get_bar(d),a=n.$bar_progress,o=n.$bar,a.finaldx=0,a.owidth=a.getWidth(),a.min_dx=-a.getWidth(),a.max_dx=o.getWidth()-a.getWidth()}),i.on(this.$svg,"mousemove",e=>{if(!s)return;let o=e.offsetX-t;e.offsetY,o>a.max_dx&&(o=a.max_dx),o{s=!1,a&&a.finaldx&&(n.progress_changed(),n.set_action_completed())})}get_all_dependent_tasks(t){let e=[],s=[t];for(;s.length;){const t=s.reduce((t,e)=>t=t.concat(this.dependency_map[e]),[]);e=e.concat(t),s=t.filter(t=>!s.includes(t))}return e.filter(Boolean)}get_snap_position(t){let e,s,i=t;return s=this.view_is("Week")?i-(e=t%(this.options.column_width/7))+(e{t.classList.remove("active")})}view_is(t){return"string"==typeof t?this.options.view_mode===t:!!Array.isArray(t)&&t.some(t=>this.options.view_mode===t)}get_task(t){return this.tasks.find(e=>e.id===t)}get_bar(t){return this.bars.find(e=>e.task.id===t)}show_popup(t){this.popup||(this.popup=new r(this.popup_wrapper)),this.popup.show(t)}trigger_event(t,e){this.options["on_"+t]&&this.options["on_"+t].apply(null,e)}get_oldest_starting_date(){return this.tasks.map(t=>t._start).reduce((t,e)=>e<=t?e:t)}clear(){this.$svg.innerHTML=""}}}(); +var Gantt=function(){"use strict";const t=["January","February","March","April","May","June","July","August","September","October","November","December"];var e={parse(t,e="-",s=":"){if(t instanceof Date)return t;if("string"==typeof t){let i,n;const a=t.split(" ");i=a[0].split(e).map(t=>parseInt(t,10)),n=a[1]&&a[1].split(s),i[1]=i[1]-1;let o=i;return n&&n.length&&(o=o.concat(n)),new Date(...o)}},to_string(t,e=!1){if(!(t instanceof Date))throw new TypeError("Invalid argument type");const i=this.get_date_values(t).map((t,e)=>(1===e&&(t+=1),s(t+"",2,"0"))),n=`${i[0]}-${i[1]}-${i[2]}`,a=`${i[3]}:${i[4]}:${i[5]}`;return n+(e?" "+a:"")},format(e,i="YYYY-MM-DD HH:mm:ss"){const n=this.get_date_values(e).map(t=>s(t,2,0)),a={YYYY:n[0],MM:s(+n[1]+1,2,0),DD:n[2],HH:n[3],mm:n[4],ss:n[5],D:n[2],MMMM:t[+n[1]],MMM:t[+n[1]]};let o=i;return Object.keys(a).sort((t,e)=>e.length-t.length).forEach(t=>{o=o.replace(t,a[t])}),o},diff(t,e,s="day"){let i,n,a,o,r,h,d;return d=(h=(r=(a=(o=(n=(i=t-e)/1e3)/60)/60)/24)/30)/12,s.endsWith("s")||(s+="s"),Math.floor({milliseconds:i,seconds:n,minutes:o,hours:a,days:r,months:h,years:d}[s])},today(){const t=this.get_date_values(new Date).slice(0,3);return new Date(...t)},now:()=>new Date,add(t,e,s){e=parseInt(e,10);const i=[t.getFullYear()+("year"===s?e:0),t.getMonth()+("month"===s?e:0),t.getDate()+("day"===s?e:0),t.getHours()+("hour"===s?e:0),t.getMinutes()+("minute"===s?e:0),t.getSeconds()+("second"===s?e:0),t.getMilliseconds()+("millisecond"===s?e:0)];return new Date(...i)},start_of(t,e){const s={year:6,month:5,day:4,hour:3,minute:2,second:1,millisecond:0};function i(t){const i=s[e];return s[t]<=i}const n=[t.getFullYear(),i("year")?0:t.getMonth(),i("month")?1:t.getDate(),i("day")?0:t.getHours(),i("hour")?0:t.getMinutes(),i("minute")?0:t.getSeconds(),i("second")?0:t.getMilliseconds()];return new Date(...n)},clone(t){return new Date(...this.get_date_values(t))},get_date_values:t=>[t.getFullYear(),t.getMonth(),t.getDate(),t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds()],get_days_in_month(t){const e=[31,28,31,30,31,30,31,31,30,31,30,31],s=t.getMonth();if(1!==s)return e[s];const i=t.getFullYear();return i%4==0&&i%100!=0||i%400==0?29:28}};function s(t,e,s){return t+="",e>>=0,s=String(void 0!==s?s:" "),t.length>e?String(t):((e-=t.length)>s.length&&(s+=s.repeat(e/s.length)),s.slice(0,e)+String(t))}function i(t,e){return"string"==typeof t?(e||document).querySelector(t):t||null}function n(t,e){const s=document.createElementNS("http://www.w3.org/2000/svg",t);for(let t in e)if("append_to"===t){e.append_to.appendChild(s)}else"innerHTML"===t?s.innerHTML=e.innerHTML:s.setAttribute(t,e[t]);return s}function a(t,e,s,a){const o=function(t,e,s,a,o="0.4s",r="0.1s"){const h=t.querySelector("animate");if(h)return i.attr(h,{attributeName:e,from:s,to:a,dur:o,begin:"click + "+r}),t;const d=n("animate",{attributeName:e,from:s,to:a,dur:o,begin:r,calcMode:"spline",values:s+";"+a,keyTimes:"0; 1",keySplines:(p="ease-out",{ease:".25 .1 .25 1",linear:"0 0 1 1","ease-in":".42 0 1 1","ease-out":"0 0 .58 1","ease-in-out":".42 0 .58 1"}[p])});var p;return t.appendChild(d),t}(t,e,s,a);if(o===t){const t=document.createEvent("HTMLEvents");t.initEvent("click",!0,!0),t.eventName="click",o.dispatchEvent(t)}}i.on=((t,e,s,n)=>{n?i.delegate(t,e,s,n):(n=s,i.bind(t,e,n))}),i.off=((t,e,s)=>{t.removeEventListener(e,s)}),i.bind=((t,e,s)=>{e.split(/\s+/).forEach(function(e){t.addEventListener(e,s)})}),i.delegate=((t,e,s,i)=>{t.addEventListener(e,function(t){const e=t.target.closest(s);e&&(t.delegatedTarget=e,i.call(this,t,e))})}),i.closest=((t,e)=>e?e.matches(t)?e:i.closest(t,e.parentNode):null),i.attr=((t,e,s)=>{if(!s&&"string"==typeof e)return t.getAttribute(e);if("object"!=typeof e)t.setAttribute(e,s);else for(let s in e)i.attr(t,s,e[s])});class o{constructor(t,e){this.set_defaults(t,e),this.prepare(),this.draw(),this.bind()}set_defaults(t,e){this.action_completed=!1,this.gantt=t,this.task=e}prepare(){this.prepare_values(),this.prepare_helpers()}prepare_values(){this.invalid=this.task.invalid,this.height=this.gantt.options.bar.height,this.x=this.compute_x(),this.y=this.compute_y(),this.corner_radius=3,this.duration=(e.diff(this.task._end,this.task._start,"hour")+24)/this.gantt.options.step,this.width=this.gantt.options.column_width*this.duration,this.progress_width=this.gantt.options.column_width*this.duration*(this.task.progress/100)||0,this.group=n("g",{class:"bar-wrapper "+(this.task.custom_class||""),"data-id":this.task.id}),this.bar_group=n("g",{class:"bar-group",append_to:this.group}),this.handle_group=n("g",{class:"handle-group",append_to:this.group})}prepare_helpers(){SVGElement.prototype.getX=function(){return+this.getAttribute("x")},SVGElement.prototype.getY=function(){return+this.getAttribute("y")},SVGElement.prototype.getWidth=function(){return+this.getAttribute("width")},SVGElement.prototype.getHeight=function(){return+this.getAttribute("height")},SVGElement.prototype.getEndX=function(){return this.getX()+this.getWidth()}}draw(){this.draw_bar(),this.draw_progress_bar(),this.draw_label(),this.draw_resize_handles()}draw_bar(){this.$bar=n("rect",{x:this.x,y:this.y,width:this.width,height:this.height,rx:this.corner_radius,ry:this.corner_radius,class:"bar",append_to:this.bar_group}),a(this.$bar,"width",0,this.width),this.invalid&&this.$bar.classList.add("bar-invalid")}draw_progress_bar(){this.invalid||(this.$bar_progress=n("rect",{x:this.x,y:this.y,width:this.progress_width,height:this.height,rx:this.corner_radius,ry:this.corner_radius,class:"bar-progress",append_to:this.bar_group}),a(this.$bar_progress,"width",0,this.progress_width))}draw_label(){n("text",{x:this.x+this.width/2,y:this.y+this.height/2,innerHTML:this.task.name,class:"bar-label",append_to:this.bar_group}),requestAnimationFrame(()=>this.update_label_position())}draw_resize_handles(){if(this.invalid)return;const t=this.$bar;n("rect",{x:t.getX()+t.getWidth()-9,y:t.getY()+1,width:8,height:this.height-2,rx:this.corner_radius,ry:this.corner_radius,class:"handle right",append_to:this.handle_group}),n("rect",{x:t.getX()+1,y:t.getY()+1,width:8,height:this.height-2,rx:this.corner_radius,ry:this.corner_radius,class:"handle left",append_to:this.handle_group}),this.task.progress&&this.task.progress<100&&(this.$handle_progress=n("polygon",{points:this.get_progress_polygon_points().join(","),class:"handle progress",append_to:this.handle_group}))}get_progress_polygon_points(){const t=this.$bar_progress;return[t.getEndX()-5,t.getY()+t.getHeight(),t.getEndX()+5,t.getY()+t.getHeight(),t.getEndX(),t.getY()+t.getHeight()-8.66]}bind(){this.invalid||this.setup_click_event()}setup_click_event(){i.on(this.group,"click",t=>{this.action_completed||(this.group.classList.contains("active")&&this.gantt.trigger_event("click",[this.task]),this.gantt.unselect_all(),this.group.classList.toggle("active"),this.show_popup())})}show_popup(){const t=e.format(this.task._start,"MMM D")+" - "+e.format(this.task._end,"MMM D");this.gantt.show_popup({target_element:this.$bar,title:this.task.name,subtitle:t})}update_bar_position({x:t=null,width:e=null}){const s=this.$bar;if(t){if(!this.task.dependencies.map(t=>this.gantt.get_bar(t).$bar.getX()).reduce((e,s)=>t>=s,t))return void(e=null);this.update_attr(s,"x",t)}e&&e>=this.gantt.options.column_width&&this.update_attr(s,"width",e),this.update_label_position(),this.update_handle_position(),this.update_progressbar_position(),this.update_arrow_position()}date_changed(){const{new_start_date:t,new_end_date:e}=this.compute_start_end_date();this.task._start=t,this.task._end=e,this.gantt.trigger_event("date_change",[this.task,t,e])}progress_changed(){const t=this.compute_progress();this.task.progress=t,this.gantt.trigger_event("progress_change",[this.task,t])}set_action_completed(){this.action_completed=!0,setTimeout(()=>this.action_completed=!1,2e3)}compute_start_end_date(){const t=this.$bar,s=t.getX()/this.gantt.options.column_width,i=e.add(this.gantt.gantt_start,s*this.gantt.options.step,"hours"),n=t.getWidth()/this.gantt.options.column_width,a=e.add(i,n*this.gantt.options.step,"hours");return e.add(a,-1,"second"),{new_start_date:i,new_end_date:a}}compute_progress(){const t=this.$bar_progress.getWidth()/this.$bar.getWidth()*100;return parseInt(t,10)}compute_x(){let t=e.diff(this.task._start,this.gantt.gantt_start,"hour")/this.gantt.options.step*this.gantt.options.column_width;return this.gantt.view_is("Month")&&(t=e.diff(this.task._start,this.gantt.gantt_start,"day")*this.gantt.options.column_width/30),t}compute_y(){return this.gantt.options.header_height+this.gantt.options.padding+this.task._index*(this.height+this.gantt.options.padding)}get_snap_position(t){let e,s,i=t;return s=this.gantt.view_is("Week")?i-(e=t%(this.gantt.options.column_width/7))+(et.getWidth()?(e.classList.add("big"),e.setAttribute("x",t.getX()+t.getWidth()+5)):(e.classList.remove("big"),e.setAttribute("x",t.getX()+t.getWidth()/2))}update_handle_position(){const t=this.$bar;this.handle_group.querySelector(".handle.left").setAttribute("x",t.getX()+1),this.handle_group.querySelector(".handle.right").setAttribute("x",t.getEndX()-9);const e=this.group.querySelector(".handle.progress");e&&e.setAttribute("points",this.get_progress_polygon_points())}update_arrow_position(){this.arrows=this.arrows||[];for(let t of this.arrows)t.update()}update_details_position(){const{x:t,y:e}=get_details_position();this.details_box&&this.details_box.transform(`t${t},${e}`)}}class r{constructor(t,e,s){this.gantt=t,this.from_task=e,this.to_task=s,this.calculate_path(),this.draw()}calculate_path(){let t=this.from_task.$bar.getX()+this.from_task.$bar.getWidth()/2;const e=()=>this.to_task.$bar.getX()this.from_task.$bar.getX()+this.gantt.options.padding;for(;e();)t-=10;const s=this.gantt.options.header_height+this.gantt.options.bar.height+(this.gantt.options.padding+this.gantt.options.bar.height)*this.from_task.task._index+this.gantt.options.padding,i=this.to_task.$bar.getX()-this.gantt.options.padding/2,n=this.gantt.options.header_height+this.gantt.options.bar.height/2+(this.gantt.options.padding+this.gantt.options.bar.height)*this.to_task.task._index+this.gantt.options.padding,a=this.from_task.task._index>this.to_task.task._index,o=this.gantt.options.arrow.curve,r=a?1:0,h=a?-o:o,d=a?n+this.gantt.options.arrow.curve:n-this.gantt.options.arrow.curve;if(this.path=`\n M ${t} ${s}\n V ${d}\n a ${o} ${o} 0 0 ${r} ${o} ${h}\n L ${i} ${n}\n m -5 -5\n l 5 5\n l -5 5`,this.to_task.$bar.getX()\n
\n
\n ',this.hide(),this.title=this.parent.querySelector(".title"),this.subtitle=this.parent.querySelector(".subtitle"),this.pointer=this.parent.querySelector(".pointer")}show(t){if(!t.target_element)throw new Error("target_element is required to show popup");t.position||(t.position="left");const e=t.target_element;let s;this.title.innerHTML=t.title,this.subtitle.innerHTML=t.subtitle,this.parent.style.width=this.parent.clientWidth+"px",e instanceof HTMLElement?s=e.getBoundingClientRect():e instanceof SVGElement&&(s=t.target_element.getBBox()),"left"===t.position&&(this.parent.style.left=s.x+(s.width+10)+"px",this.parent.style.top=s.y-this.title.clientHeight/2+s.height/2+"px",this.pointer.style.transform="rotateZ(90deg)",this.pointer.style.left="-7px",this.pointer.style.top=this.title.clientHeight/2-this.pointer.getBoundingClientRect().height+2+"px"),this.parent.style.opacity=1}hide(){this.parent.style.opacity=0}}return class{constructor(t,e,s){this.setup_wrapper(t),this.setup_options(s),this.setup_tasks(e),this.change_view_mode(),this.bind_events()}setup_wrapper(t){if("string"==typeof t&&(t=document.querySelector(t)),!(t instanceof HTMLElement))throw new Error("Invalid argument passed for element");this.$container=document.createElement("div"),this.$container.classList.add("gantt-container"),t.appendChild(this.$container),this.$svg=n("svg",{append_to:this.$container,class:"gantt"}),this.popup_wrapper=document.createElement("div"),this.popup_wrapper.classList.add("popup-wrapper"),this.$svg.parentElement.appendChild(this.popup_wrapper)}setup_options(t){this.options=Object.assign({},{header_height:50,column_width:30,step:24,view_modes:["Quarter Day","Half Day","Day","Week","Month"],bar:{height:20},arrow:{curve:5},padding:18,view_mode:"Day",date_format:"YYYY-MM-DD",custom_popup_html:null},t)}setup_tasks(t){this.tasks=t.map((t,s)=>{if(t._start=e.parse(t.start),t._end=e.parse(t.end),e.diff(t._end,t._start,"year")>10&&(t.end=null),t._index=s,!t.start&&!t.end){const s=e.today();t._start=s,t._end=e.add(s,2,"day")}if(!t.start&&t.end&&(t._start=e.add(t._end,-2,"day")),t.start&&!t.end&&(t._end=e.add(t._start,2,"day")),t.start&&t.end||(t.invalid=!0),"string"==typeof t.dependencies||!t.dependencies){let e=[];t.dependencies&&(e=t.dependencies.split(",").map(t=>t.trim()).filter(t=>t)),t.dependencies=e}return t.id||(t.id=function(t){return t.name+"_"+Math.random().toString(36).slice(2,12)}(t)),t}),this.setup_dependencies()}setup_dependencies(){this.dependency_map={};for(let t of this.tasks)for(let e of t.dependencies)this.dependency_map[e]=this.dependency_map[e]||[],this.dependency_map[e].push(t.id)}refresh(t){this.setup_tasks(t),this.change_view_mode()}change_view_mode(t=this.options.view_mode){this.update_view_scale(t),this.setup_dates(),this.render(),this.trigger_event("view_change",[t])}update_view_scale(t){this.options.view_mode=t,"Day"===t?(this.options.step=24,this.options.column_width=38):"Half Day"===t?(this.options.step=12,this.options.column_width=38):"Quarter Day"===t?(this.options.step=6,this.options.column_width=38):"Week"===t?(this.options.step=168,this.options.column_width=140):"Month"===t&&(this.options.step=720,this.options.column_width=120)}setup_dates(){this.setup_gantt_dates(),this.setup_date_values()}setup_gantt_dates(){this.gantt_start=this.gantt_end=null;for(let t of this.tasks)(!this.gantt_start||t._startthis.gantt_end)&&(this.gantt_end=t._end);this.view_is(["Quarter Day","Half Day"])?(this.gantt_start=e.add(this.gantt_start,-7,"day"),this.gantt_end=e.add(this.gantt_end,7,"day")):this.view_is("Month")?(this.gantt_start=e.start_of(this.gantt_start,"year"),this.gantt_end=e.add(this.gantt_end,1,"year")):(this.gantt_start=e.add(this.gantt_start,-1,"month"),this.gantt_end=e.add(this.gantt_end,1,"month"))}setup_date_values(){this.dates=[];let t=null;for(;null===t||t=1&&a.getDate()<8&&(o+=" thick"),this.view_is("Month")&&(a.getMonth()+1)%3==0&&(o+=" thick"),n("path",{d:`M ${t} ${s} v ${i}`,class:o,append_to:this.layers.grid}),this.view_is("Month")?t+=e.get_days_in_month(a)*this.options.column_width/30:t+=this.options.column_width}}make_grid_highlights(){this.view_is("Day")&&n("rect",{x:e.diff(e.today(),this.gantt_start,"hour")/this.options.step*this.options.column_width,y:0,width:this.options.column_width,height:(this.options.bar.height+this.options.padding)*this.tasks.length+this.options.header_height+this.options.padding/2,class:"today-highlight",append_to:this.layers.grid})}make_dates(){for(let t of this.get_dates_to_draw())if(n("text",{x:t.lower_x,y:t.lower_y,innerHTML:t.lower_text,class:"lower-text",append_to:this.layers.date}),t.upper_text){const e=n("text",{x:t.upper_x,y:t.upper_y,innerHTML:t.upper_text,class:"upper-text",append_to:this.layers.date});e.getBBox().x2>this.layers.grid.getBBox().width&&e.remove()}}get_dates_to_draw(){let t=null;return this.dates.map((e,s)=>{const i=this.get_date_info(e,t,s);return t=e,i})}get_date_info(t,s,i){s||(s=e.add(t,1,"year"));const n={"Quarter Day_lower":e.format(t,"HH"),"Half Day_lower":e.format(t,"HH"),Day_lower:t.getDate()!==s.getDate()?e.format(t,"D"):"",Week_lower:t.getMonth()!==s.getMonth()?e.format(t,"D MMM"):e.format(t,"D"),Month_lower:e.format(t,"MMMM"),"Quarter Day_upper":t.getDate()!==s.getDate()?e.format(t,"D MMM"):"","Half Day_upper":t.getDate()!==s.getDate()?t.getMonth()!==s.getMonth()?e.format(t,"D MMM"):e.format(t,"D"):"",Day_upper:t.getMonth()!==s.getMonth()?e.format(t,"MMMM"):"",Week_upper:t.getMonth()!==s.getMonth()?e.format(t,"MMMM"):"",Month_upper:t.getFullYear()!==s.getFullYear()?e.format(t,"YYYY"):""},a={x:i*this.options.column_width,lower_y:this.options.header_height,upper_y:this.options.header_height-25},o={"Quarter Day_lower":4*this.options.column_width/2,"Quarter Day_upper":0,"Half Day_lower":2*this.options.column_width/2,"Half Day_upper":0,Day_lower:this.options.column_width/2,Day_upper:30*this.options.column_width/2,Week_lower:0,Week_upper:4*this.options.column_width/2,Month_lower:this.options.column_width/2,Month_upper:12*this.options.column_width/2};return{upper_text:n[`${this.options.view_mode}_upper`],lower_text:n[`${this.options.view_mode}_lower`],upper_x:a.x+o[`${this.options.view_mode}_upper`],upper_y:a.upper_y,lower_x:a.x+o[`${this.options.view_mode}_lower`],lower_y:a.lower_y}}make_bars(){this.bars=this.tasks.map(t=>{const e=new o(this,t);return this.layers.bar.appendChild(e.group),e})}make_arrows(){this.arrows=[];for(let t of this.tasks){let e=[];e=t.dependencies.map(e=>{const s=this.get_task(e);if(!s)return;const i=new r(this,this.bars[s._index],this.bars[t._index]);return this.layers.arrow.appendChild(i.element),i}).filter(Boolean),this.arrows=this.arrows.concat(e)}}map_arrows_on_bars(){for(let t of this.bars)t.arrows=this.arrows.filter(e=>e.from_task.task.id===t.task.id||e.to_task.task.id===t.task.id)}set_width(){const t=this.$svg.getBoundingClientRect().width,e=this.$svg.querySelector(".grid .grid-row").getAttribute("width");t{this.unselect_all(),this.hide_popup()})}bind_bar_events(){let t=!1,e=0,s=0,n=!1,a=!1,o=null,r=[];i.on(this.layers.bar,"mousedown",".bar-wrapper, .handle",(h,d)=>{const p=i.closest(".bar-wrapper",d);d.classList.contains("left")?n=!0:d.classList.contains("right")?a=!0:d.classList.contains("bar-wrapper")&&(t=!0),e=h.offsetX,s=h.offsetY;const _=[o=p.getAttribute("data-id"),...this.get_all_dependent_tasks(o)];(r=_.map(t=>this.get_bar(t))).forEach(t=>{const e=t.$bar;e.ox=e.getX(),e.oy=e.getY(),e.owidth=e.getWidth(),e.finaldx=0})}),i.on(this.$svg,"mousemove",s=>{if(!(t||n||a))return;const i=s.offsetX-e;s.offsetY,r.forEach(e=>{const s=e.$bar;s.finaldx=this.get_snap_position(i),n?o===e.task.id?e.update_bar_position({x:s.ox+s.finaldx,width:s.owidth-s.finaldx}):e.update_bar_position({x:s.ox+s.finaldx}):a?o===e.task.id&&e.update_bar_position({width:s.owidth+s.finaldx}):t&&e.update_bar_position({x:s.ox+s.finaldx})})}),document.addEventListener("mouseup",e=>{t=!1,n=!1,a=!1}),i.on(this.$svg,"mouseup",t=>{r.forEach(t=>{t.$bar.finaldx&&(t.date_changed(),t.set_action_completed())})}),this.bind_bar_progress()}bind_bar_progress(){let t=0,e=0,s=null,n=null,a=null,o=null;i.on(this.$svg,"mousedown",".handle.progress",(r,h)=>{s=!0,t=r.offsetX,e=r.offsetY;const d=i.closest(".bar-wrapper",h).getAttribute("data-id");n=this.get_bar(d),a=n.$bar_progress,o=n.$bar,a.finaldx=0,a.owidth=a.getWidth(),a.min_dx=-a.getWidth(),a.max_dx=o.getWidth()-a.getWidth()}),i.on(this.$svg,"mousemove",e=>{if(!s)return;let o=e.offsetX-t;e.offsetY,o>a.max_dx&&(o=a.max_dx),o{s=!1,a&&a.finaldx&&(n.progress_changed(),n.set_action_completed())})}get_all_dependent_tasks(t){let e=[],s=[t];for(;s.length;){const t=s.reduce((t,e)=>t=t.concat(this.dependency_map[e]),[]);e=e.concat(t),s=t.filter(t=>!s.includes(t))}return e.filter(Boolean)}get_snap_position(t){let e,s,i=t;return s=this.view_is("Week")?i-(e=t%(this.options.column_width/7))+(e{t.classList.remove("active")})}view_is(t){return"string"==typeof t?this.options.view_mode===t:!!Array.isArray(t)&&t.some(t=>this.options.view_mode===t)}get_task(t){return this.tasks.find(e=>e.id===t)}get_bar(t){return this.bars.find(e=>e.task.id===t)}show_popup(t){this.popup||(this.popup=new h(this.popup_wrapper)),this.popup.show(t)}hide_popup(){this.popup&&this.popup.hide()}trigger_event(t,e){this.options["on_"+t]&&this.options["on_"+t].apply(null,e)}get_oldest_starting_date(){return this.tasks.map(t=>t._start).reduce((t,e)=>e<=t?e:t)}clear(){this.$svg.innerHTML=""}}}(); diff --git a/src/Bar.js b/src/Bar.js index 6e6cdd5..1bd6b85 100644 --- a/src/Bar.js +++ b/src/Bar.js @@ -1,5 +1,5 @@ import date_utils from './date_utils'; -import { createSVG } from './svg_utils'; +import { $, createSVG, animateSVG } from './svg_utils'; export default class Bar { constructor(gantt, task) { @@ -85,6 +85,8 @@ export default class Bar { append_to: this.bar_group }); + animateSVG(this.$bar, 'width', 0, this.width); + if (this.invalid) { this.$bar.classList.add('bar-invalid'); } @@ -102,6 +104,8 @@ export default class Bar { class: 'bar-progress', append_to: this.bar_group }); + + animateSVG(this.$bar_progress, 'width', 0, this.progress_width); } draw_label() { @@ -112,7 +116,8 @@ export default class Bar { class: 'bar-label', append_to: this.bar_group }); - this.update_label_position(); + // labels get BBox in the next tick + requestAnimationFrame(() => this.update_label_position()); } draw_resize_handles() { @@ -167,27 +172,35 @@ export default class Bar { bind() { if (this.invalid) return; this.setup_click_event(); - this.show_details(); - // this.bind_resize_progress(); } - show_details() { - this.group.onclick = e => { + setup_click_event() { + $.on(this.group, 'click', e => { if (this.action_completed) { // just finished a move action, wait for a few seconds return; } - const start_date = date_utils.format(this.task._start, 'MMM D'); - const end_date = date_utils.format(this.task._end, 'MMM D'); - const subtitle = start_date + ' - ' + end_date; + if (this.group.classList.contains('active')) { + this.gantt.trigger_event('click', [this.task]); + } + this.gantt.unselect_all(); + this.group.classList.toggle('active'); - this.gantt.show_popup({ - target_element: this.$bar, - title: this.task.name, - subtitle: subtitle - }); - }; + this.show_popup(); + }); + } + + show_popup() { + const start_date = date_utils.format(this.task._start, 'MMM D'); + const end_date = date_utils.format(this.task._end, 'MMM D'); + const subtitle = start_date + ' - ' + end_date; + + this.gantt.show_popup({ + target_element: this.$bar, + title: this.task.name, + subtitle: subtitle + }); } update_bar_position({ x = null, width = null }) { @@ -217,20 +230,6 @@ export default class Bar { // this.update_details_position(); } - setup_click_event() { - this.group.onclick = () => { - if (this.action_completed) { - // just finished a move action, wait for a few seconds - return; - } - if (this.group.classList.contains('active')) { - this.gantt.trigger_event('click', [this.task]); - } - this.gantt.unselect_all(); - this.group.classList.toggle('active'); - }; - } - date_changed() { const { new_start_date, new_end_date } = this.compute_start_end_date(); this.task._start = new_start_date; @@ -362,6 +361,7 @@ export default class Bar { update_label_position() { const bar = this.$bar, label = this.group.querySelector('.bar-label'); + if (label.getBBox().width > bar.getWidth()) { label.classList.add('big'); label.setAttribute('x', bar.getX() + bar.getWidth() + 5); diff --git a/src/gantt.scss b/src/gantt.scss index 6eba6cf..e4eac11 100644 --- a/src/gantt.scss +++ b/src/gantt.scss @@ -92,7 +92,11 @@ $handle-color: #ddd; &:hover { .bar { - stroke-width: 2; + fill: darken($bar-color, 5); + } + + .bar-progress { + fill: darken($blue, 5); } .handle { @@ -103,7 +107,11 @@ $handle-color: #ddd; &.active { .bar { - stroke-width: 2; + fill: darken($bar-color, 5); + } + + .bar-progress { + fill: darken($blue, 5); } } } diff --git a/src/index.js b/src/index.js index 9b674a2..b0365b7 100644 --- a/src/index.js +++ b/src/index.js @@ -566,7 +566,7 @@ export default class Gantt { bind_grid_click() { this.layers.grid.onclick = () => { this.unselect_all(); - this.popup && this.popup.hide(); + this.hide_popup(); }; } @@ -806,6 +806,10 @@ export default class Gantt { this.popup.show(options); } + hide_popup() { + this.popup && this.popup.hide(); + } + trigger_event(event, args) { if (this.options['on_' + event]) { this.options['on_' + event].apply(null, args); diff --git a/src/popup.js b/src/popup.js index f5c7626..6fdabdd 100644 --- a/src/popup.js +++ b/src/popup.js @@ -55,6 +55,7 @@ export default class Popup { this.pointer.style.top = this.title.clientHeight / 2 - this.pointer.getBoundingClientRect().height + + 2 + 'px'; } diff --git a/src/svg_utils.js b/src/svg_utils.js index a0f7bff..44add70 100644 --- a/src/svg_utils.js +++ b/src/svg_utils.js @@ -37,8 +37,8 @@ function getAnimationElement( attr, from, to, - dur = '0.3s', - begin = '0s' + dur = '0.4s', + begin = '0.1s' ) { const animEl = svgElement.querySelector('animate'); if (animEl) { @@ -57,13 +57,27 @@ function getAnimationElement( from, to, dur, - begin + begin, + calcMode: 'spline', + values: from + ';' + to, + keyTimes: '0; 1', + keySplines: cubic_bezier('ease-out') }); svgElement.appendChild(animateElement); return svgElement; } +function cubic_bezier(name) { + return { + ease: '.25 .1 .25 1', + linear: '0 0 1 1', + 'ease-in': '.42 0 1 1', + 'ease-out': '0 0 .58 1', + 'ease-in-out': '.42 0 .58 1' + }[name]; +} + $.on = (element, event, selector, callback) => { if (!callback) { callback = selector;