From 408ad5434376f9d4b707446a15d1d5da7de8bfec Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 00:25:30 +0100 Subject: [PATCH 01/14] Gantt diagram initial page --- app/controllers/projects/gantts_controller.rb | 37 +++++++++++++++++++ app/views/projects/gantts/_show.html.haml | 11 ++++++ app/views/projects/gantts/index.html.haml | 1 + app/views/projects/gantts/show.html.haml | 1 + app/views/projects/issues/_head.html.haml | 5 +++ config/routes/project.rb | 2 + 6 files changed, 57 insertions(+) create mode 100644 app/controllers/projects/gantts_controller.rb create mode 100644 app/views/projects/gantts/_show.html.haml create mode 100644 app/views/projects/gantts/index.html.haml create mode 100644 app/views/projects/gantts/show.html.haml diff --git a/app/controllers/projects/gantts_controller.rb b/app/controllers/projects/gantts_controller.rb new file mode 100644 index 000000000000..d1958b018056 --- /dev/null +++ b/app/controllers/projects/gantts_controller.rb @@ -0,0 +1,37 @@ +class Projects::GanttsController < Projects::ApplicationController + include IssuableCollections + + before_action :authorize_read_gantt!, only: [:index, :show] + + def index + @boards = ::Boards::ListService.new(project, current_user).execute + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@boards) + end + end + end + + def show + @board = project.boards.find(params[:id]) + + respond_to do |format| + format.html + format.json do + render json: serialize_as_json(@board) + end + end + end + + private + + def authorize_read_gantt! + return access_denied! unless can?(current_user, :read_board, project) + end + + def serialize_as_json(resource) + resource.as_json(only: [:id]) + end +end diff --git a/app/views/projects/gantts/_show.html.haml b/app/views/projects/gantts/_show.html.haml new file mode 100644 index 000000000000..23941b0305d1 --- /dev/null +++ b/app/views/projects/gantts/_show.html.haml @@ -0,0 +1,11 @@ +- @no_container = true +- @content_class = "issue-gantt-content" +- page_title "Gantt Diagram" + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('boards/gantt_bundle.js') + += render "projects/issues/head" + += render 'shared/issuable/filter', type: :boards + diff --git a/app/views/projects/gantts/index.html.haml b/app/views/projects/gantts/index.html.haml new file mode 100644 index 000000000000..2a5b8b1441ef --- /dev/null +++ b/app/views/projects/gantts/index.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/projects/gantts/show.html.haml b/app/views/projects/gantts/show.html.haml new file mode 100644 index 000000000000..2a5b8b1441ef --- /dev/null +++ b/app/views/projects/gantts/show.html.haml @@ -0,0 +1 @@ += render "show" diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 4825820c4d9d..4005d0c19aaf 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -14,6 +14,11 @@ %span Board + = nav_link(controller: :gantts) do + = link_to namespace_project_gantts_path(@project.namespace, @project), title: 'Gantt' do + %span + Gantt + - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do diff --git a/config/routes/project.rb b/config/routes/project.rb index 9cf8465dca8b..d3a584ef8e1f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -263,6 +263,8 @@ end end + resources :gantts, only: [:index, :show] + resources :todos, only: [:create] resources :uploads, only: [:create] do -- GitLab From d7b8dff50ec14a6ea7bf175b6dbe2d32f92b7e37 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 10:18:48 +0100 Subject: [PATCH 02/14] Gantt: Issue filtering --- app/controllers/projects/gantts_controller.rb | 14 ++-------- app/views/projects/gantts/_show.html.haml | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects/gantts_controller.rb b/app/controllers/projects/gantts_controller.rb index d1958b018056..31e4a3dd747c 100644 --- a/app/controllers/projects/gantts_controller.rb +++ b/app/controllers/projects/gantts_controller.rb @@ -4,24 +4,14 @@ class Projects::GanttsController < Projects::ApplicationController before_action :authorize_read_gantt!, only: [:index, :show] def index - @boards = ::Boards::ListService.new(project, current_user).execute - - respond_to do |format| - format.html - format.json do - render json: serialize_as_json(@boards) - end - end + show() end def show - @board = project.boards.find(params[:id]) + @issues = issues_collection respond_to do |format| format.html - format.json do - render json: serialize_as_json(@board) - end end end diff --git a/app/views/projects/gantts/_show.html.haml b/app/views/projects/gantts/_show.html.haml index 23941b0305d1..b69a2312cf1d 100644 --- a/app/views/projects/gantts/_show.html.haml +++ b/app/views/projects/gantts/_show.html.haml @@ -7,5 +7,31 @@ = render "projects/issues/head" -= render 'shared/issuable/filter', type: :boards +-# Issue filters +- if @project.issues.any? + .top-area + = render 'shared/issuable/nav', type: :issues + .nav-controls + = render 'shared/issuable/search_form', path: namespace_project_gantts_path(@project.namespace, @project) + - if can? current_user, :create_issue, @project + = link_to new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New Issue", + id: "new_issue_link" do + New Issue + = render 'shared/issuable/filter', type: :issues + +%div{:style => "padding: 5%; text-align: center;"} + = "Gantt diagram goes here" + +-# Debug dump +%pre + = "issues = " + = @issues.inspect +%ul.content-list.issues-list.issuable-list + = render partial: "projects/issues/issue", collection: @issues + -- GitLab From beaeff229b88dea3e53c9bdf746aba6b3b7627db Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 12:16:21 +0100 Subject: [PATCH 03/14] Gantt: Add moment.js --- app/assets/javascripts/lib/moment.js | 7 + vendor/assets/javascripts/moment.js | 551 +++++++++++++++++++++++++++ 2 files changed, 558 insertions(+) create mode 100644 app/assets/javascripts/lib/moment.js create mode 100644 vendor/assets/javascripts/moment.js diff --git a/app/assets/javascripts/lib/moment.js b/app/assets/javascripts/lib/moment.js new file mode 100644 index 000000000000..a434a1e8476a --- /dev/null +++ b/app/assets/javascripts/lib/moment.js @@ -0,0 +1,7 @@ +/* eslint-disable */ + +/*= require moment */ + +(function() { + +}).call(this); diff --git a/vendor/assets/javascripts/moment.js b/vendor/assets/javascripts/moment.js new file mode 100644 index 000000000000..097938e26b11 --- /dev/null +++ b/vendor/assets/javascripts/moment.js @@ -0,0 +1,551 @@ +//! moment.js +//! version : 2.16.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return od.apply(null,arguments)} +// This is done to register the method called with moment() +// without creating circular dependencies. +function b(a){od=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){ +// IE8 will treat undefined and null as object if it wasn't for +// input != null +return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a) +// even if its not own property I'd still call it non-empty +return!1;return!0}function f(a){return"number"==typeof value||"[object Number]"===Object.prototype.toString.call(a)}function g(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function h(a,b){var c,d=[];for(c=0;c0)for(c in rd)d=rd[c],e=b[d],p(e)||(a[d]=e);return a} +// Moment prototype object +function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN), +// Prevent infinite loop in case updateOffset creates new moment +// objects. +sd===!1&&(sd=!0,a.updateOffset(this),sd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c} +// compare two arrays, return the number of differences +function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Dd[c]=Dd[c+"s"]=Dd[b]=a}function K(a){return"string"==typeof a?Dd[a]||Dd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)i(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Ed[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Ed[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)} +// MOMENTS +function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d} +// token: 'M' +// padded: ['MM', 2] +// ordinal: 'Mo' +// callback: function () { this.month() + 1 } +function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Id[a]=e),b&&(Id[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Id[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Fd);for(b=0,c=d.length;b=0&&Gd.test(a);)a=a.replace(Gd,c),Gd.lastIndex=0,d-=1;return a}function Z(a,b,c){$d[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return i($d,a)?$d[a](b._strict,b._locale):new RegExp(_(a))} +// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript +function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),f(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments)); +//the Date.UTC function remaps years 0-99 to 1900-1999 +return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b} +// start-of-first-week - start-of-year +function ua(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other) +d=7+b-c, +// first-week day local weekday -- which local weekday is fwd +e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1} +//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday +function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7} +// HELPERS +// LOCALES +function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy} +// MOMENTS +function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")} +// HELPERS +function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=k([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){ +// test the regex +if( +// make the regex if we don't have it already +e=k([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}} +// MOMENTS +function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN; +// behaves the same as moment#day except +// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) +// as a setter, sunday should belong to the previous week. +if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,"_weekdaysRegex")||(this._weekdaysRegex=ue),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ve),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=we),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++) +// make the regex if we don't have it already +c=k([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for( +// Sorting makes sure if one weekday (or abbr) is a prefix of another it +// will match the longer piece. +g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")} +// FORMATTING +function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})} +// PARSING +function Ua(a,b){return b._meridiemParse} +// LOCALES +function Va(a){ +// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays +// Using charAt should be more compatible. +return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a} +// pick the locale from the array +// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each +// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root +function Ya(a){for(var b,c,d,e,f=0;f0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1) +//the next array item is better than a shallower substring of this one +break;b--}f++}return null}function Za(a){var b=null; +// TODO: Find a better way to register and load all the locales in Node +if(!Be[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=xe._abbr,require("./locale/"+a), +// because defineLocale currently also sets the global locale, we +// want to undo that for lazy loaded locales +$a(b)}catch(a){}return Be[a]} +// This function will load locale and then set the global locale. If +// no arguments are passed in, it will simply return the current global +// locale key. +function $a(a,b){var c; +// moment.duration._locale = moment._locale = data; +return a&&(c=p(b)?bb(a):_a(a,b),c&&(xe=c)),xe._abbr}function _a(a,b){if(null!==b){var c=Ae;if(b.abbr=a,null!=Be[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Be[a]._config;else if(null!=b.parentLocale){if(null==Be[b.parentLocale])return Ce[b.parentLocale]||(Ce[b.parentLocale]=[]),Ce[b.parentLocale].push({name:a,config:b}),null;c=Be[b.parentLocale]._config} +// backwards compat for now: also set the locale +// make sure we set the locale AFTER all child locales have been +// created, so we won't end up with the child locale set. +return Be[a]=new C(B(c,b)),Ce[a]&&Ce[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Be[a]} +// useful for testing +return delete Be[a],null}function ab(a,b){if(null!=b){var c,d=Ae; +// MERGE +null!=Be[a]&&(d=Be[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Be[a],Be[a]=c, +// backwards compat for now: also set the locale +$a(a)}else +// pass null for config to unupdate, useful for tests +null!=Be[a]&&(null!=Be[a].parentLocale?Be[a]=Be[a].parentLocale:null!=Be[a]&&delete Be[a]);return Be[a]} +// returns locale data +function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return xe;if(!c(a)){if( +//short-circuit everything else +b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return wd(Be)}function db(a){var b,c=a._a;return c&&m(a).overflow===-2&&(b=c[be]<0||c[be]>11?be:c[ce]<1||c[ce]>ea(c[ae],c[be])?ce:c[de]<0||c[de]>24||24===c[de]&&(0!==c[ee]||0!==c[fe]||0!==c[ge])?de:c[ee]<0||c[ee]>59?ee:c[fe]<0||c[fe]>59?fe:c[ge]<0||c[ge]>999?ge:-1,m(a)._overflowDayOfYear&&(bce)&&(b=ce),m(a)._overflowWeeks&&b===-1&&(b=he),m(a)._overflowWeekday&&b===-1&&(b=ie),m(a).overflow=b),a} +// date from iso format +function eb(a){var b,c,d,e,f,g,h=a._i,i=De.exec(h)||Ee.exec(h);if(i){for(m(a).iso=!0,b=0,c=Ge.length;bpa(e)&&(m(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[be]=c.getUTCMonth(),a._a[ce]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b]; +// Zero out whatever was not defaulted, including time +for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b]; +// Check for 24:00:00.000 +24===a._a[de]&&0===a._a[ee]&&0===a._a[fe]&&0===a._a[ge]&&(a._nextDay=!0,a._a[de]=0),a._d=(a._useUTC?ta:sa).apply(null,f), +// Apply timezone offset from input. The actual utcOffset can be changed +// with parseZone. +null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[de]=24)}}function jb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4, +// TODO: We need to take the current isoWeekYear, but that depends on +// how we interpret now (local, utc, fixed offset). So create +// a now version of current config (take local/utc/offset flags, and +// create now). +c=gb(b.GG,a._a[ae],wa(sb(),1,4).year),d=gb(b.W,1),e=gb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(sb(),f,g);c=gb(b.gg,a._a[ae],j.year), +// Default to current week. +d=gb(b.w,j.week),null!=b.d?( +// weekday -- low day numbers are considered next week +e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?( +// local weekday -- counting starts from begining of week +e=b.e+f,(b.e<0||b.e>6)&&(i=!0)): +// default to begining of week +e=f}d<1||d>xa(c,f,g)?m(a)._overflowWeeks=!0:null!=i?m(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ae]=h.year,a._dayOfYear=h.dayOfYear)} +// date from string and format string +function kb(b){ +// TODO: Move this to another part of the creation flow to prevent circular deps +if(b._f===a.ISO_8601)return void eb(b);b._a=[],m(b).empty=!0; +// This array is used to make a Date, either with `new Date` or `Date.UTC` +var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Fd)||[],c=0;c0&&m(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length), +// don't parse if it's not a known token +Id[f]?(d?m(b).empty=!1:m(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&m(b).unusedTokens.push(f); +// add remaining unparsed input length to the string +m(b).charsLeftOver=i-j,h.length>0&&m(b).unusedInput.push(h), +// clear _12h flag if hour is <= 12 +b._a[de]<=12&&m(b).bigHour===!0&&b._a[de]>0&&(m(b).bigHour=void 0),m(b).parsedDateParts=b._a.slice(0),m(b).meridiem=b._meridiem, +// handle meridiem +b._a[de]=lb(b._locale,b._a[de],b._meridiem),ib(b),db(b)}function lb(a,b,c){var d; +// Fallback +return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b} +// date from string and array of format strings +function mb(a){var b,c,d,e,f;if(0===a._f.length)return m(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e +// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset +// +0200, so we adjust the time as needed, to be valid. +// +// Keeping the time actually adds/subtracts (one hour) +// from the actual represented time. That is why we call updateOffset +// a second time. In case it wants us to change the offset again +// _changeInProgress == true case, then we have to adjust, because +// there is no such time in the given timezone. +function Db(b,c){var d,e=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Ab(Xd,b),null===b)return this}else Math.abs(b)<16&&(b=60*b);return!this._isUTC&&c&&(d=Cb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Tb(this,Ob(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?e:Cb(this)}function Eb(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Fb(a){return this.utcOffset(0,a)}function Gb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Cb(this),"m")),this}function Hb(){if(null!=this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=Ab(Wd,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Ib(a){return!!this.isValid()&&(a=a?sb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Jb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Kb(){if(!p(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=pb(a),a._a){var b=a._isUTC?k(a._a):sb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Lb(){return!!this.isValid()&&!this._isUTC}function Mb(){return!!this.isValid()&&this._isUTC}function Nb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ob(a,b){var c,d,e,g=a, +// matching against regexp is expensive, do it on demand +h=null;// checks for null or undefined +return xb(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:f(a)?(g={},b?g[b]=a:g.milliseconds=a):(h=Ne.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:u(h[ce])*c,h:u(h[de])*c,m:u(h[ee])*c,s:u(h[fe])*c,ms:u(yb(1e3*h[ge]))*c}):(h=Oe.exec(a))?(c="-"===h[1]?-1:1,g={y:Pb(h[2],c),M:Pb(h[3],c),w:Pb(h[4],c),d:Pb(h[5],c),h:Pb(h[6],c),m:Pb(h[7],c),s:Pb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Rb(sb(g.from),sb(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new wb(g),xb(a)&&i(a,"_locale")&&(d._locale=a._locale),d}function Pb(a,b){ +// We'd normally use ~~inp for this, but unfortunately it also +// converts floats to ints. +// inp may be undefined, so careful calling replace on it. +var c=a&&parseFloat(a.replace(",",".")); +// apply sign while we're at it +return(isNaN(c)?0:c)*b}function Qb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Rb(a,b){var c;return a.isValid()&&b.isValid()?(b=Bb(b,a),a.isBefore(b)?c=Qb(a,b):(c=Qb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}} +// TODO: remove 'name' arg after deprecation is removed +function Sb(a,b){return function(c,d){var e,f; +//invert the arguments, but complain about it +return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ob(c,d),Tb(this,e,a),this}}function Tb(b,c,d,e){var f=c._milliseconds,g=yb(c._days),h=yb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Ub(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Vb(b,c){ +// We want to compare the start of today, vs this. +// Getting start-of-today depends on whether we're local/utc/offset or not. +var d=b||sb(),e=Bb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,sb(d)))}function Wb(){return new r(this)}function Xb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()f&&(b=f),Fc.call(this,a,b,c,d,e))}function Fc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this} +// MOMENTS +function Gc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)} +// HELPERS +// MOMENTS +function Hc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Ic(a,b){b[ge]=u(1e3*("0."+a))} +// MOMENTS +function Jc(){return this._isUTC?"UTC":""}function Kc(){return this._isUTC?"Coordinated Universal Time":""}function Lc(a){return sb(1e3*a)}function Mc(){return sb.apply(null,arguments).parseZone()}function Nc(a){return a}function Oc(a,b,c,d){var e=bb(),f=k().set(d,b);return e[c](f,a)}function Pc(a,b,c){if(f(a)&&(b=a,a=void 0),a=a||"",null!=b)return Oc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Oc(a,d,c,"month");return e} +// () +// (5) +// (fmt, 5) +// (fmt) +// (true) +// (true, 5) +// (true, fmt, 5) +// (true, fmt) +function Qc(a,b,c,d){"boolean"==typeof a?(f(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,f(b)&&(c=b,b=void 0),b=b||"");var e=bb(),g=a?e._week.dow:0;if(null!=c)return Oc(b,(c+g)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Oc(b,(h+g)%7,d,"day");return i}function Rc(a,b){return Pc(a,b,"months")}function Sc(a,b){return Pc(a,b,"monthsShort")}function Tc(a,b,c){return Qc(a,b,c,"weekdays")}function Uc(a,b,c){return Qc(a,b,c,"weekdaysShort")}function Vc(a,b,c){return Qc(a,b,c,"weekdaysMin")}function Wc(){var a=this._data;return this._milliseconds=Ze(this._milliseconds),this._days=Ze(this._days),this._months=Ze(this._months),a.milliseconds=Ze(a.milliseconds),a.seconds=Ze(a.seconds),a.minutes=Ze(a.minutes),a.hours=Ze(a.hours),a.months=Ze(a.months),a.years=Ze(a.years),this}function Xc(a,b,c,d){var e=Ob(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()} +// supports only 2.0-style add(1, 's') or add(duration) +function Yc(a,b){return Xc(this,a,b,1)} +// supports only 2.0-style subtract(1, 's') or subtract(duration) +function Zc(a,b){return Xc(this,a,b,-1)}function $c(a){return a<0?Math.floor(a):Math.ceil(a)}function _c(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data; +// if we have a mix of positive and negative values, bubble down first +// check: https://github.com/moment/moment/issues/2166 +// The following code bubbles up values, see the tests for +// examples of what that means. +// convert days to months +// 12 months -> 1 year +return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*$c(bd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ad(g)),h+=e,g-=$c(bd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ad(a){ +// 400 years have 146097 days (taking into account leap year rules) +// 400 years have 12 months === 4800 +return 4800*a/146097}function bd(a){ +// the reverse of daysToMonths +return 146097*a/4800}function cd(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ad(b),"month"===a?c:c/12;switch( +// handle milliseconds separately because of floating point math errors (issue #1867) +b=this._days+Math.round(bd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3; +// Math.floor prevents floating point math errors here +case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}} +// TODO: Use this.as('ms')? +function dd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)}function ed(a){return function(){return this.as(a)}}function fd(a){return a=K(a),this[a+"s"]()}function gd(a){return function(){return this._data[a]}}function hd(){return t(this.days()/7)} +// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize +function id(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function jd(a,b,c){var d=Ob(a).abs(),e=of(d.as("s")),f=of(d.as("m")),g=of(d.as("h")),h=of(d.as("d")),i=of(d.as("M")),j=of(d.as("y")),k=e0,k[4]=c,id.apply(null,k)} +// This function allows you to set the rounding function for relative time strings +function kd(a){return void 0===a?of:"function"==typeof a&&(of=a,!0)} +// This function allows you to set a threshold for relative time strings +function ld(a,b){return void 0!==pf[a]&&(void 0===b?pf[a]:(pf[a]=b,!0))}function md(a){var b=this.localeData(),c=jd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function nd(){ +// for ISO strings we do not use the normal bubbling rules: +// * milliseconds bubble up until they become hours +// * days do not bubble at all +// * months bubble up until they become years +// This is because there is no context-free conversion between hours and days +// (think of clock changes) +// and also not between days and months (28-31 days per month) +var a,b,c,d=qf(this._milliseconds)/1e3,e=qf(this._days),f=qf(this._months); +// 3600 seconds -> 60 minutes -> 1 hour +a=t(d/60),b=t(a/60),d%=60,a%=60, +// 12 months -> 1 year +c=t(f/12),f%=12; +// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js +var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var od,pd;pd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d68?1900:2e3)}; +// MOMENTS +var pe=O("FullYear",!0); +// FORMATTING +U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"), +// ALIASES +J("week","w"),J("isoWeek","W"), +// PRIORITIES +M("week",5),M("isoWeek",5), +// PARSING +Z("w",Od),Z("ww",Od,Kd),Z("W",Od),Z("WW",Od,Kd),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var qe={dow:0,// Sunday is the first day of the week. +doy:6}; +// FORMATTING +U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"), +// ALIASES +J("day","d"),J("weekday","e"),J("isoWeekday","E"), +// PRIORITY +M("day",11),M("weekday",11),M("isoWeekday",11), +// PARSING +Z("d",Od),Z("e",Od),Z("E",Od),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict); +// if we didn't get a weekday name, mark the date as invalid +null!=e?b.d=e:m(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)}); +// LOCALES +var re="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),se="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),te="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ue=Zd,ve=Zd,we=Zd;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1), +// ALIASES +J("hour","h"), +// PRIORITY +M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Od),Z("h",Od),Z("HH",Od,Kd),Z("hh",Od,Kd),Z("hmm",Pd),Z("hmmss",Qd),Z("Hmm",Pd),Z("Hmmss",Qd),ba(["H","HH"],de),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[de]=u(a),m(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d)),m(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e)),m(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e))});var xe,ye=/[ap]\.?m?\.?/i,ze=O("Hours",!0),Ae={calendar:xd,longDateFormat:yd,invalidDate:zd,ordinal:Ad,ordinalParse:Bd,relativeTime:Cd,months:le,monthsShort:me,week:qe,weekdays:re,weekdaysMin:te,weekdaysShort:se,meridiemParse:ye},Be={},Ce={},De=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ee=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Fe=/Z|[+-]\d\d(?::?\d\d)?/,Ge=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/], +// YYYYMM is NOT allowed by the standard +["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],He=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ie=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=x("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}), +// constant that refers to the ISO standard +a.ISO_8601=function(){};var Je=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?athis?this:a:o()}),Le=function(){return Date.now?Date.now():+new Date};zb("Z",":"),zb("ZZ",""), +// PARSING +Z("Z",Xd),Z("ZZ",Xd),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ab(Xd,a)}); +// HELPERS +// timezone chunker +// '+10:00' > ['10', '00'] +// '-1530' > ['-15', '30'] +var Me=/([\+\-]|\d\d)/gi; +// HOOKS +// This function will be called whenever a moment is mutated. +// It is intended to keep the offset in sync with the timezone. +a.updateOffset=function(){}; +// ASP.NET json date format regex +var Ne=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Oe=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ob.fn=wb.prototype;var Pe=Sb(1,"add"),Qe=Sb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Re=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)}); +// FORMATTING +U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),zc("gggg","weekYear"),zc("ggggg","weekYear"),zc("GGGG","isoWeekYear"),zc("GGGGG","isoWeekYear"), +// ALIASES +J("weekYear","gg"),J("isoWeekYear","GG"), +// PRIORITY +M("weekYear",1),M("isoWeekYear",1), +// PARSING +Z("G",Vd),Z("g",Vd),Z("GG",Od,Kd),Z("gg",Od,Kd),Z("GGGG",Sd,Md),Z("gggg",Sd,Md),Z("GGGGG",Td,Nd),Z("ggggg",Td,Nd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}), +// FORMATTING +U("Q",0,"Qo","quarter"), +// ALIASES +J("quarter","Q"), +// PRIORITY +M("quarter",7), +// PARSING +Z("Q",Jd),ba("Q",function(a,b){b[be]=3*(u(a)-1)}), +// FORMATTING +U("D",["DD",2],"Do","date"), +// ALIASES +J("date","D"), +// PRIOROITY +M("date",9), +// PARSING +Z("D",Od),Z("DD",Od,Kd),Z("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),ba(["D","DD"],ce),ba("Do",function(a,b){b[ce]=u(a.match(Od)[0],10)}); +// MOMENTS +var Se=O("Date",!0); +// FORMATTING +U("DDD",["DDDD",3],"DDDo","dayOfYear"), +// ALIASES +J("dayOfYear","DDD"), +// PRIORITY +M("dayOfYear",4), +// PARSING +Z("DDD",Rd),Z("DDDD",Ld),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}), +// FORMATTING +U("m",["mm",2],0,"minute"), +// ALIASES +J("minute","m"), +// PRIORITY +M("minute",14), +// PARSING +Z("m",Od),Z("mm",Od,Kd),ba(["m","mm"],ee); +// MOMENTS +var Te=O("Minutes",!1); +// FORMATTING +U("s",["ss",2],0,"second"), +// ALIASES +J("second","s"), +// PRIORITY +M("second",15), +// PARSING +Z("s",Od),Z("ss",Od,Kd),ba(["s","ss"],fe); +// MOMENTS +var Ue=O("Seconds",!1); +// FORMATTING +U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}), +// ALIASES +J("millisecond","ms"), +// PRIORITY +M("millisecond",16), +// PARSING +Z("S",Rd,Jd),Z("SS",Rd,Kd),Z("SSS",Rd,Ld);var Ve;for(Ve="SSSS";Ve.length<=9;Ve+="S")Z(Ve,Ud);for(Ve="S";Ve.length<=9;Ve+="S")ba(Ve,Ic); +// MOMENTS +var We=O("Milliseconds",!1); +// FORMATTING +U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var Xe=r.prototype;Xe.add=Pe,Xe.calendar=Vb,Xe.clone=Wb,Xe.diff=bc,Xe.endOf=oc,Xe.format=gc,Xe.from=hc,Xe.fromNow=ic,Xe.to=jc,Xe.toNow=kc,Xe.get=R,Xe.invalidAt=xc,Xe.isAfter=Xb,Xe.isBefore=Yb,Xe.isBetween=Zb,Xe.isSame=$b,Xe.isSameOrAfter=_b,Xe.isSameOrBefore=ac,Xe.isValid=vc,Xe.lang=Re,Xe.locale=lc,Xe.localeData=mc,Xe.max=Ke,Xe.min=Je,Xe.parsingFlags=wc,Xe.set=S,Xe.startOf=nc,Xe.subtract=Qe,Xe.toArray=sc,Xe.toObject=tc,Xe.toDate=rc,Xe.toISOString=ec,Xe.inspect=fc,Xe.toJSON=uc,Xe.toString=dc,Xe.unix=qc,Xe.valueOf=pc,Xe.creationData=yc, +// Year +Xe.year=pe,Xe.isLeapYear=ra, +// Week Year +Xe.weekYear=Ac,Xe.isoWeekYear=Bc, +// Quarter +Xe.quarter=Xe.quarters=Gc, +// Month +Xe.month=ka,Xe.daysInMonth=la, +// Week +Xe.week=Xe.weeks=Ba,Xe.isoWeek=Xe.isoWeeks=Ca,Xe.weeksInYear=Dc,Xe.isoWeeksInYear=Cc, +// Day +Xe.date=Se,Xe.day=Xe.days=Ka,Xe.weekday=La,Xe.isoWeekday=Ma,Xe.dayOfYear=Hc, +// Hour +Xe.hour=Xe.hours=ze, +// Minute +Xe.minute=Xe.minutes=Te, +// Second +Xe.second=Xe.seconds=Ue, +// Millisecond +Xe.millisecond=Xe.milliseconds=We, +// Offset +Xe.utcOffset=Db,Xe.utc=Fb,Xe.local=Gb,Xe.parseZone=Hb,Xe.hasAlignedHourOffset=Ib,Xe.isDST=Jb,Xe.isLocal=Lb,Xe.isUtcOffset=Mb,Xe.isUtc=Nb,Xe.isUTC=Nb, +// Timezone +Xe.zoneAbbr=Jc,Xe.zoneName=Kc, +// Deprecations +Xe.dates=x("dates accessor is deprecated. Use date instead.",Se),Xe.months=x("months accessor is deprecated. Use month instead",ka),Xe.years=x("years accessor is deprecated. Use year instead",pe),Xe.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Eb),Xe.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Kb);var Ye=C.prototype;Ye.calendar=D,Ye.longDateFormat=E,Ye.invalidDate=F,Ye.ordinal=G,Ye.preparse=Nc,Ye.postformat=Nc,Ye.relativeTime=H,Ye.pastFuture=I,Ye.set=A, +// Month +Ye.months=fa,Ye.monthsShort=ga,Ye.monthsParse=ia,Ye.monthsRegex=na,Ye.monthsShortRegex=ma, +// Week +Ye.week=ya,Ye.firstDayOfYear=Aa,Ye.firstDayOfWeek=za, +// Day of Week +Ye.weekdays=Fa,Ye.weekdaysMin=Ha,Ye.weekdaysShort=Ga,Ye.weekdaysParse=Ja,Ye.weekdaysRegex=Na,Ye.weekdaysShortRegex=Oa,Ye.weekdaysMinRegex=Pa, +// Hours +Ye.isPM=Va,Ye.meridiem=Wa,$a("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}), +// Side effect imports +a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var Ze=Math.abs,$e=ed("ms"),_e=ed("s"),af=ed("m"),bf=ed("h"),cf=ed("d"),df=ed("w"),ef=ed("M"),ff=ed("y"),gf=gd("milliseconds"),hf=gd("seconds"),jf=gd("minutes"),kf=gd("hours"),lf=gd("days"),mf=gd("months"),nf=gd("years"),of=Math.round,pf={s:45,// seconds to minute +m:45,// minutes to hour +h:22,// hours to day +d:26,// days to month +M:11},qf=Math.abs,rf=wb.prototype; +// Deprecations +// Side effect imports +// FORMATTING +// PARSING +// Side effect imports +return rf.abs=Wc,rf.add=Yc,rf.subtract=Zc,rf.as=cd,rf.asMilliseconds=$e,rf.asSeconds=_e,rf.asMinutes=af,rf.asHours=bf,rf.asDays=cf,rf.asWeeks=df,rf.asMonths=ef,rf.asYears=ff,rf.valueOf=dd,rf._bubble=_c,rf.get=fd,rf.milliseconds=gf,rf.seconds=hf,rf.minutes=jf,rf.hours=kf,rf.days=lf,rf.weeks=hd,rf.months=mf,rf.years=nf,rf.humanize=md,rf.toISOString=nd,rf.toString=nd,rf.toJSON=nd,rf.locale=lc,rf.localeData=mc,rf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",nd),rf.lang=Re,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Vd),Z("X",Yd),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.16.0",b(sb),a.fn=Xe,a.min=ub,a.max=vb,a.now=Le,a.utc=k,a.unix=Lc,a.months=Rc,a.isDate=g,a.locale=$a,a.invalid=o,a.duration=Ob,a.isMoment=s,a.weekdays=Tc,a.parseZone=Mc,a.localeData=bb,a.isDuration=xb,a.monthsShort=Sc,a.weekdaysMin=Vc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Uc,a.normalizeUnits=K,a.relativeTimeRounding=kd,a.relativeTimeThreshold=ld,a.calendarFormat=Ub,a.prototype=Xe,a}); \ No newline at end of file -- GitLab From 4240801db02734922454d091edf6cdedec24ed3f Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 12:15:17 +0100 Subject: [PATCH 04/14] Gantt: The first diagram --- app/assets/javascripts/gantts/gantt.js.es6 | 675 ++++++++++++++++++ .../javascripts/gantts/gantt_bundle.js.es6 | 15 + app/assets/stylesheets/pages/gantts.scss | 108 +++ app/views/projects/gantts/_show.html.haml | 31 +- config/application.rb | 1 + 5 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/gantts/gantt.js.es6 create mode 100644 app/assets/javascripts/gantts/gantt_bundle.js.es6 create mode 100644 app/assets/stylesheets/pages/gantts.scss diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 new file mode 100644 index 000000000000..0b2c210b5ebc --- /dev/null +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -0,0 +1,675 @@ +//= require vue +//= require d3 +//= require moment + +(() => { + // Moment.js locale configuration + const MOMENTJS_LOCALE = '"en"'; + + // You are free to configure any string for gantt start/due dates, + // which are read in your issues descriptions + const GANTT_START_STRING = '"GanttStart: "'; + const GANTT_DUE_STRING = '"GanttDue: "'; + + window.gl = window.gl || {}; + window.gl.gantt = window.gl.gantt || {}; + +gl.gantt.Gantt = Vue.extend({ + props: [ + 'issues' + ], + data () { + return { + ganttStartString: GANTT_START_STRING, + ganttDueString: GANTT_DUE_STRING, + dataset: [] + } + }, + watch: { + issues: function (newIssues) { + this.buildDataSet() + this.refreshChart() + } + }, + methods: { + buildDataSet: function (event) { + // clearing the dataset to build it from issues list + this.dataset = [] + + // looping on issues + for (var i = this.issues.length - 1; i >= 0; i--) { + var theIssue = this.issues[i] + + // stripping issue title to the first 40 characters + var title = theIssue.title + if (title.length > 40) { + title = title.substring(0, 40) + '...' + } + + // creating the dataset + var aDataset = { + 'measure': title, + 'link': theIssue.web_url + } + + // initializing issue start and due date + var startDate = null + var dueDate = null + + // reading lines from this issue description to search for ganttStartString and ganttDueString + if (theIssue.description != null) { + var lines = theIssue.description.split('\r\n') + for (var j = 0; j < lines.length; j++) { + // this description line starts with the ganttStartString + if (!lines[j].indexOf(this.ganttStartString)) { + // this issue start date for gantt view is set to the appropriate date + startDate = new Date(lines[j].replace(this.ganttStartString, '')) + } + + // this description line starts with the ganttDueString + if (!lines[j].indexOf(this.ganttDueString)) { + // this issue due date for gantt view is set to the appropriate date + dueDate = new Date(lines[j].replace(this.ganttDueString, '')) + } + } + } + + // if start date is still null, we set it from issue creation date + if (startDate == null) { + startDate = new Date(theIssue.created_at) + } + + // if due date is still null we set it to the issue due date, or to the day after the issue creation date + if (dueDate == null) { + dueDate = theIssue.due_date + if (dueDate == null) { + // the issue due date is unset + dueDate = new Date(theIssue.created_at) + // the due date is calculated to the day after the issue creation date + dueDate.setDate(dueDate.getDate() + 1) + } else { + // the issue due date is used + dueDate = new Date(theIssue.due_date) + } + } + + // determining if the issue is late or not + var today = new Date() + var status = 1 + if (dueDate < today) { + status = 0 + } + + // formatting start and due dates for visavail + var fDueDate = dueDate.getUTCFullYear() + '-' + this.pad(dueDate.getUTCMonth() + 1) + '-' + this.pad(dueDate.getUTCDate()) + var fStartDate = startDate.getUTCFullYear() + '-' + this.pad(startDate.getUTCMonth() + 1) + '-' + this.pad(startDate.getUTCDate()) + aDataset.data = [ + [ fStartDate, status, fDueDate ] + ] + + // adding the dataset built to the main dataset list + this.dataset.push(aDataset) + } + }, + // thank you Florian Roscheck for this, you made an awesome work I only needed to tweak a little + visavailChart: function (event) { + // define chart layout + var margin = { + // top margin includes title and legend + top: 70, + + // right margin should provide space for last horz. axis title + right: 40, + + bottom: 20, + + // left margin should provide space for y axis titles + left: 250 + } + + // height of horizontal data bars + var dataHeight = 18 + + // spacing between horizontal data bars + var lineSpacing = 14 + + // vertical space for heading + var paddingTopHeading = -50 + + // vertical overhang of vertical grid lines on bottom + var paddingBottom = 10 + + // space for y axis titles + var paddingLeft = -250 + + var width = window.innerWidth - margin.left - margin.right - 20 + + // title of chart is drawn or not (default: yes) + var drawTitle = 0 + + // year ticks to be emphasized or not (default: yes) + var emphasizeYearTicks = 1 + + // define chart pagination + // max. no. of datasets that is displayed, 0: all (default: all) + var maxDisplayDatasets = 0 + + // dataset that is displayed first in the current + // display, chart will show datasets "curDisplayFirstDataset" to + // "curDisplayFirstDataset+maxDisplayDatasets" + var curDisplayFirstDataset = 0 + + // global div for tooltip + var div = d3.select('body').append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + + var definedBlocks = null + var isDateOnlyFormat = null + + function chart (selection) { + selection.each(function drawGraph (dataset) { + // check which subset of datasets have to be displayed + var maxPages = 0 + var startSet + var endSet + if (maxDisplayDatasets !== 0) { + startSet = curDisplayFirstDataset + if (curDisplayFirstDataset + maxDisplayDatasets > dataset.length) { + endSet = dataset.length + } else { + endSet = curDisplayFirstDataset + maxDisplayDatasets + } + maxPages = Math.ceil(dataset.length / maxDisplayDatasets) + } else { + startSet = 0 + endSet = dataset.length + } + + // append data attribute in HTML for pagination interface + selection.attr('data-max-pages', maxPages) + + var noOfDatasets = endSet - startSet + var height = dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + + // check how data is arranged + if (definedBlocks === null) { + definedBlocks = 0 + for (var i = 0; i < dataset.length; i++) { + if (dataset[i].data[0].length === 3) { + definedBlocks = 1 + break + } else { + if (definedBlocks) { + throw new Error('Detected different data formats in input data. Format can either be ' + + 'continuous data format or time gap data format but not both.') + } + } + } + } + + // parse data text strings to JavaScript date stamps + var parseDate = d3.time.format('%Y-%m-%d') + var parseDateTime = d3.time.format('%Y-%m-%d %H:%M:%S') + var parseDateRegEx = new RegExp(/^\d{4}-\d{2}-\d{2}$/) + var parseDateTimeRegEx = new RegExp(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) + if (isDateOnlyFormat === null) { + isDateOnlyFormat = true + } + dataset.forEach(function (d) { + d.data.forEach(function (d1) { + if (!(d1[0] instanceof Date)) { + if (parseDateRegEx.test(d1[0])) { + // d1[0] is date without time data + d1[0] = parseDate.parse(d1[0]) + } else if (parseDateTimeRegEx.test(d1[0])) { + // d1[0] is date with time data + d1[0] = parseDateTime.parse(d1[0]) + isDateOnlyFormat = false + } else { + throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + + '\'YYYY-MM-DD HH:MM:SS\'.') + } + + if (!definedBlocks) { + d1[2] = d3.time.second.offset(d1[0], d.interval_s) + } else { + if (parseDateRegEx.test(d1[2])) { + // d1[2] is date without time data + d1[2] = parseDate.parse(d1[2]) + } else if (parseDateTimeRegEx.test(d1[2])) { + // d1[2] is date with time data + d1[2] = parseDateTime.parse(d1[2]) + } else { + throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + + '\'YYYY-MM-DD HH:MM:SS\'.') + } + } + } + }) + }) + + // cluster data by dates to form time blocks + dataset.forEach(function (series, seriesI) { + var tmpData = [] + var dataLength = series.data.length + series.data.forEach(function (d, i) { + if (i !== 0 && i < dataLength) { + if (d[1] === tmpData[tmpData.length - 1][1]) { + // the value has not changed since the last date + if (definedBlocks) { + if (tmpData[tmpData.length - 1][2].getTime() === d[0].getTime()) { + // end of old and start of new block are the same + tmpData[tmpData.length - 1][2] = d[2] + tmpData[tmpData.length - 1][3] = 1 + } else { + tmpData.push(d) + } + } else { + tmpData[tmpData.length - 1][2] = d[2] + tmpData[tmpData.length - 1][3] = 1 + } + } else { + // the value has changed since the last date + d[3] = 0 + if (!definedBlocks) { + // extend last block until new block starts + tmpData[tmpData.length - 1][2] = d[0] + } + tmpData.push(d) + } + } else if (i === 0) { + d[3] = 0 + tmpData.push(d) + } + }) + dataset[seriesI].disp_data = tmpData + }) + + // determine start and end dates among all nested datasets + var startDate = 0 + var endDate = 0 + + dataset.forEach(function (series, seriesI) { + if (series.disp_data.length > 0) { + if (startDate === 0) { + startDate = series.disp_data[0][0] + endDate = series.disp_data[series.disp_data.length - 1][2] + } else { + if (series.disp_data[0][0] < startDate) { + startDate = series.disp_data[0][0] + } + if (series.disp_data[series.disp_data.length - 1][2] > endDate) { + endDate = series.disp_data[series.disp_data.length - 1][2] + } + } + } + }) + + // define scales + var xScale = d3.time.scale() + .domain([startDate, endDate]) + .range([0, width]) + .clamp(1) + + // define axes + var xAxis = d3.svg.axis() + .scale(xScale) + .orient('top') + + // create SVG element + var svg = d3.select(this).append('svg') + .attr('width', width + margin.left + margin.right - 20) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + + // create basic element groups + svg.append('g').attr('id', 'g_title') + svg.append('g').attr('id', 'g_axis') + svg.append('g').attr('id', 'g_data') + + // create y axis labels + svg.select('#g_axis').selectAll('text') + .data(dataset.slice(startSet, endSet)) + .enter() + .append('a') + .attr('xlink:href', function (d) { + return d.link + }) + .attr('xlink:show', 'new') + .append('text') + .attr('x', paddingLeft) + .attr('y', lineSpacing + dataHeight / 2) + .text(function (d) { + return d.measure + }) + .attr('transform', function (d, i) { + return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' + }) + .attr('class', 'ytitle') + + // create vertical grid + svg.select('#g_axis').selectAll('line.vert_grid').data(xScale.ticks()) + .enter() + .append('line') + .attr({ + 'class': 'vert_grid', + 'x1': function (d) { + return xScale(d) + }, + 'x2': function (d) { + return xScale(d) + }, + 'y1': 0, + 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom + }) + + // create horizontal grid + svg.select('#g_axis').selectAll('line.horz_grid').data(dataset) + .enter() + .append('line') + .attr({ + 'class': 'horz_grid', + 'x1': 0, + 'x2': width, + 'y1': function (d, i) { + return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 + }, + 'y2': function (d, i) { + return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 + } + }) + + // create x axis + svg.select('#g_axis').append('g') + .attr('class', 'axis') + .call(xAxis) + + // make y groups for different data series + var g = svg.select('#g_data').selectAll('.g_data') + .data(dataset.slice(startSet, endSet)) + .enter() + .append('a') + .attr('xlink:href', function (d) { + return d.link + }) + .attr('xlink:show', 'new') + .append('g') + .attr('transform', function (d, i) { + return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' + }) + .attr('class', 'dataset') + + // add data series + g.selectAll('rect') + .data(function (d) { + return d.disp_data + }) + .enter() + .append('rect') + .attr('x', function (d) { + return xScale(d[0]) + }) + .attr('y', lineSpacing) + .attr('width', function (d) { + return (xScale(d[2]) - xScale(d[0])) + }) + .attr('height', dataHeight) + .attr('class', function (d) { + if (d[1] === 1) { + return 'rect_has_data' + } + return 'rect_has_no_data' + }) + .on('mouseover', function (d, i) { + var matrix = this.getScreenCTM().translate(+this.getAttribute('x'), +this.getAttribute('y')) + div.transition() + .duration(200) + .style('opacity', 0.9) + div.html(function () { + var output = '' + if (d[1] === 1) { + output = '' + } else { + output = '' + } + if (isDateOnlyFormat) { + if (d[2] > d3.time.second.offset(d[0], 86400)) { + output = output + moment(parseDate(d[0])).format('l') + + ' - ' + moment(parseDate(d[2])).format('l') + } else { + output = output + moment(parseDate(d[0])).format('l') + } + } else { + if (d[2] > d3.time.second.offset(d[0], 86400)) { + output = output + moment(parseDateTime(d[0])).format('l') + ' ' + + moment(parseDateTime(d[0])).format('LTS') + ' - ' + + moment(parseDateTime(d[2])).format('l') + ' ' + + moment(parseDateTime(d[2])).format('LTS') + } else { + output = output + moment(parseDateTime(d[0])).format('LTS') + ' - ' + + moment(parseDateTime(d[2])).format('LTS') + } + } + return output + }) + .style('left', function () { + return window.pageXOffset + matrix.e + 'px' + }) + .style('top', function () { + return window.pageYOffset + matrix.f - 11 + 'px' + }) + .style('height', dataHeight + 11 + 'px') + }) + .on('mouseout', function () { + div.transition() + .duration(500) + .style('opacity', 0) + }) + + // rework ticks and grid for better visual structure + function isYear (t) { + return +t === +(new Date(t.getFullYear(), 0, 1, 0, 0, 0)) + } + + function isMonth (t) { + return +t === +(new Date(t.getFullYear(), t.getMonth(), 1, 0, 0, 0)) + } + + var xTicks = xScale.ticks() + var isYearTick = xTicks.map(isYear) + var isMonthTick = xTicks.map(isMonth) + // year emphasis + // ensure year emphasis is only active if years are the biggest clustering unit + if (emphasizeYearTicks && + !(isYearTick.every(function (d) { return d === true })) && + isMonthTick.every(function (d) { return d === true })) { + d3.selectAll('g.tick').each(function (d, i) { + if (isYearTick[i]) { + d3.select(this) + .attr({ + 'class': 'x_tick_emph' + }) + } + }) + d3.selectAll('.vert_grid').each(function (d, i) { + if (isYearTick[i]) { + d3.select(this) + .attr({ + 'class': 'vert_grid_emph' + }) + } + }) + } + + // today emphasis + var todayDate = new Date() + var emphasizedToday = false + todayDate.setHours(0, 0, 0, 0) + d3.selectAll('g.tick').each(function (d, i) { + if (d.getTime() === todayDate.getTime()) { + // tick exists for today, emphasizing it + d3.select(this) + .attr({ + 'class': 'x_tick_today' + }) + } + }) + d3.selectAll('.vert_grid').each(function (d, i) { + if (d.getTime() === todayDate.getTime()) { + // vertical line exists in the grid for today, emphasizing it + d3.select(this) + .attr({ + 'class': 'vert_grid_today', + 'stroke-dasharray': '10, 5' + }) + emphasizedToday = true + } + }) + + // not emphasized today yet + if (emphasizedToday === false) { + var interpolatedX = xScale.interpolate(d3.interpolate)(todayDate) + var lastInterpolatedX = xScale.interpolate(d3.interpolate)(endDate) + if (interpolatedX === lastInterpolatedX && todayDate > endDate) { + // today is out of range (not displayed in the chart) + interpolatedX += 18 + } + // adding the emphasized today vertical line + svg.select('#g_axis') + .append('line') + .attr({ + 'class': 'vert_grid_today', + 'stroke-dasharray': '10, 5', + 'x1': interpolatedX, + 'x2': interpolatedX, + 'y1': 0, + 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom + }) + } + + // create title + if (drawTitle) { + svg.select('#g_title') + .append('text') + .attr('x', paddingLeft) + .attr('y', paddingTopHeading) + .text('Data Availability Plot') + .attr('class', 'heading') + } + + // create subtitle + var subtitleText = '' + if (isDateOnlyFormat) { + subtitleText = moment(parseDate(startDate)).format('MMMM Y') + ' - ' + + moment(parseDate(endDate)).format('MMMM Y') + } else { + subtitleText = moment(parseDateTime(startDate)).format('l') + ' ' + + moment(parseDateTime(startDate)).format('LTS') + ' - ' + + moment(parseDateTime(endDate)).format('l') + ' ' + + moment(parseDateTime(endDate)).format('LTS') + } + + svg.select('#g_title') + .append('text') + .attr('x', paddingLeft) + .attr('y', paddingTopHeading + 17) + .text(subtitleText) + .attr('class', 'subheading') + + // create legend + var legend = svg.select('#g_title') + .append('g') + .attr('id', 'g_legend') + .attr('transform', 'translate(0,-12)') + + legend.append('rect') + .attr('x', width + margin.right - 150) + .attr('y', paddingTopHeading) + .attr('height', 15) + .attr('width', 15) + .attr('class', 'rect_has_data') + + legend.append('text') + .attr('x', width + margin.right - 150 + 20) + .attr('y', paddingTopHeading + 8.5) + .text('On time issues') + .attr('class', 'legend') + + legend.append('rect') + .attr('x', width + margin.right - 150) + .attr('y', paddingTopHeading + 17) + .attr('height', 15) + .attr('width', 15) + .attr('class', 'rect_has_no_data') + + legend.append('text') + .attr('x', width + margin.right - 150 + 20) + .attr('y', paddingTopHeading + 8.5 + 15 + 2) + .text('Late issues') + .attr('class', 'legend') + }) + } + + chart.width = function (_) { + if (!arguments.length) return width + width = _ + return chart + } + + chart.drawTitle = function (_) { + if (!arguments.length) return drawTitle + drawTitle = _ + return chart + } + + chart.maxDisplayDatasets = function (_) { + if (!arguments.length) return maxDisplayDatasets + maxDisplayDatasets = _ + return chart + } + + chart.curDisplayFirstDataset = function (_) { + if (!arguments.length) return curDisplayFirstDataset + curDisplayFirstDataset = _ + return chart + } + + chart.emphasizeYearTicks = function (_) { + if (!arguments.length) return emphasizeYearTicks + emphasizeYearTicks = _ + return chart + } + + return chart + }, + refreshChart: function (event) { + var chart = this.visavailChart().width(document.body.clientWidth - 290) + + d3.select('#gantt-app') // FIXME: Duplicate selector + .datum(this.dataset) + .call(chart) + }, + pad: function (number) { + var r = String(number) + if (r.length === 1) { + r = '0' + r + } + return r + } + }, + mounted: function () { + // set Moment.js locale + moment.locale(MOMENTJS_LOCALE) + + // build the dataset with the issues + this.buildDataSet() + + // refresh the gantt graph + this.refreshChart() + } +}); + +})(); + diff --git a/app/assets/javascripts/gantts/gantt_bundle.js.es6 b/app/assets/javascripts/gantts/gantt_bundle.js.es6 new file mode 100644 index 000000000000..78d143d03824 --- /dev/null +++ b/app/assets/javascripts/gantts/gantt_bundle.js.es6 @@ -0,0 +1,15 @@ +/* eslint-disable */ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +//= require vue +//= require d3 +//= require moment +//= require_tree . + +(function() { +}).call(this); + diff --git a/app/assets/stylesheets/pages/gantts.scss b/app/assets/stylesheets/pages/gantts.scss new file mode 100644 index 000000000000..112fa1ab42b6 --- /dev/null +++ b/app/assets/stylesheets/pages/gantts.scss @@ -0,0 +1,108 @@ +.gantt { + padding: 1em; +} + +.gantt .rect_has_data { + /* blocks that have data */ + fill: #5cb85c; +} + +.gantt .rect_has_data:hover { + fill: #449d44 +} + +.gantt .rect_has_no_data { + /* blocks without data */ + fill: #d9534d; +} + +.gantt .rect_has_no_data:hover { + fill: #c9302c; +} + +.gantt .tooltip_has_data { + /* color of symbol in tooltip if there is data */ + color: #449d44; +} + +.gantt .tooltip_has_no_data { + /* color of symbol in tooltip if there is no data */ + color: #c9302c; +} + +.gantt div.tooltip { + position: absolute; + text-align: left; + padding-left: 0; + width: auto; + border: 0; + border-left: thin solid #000000; + pointer-events: none; + line-height: 12px; + padding-top: 0; + display: block; +} + +.gantt .x_tick_emph { + font-weight: bold; +} + +.gantt .x_tick_today { + font-weight: bold; +} + +.gantt .x_tick_today text { + fill: #ff0000 !important; +} + +.gantt .ytitle { + /* y axis labels */ + dominant-baseline: middle; +} + +.gantt .axis path, +.gantt .axis line { + display: none; +} + +.gantt .axis text { + fill: #777; +} + +.gantt .vert_grid { + fill: none; + stroke: #dddddd; + stroke-width: 1px; +} + +.gantt .vert_grid_emph { + fill: none; + stroke: #dddddd; + stroke-width: 2px; +} + +.gantt .vert_grid_today { + fill: none; + stroke: #ff0000; + stroke-width: 2px; +} + +.gantt .horz_grid { + fill: none; + stroke: #dddddd; + stroke-width: 1px; +} + +.gantt .heading { + font-weight: bold; +} + +.gantt .subheading { + fill: #777; +} + +.gantt .legend { + dominant-baseline: middle; + fill: #777; +} + diff --git a/app/views/projects/gantts/_show.html.haml b/app/views/projects/gantts/_show.html.haml index b69a2312cf1d..89b36d6878d5 100644 --- a/app/views/projects/gantts/_show.html.haml +++ b/app/views/projects/gantts/_show.html.haml @@ -3,7 +3,7 @@ - page_title "Gantt Diagram" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('boards/gantt_bundle.js') + = page_specific_javascript_tag('gantts/gantt_bundle.js') = render "projects/issues/head" @@ -24,14 +24,29 @@ New Issue = render 'shared/issuable/filter', type: :issues -%div{:style => "padding: 5%; text-align: center;"} - = "Gantt diagram goes here" +-# The Gantt diagram +- if @issues.blank? + .nothing-here-block No issues to show +-else + .gantt{:id => "gantt-app"} + :javascript + window.gl = window.gl || {}; + + if (gl.GanttApp) { + gl.GanttApp.$destroy(true); + } + + gl.GanttApp = new gl.gantt.Gantt({ + el: document.getElementById('gantt-app'), + computed: { + issues: () => { return (#{@issues.to_json}); } + } + }); -# Debug dump -%pre - = "issues = " - = @issues.inspect -%ul.content-list.issues-list.issuable-list - = render partial: "projects/issues/issue", collection: @issues +%div + %hr + %ul.content-list.issues-list.issuable-list + = render partial: "projects/issues/issue", collection: @issues diff --git a/config/application.rb b/config/application.rb index 946b632b0e80..03c6ab2aed80 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,7 @@ class Application < Rails::Application config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "gantts/gantt_bundle.js" config.assets.precompile << "graphs/graphs_bundle.js" config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" -- GitLab From 8a10d93a16dc757c9ede9883586aaee5fe0ac394 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 12:34:42 +0100 Subject: [PATCH 05/14] Gantt: Fix issue labels and links --- app/assets/javascripts/gantts/gantt.js.es6 | 8 +++----- app/views/projects/gantts/_show.html.haml | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 index 0b2c210b5ebc..1020f2355de4 100644 --- a/app/assets/javascripts/gantts/gantt.js.es6 +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -16,7 +16,7 @@ gl.gantt.Gantt = Vue.extend({ props: [ - 'issues' + 'issues', 'issueLinkPrefix' ], data () { return { @@ -41,7 +41,7 @@ gl.gantt.Gantt = Vue.extend({ var theIssue = this.issues[i] // stripping issue title to the first 40 characters - var title = theIssue.title + var title = "#" + theIssue.id + ": " + theIssue.title if (title.length > 40) { title = title.substring(0, 40) + '...' } @@ -49,7 +49,7 @@ gl.gantt.Gantt = Vue.extend({ // creating the dataset var aDataset = { 'measure': title, - 'link': theIssue.web_url + 'link': theIssue.web_url || (this.issueLinkPrefix + '/' + theIssue.id) } // initializing issue start and due date @@ -337,7 +337,6 @@ gl.gantt.Gantt = Vue.extend({ .attr('xlink:href', function (d) { return d.link }) - .attr('xlink:show', 'new') .append('text') .attr('x', paddingLeft) .attr('y', lineSpacing + dataHeight / 2) @@ -394,7 +393,6 @@ gl.gantt.Gantt = Vue.extend({ .attr('xlink:href', function (d) { return d.link }) - .attr('xlink:show', 'new') .append('g') .attr('transform', function (d, i) { return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' diff --git a/app/views/projects/gantts/_show.html.haml b/app/views/projects/gantts/_show.html.haml index 89b36d6878d5..e96735c8cb8e 100644 --- a/app/views/projects/gantts/_show.html.haml +++ b/app/views/projects/gantts/_show.html.haml @@ -39,7 +39,8 @@ gl.GanttApp = new gl.gantt.Gantt({ el: document.getElementById('gantt-app'), computed: { - issues: () => { return (#{@issues.to_json}); } + issues: () => { return (#{@issues.to_json}); }, + issueLinkPrefix: () => { return "#{namespace_project_issues_path(@project.namespace, @project);}"; }, } }); -- GitLab From 43281b4a93936fc21b9f95c1086ac16d3c15e5b7 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 14:33:54 +0100 Subject: [PATCH 06/14] Gantt: Fix CSS indentation --- app/assets/stylesheets/pages/gantts.scss | 88 ++++++++++++------------ 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/app/assets/stylesheets/pages/gantts.scss b/app/assets/stylesheets/pages/gantts.scss index 112fa1ab42b6..328525017879 100644 --- a/app/assets/stylesheets/pages/gantts.scss +++ b/app/assets/stylesheets/pages/gantts.scss @@ -1,108 +1,108 @@ .gantt { - padding: 1em; + padding: 1em; } .gantt .rect_has_data { - /* blocks that have data */ - fill: #5cb85c; + /* blocks that have data */ + fill: #5cb85c; } .gantt .rect_has_data:hover { - fill: #449d44 + fill: #449d44 } .gantt .rect_has_no_data { - /* blocks without data */ - fill: #d9534d; + /* blocks without data */ + fill: #d9534d; } .gantt .rect_has_no_data:hover { - fill: #c9302c; + fill: #c9302c; } .gantt .tooltip_has_data { - /* color of symbol in tooltip if there is data */ - color: #449d44; + /* color of symbol in tooltip if there is data */ + color: #449d44; } .gantt .tooltip_has_no_data { - /* color of symbol in tooltip if there is no data */ - color: #c9302c; + /* color of symbol in tooltip if there is no data */ + color: #c9302c; } .gantt div.tooltip { - position: absolute; - text-align: left; - padding-left: 0; - width: auto; - border: 0; - border-left: thin solid #000000; - pointer-events: none; - line-height: 12px; - padding-top: 0; - display: block; + position: absolute; + text-align: left; + padding-left: 0; + width: auto; + border: 0; + border-left: thin solid #000000; + pointer-events: none; + line-height: 12px; + padding-top: 0; + display: block; } .gantt .x_tick_emph { - font-weight: bold; + font-weight: bold; } .gantt .x_tick_today { - font-weight: bold; + font-weight: bold; } .gantt .x_tick_today text { - fill: #ff0000 !important; + fill: #ff0000 !important; } .gantt .ytitle { - /* y axis labels */ - dominant-baseline: middle; + /* y axis labels */ + dominant-baseline: middle; } .gantt .axis path, .gantt .axis line { - display: none; + display: none; } .gantt .axis text { - fill: #777; + fill: #777; } .gantt .vert_grid { - fill: none; - stroke: #dddddd; - stroke-width: 1px; + fill: none; + stroke: #dddddd; + stroke-width: 1px; } .gantt .vert_grid_emph { - fill: none; - stroke: #dddddd; - stroke-width: 2px; + fill: none; + stroke: #dddddd; + stroke-width: 2px; } .gantt .vert_grid_today { - fill: none; - stroke: #ff0000; - stroke-width: 2px; + fill: none; + stroke: #ff0000; + stroke-width: 2px; } .gantt .horz_grid { - fill: none; - stroke: #dddddd; - stroke-width: 1px; + fill: none; + stroke: #dddddd; + stroke-width: 1px; } .gantt .heading { - font-weight: bold; + font-weight: bold; } .gantt .subheading { - fill: #777; + fill: #777; } .gantt .legend { - dominant-baseline: middle; - fill: #777; + dominant-baseline: middle; + fill: #777; } -- GitLab From 80591fa8ed447a3d5088f5fb8eead3c0b43e6645 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 18:13:08 +0100 Subject: [PATCH 07/14] Gantt: Fix SCSS colors --- app/assets/stylesheets/pages/gantts.scss | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/pages/gantts.scss b/app/assets/stylesheets/pages/gantts.scss index 328525017879..8cc0cb0002bb 100644 --- a/app/assets/stylesheets/pages/gantts.scss +++ b/app/assets/stylesheets/pages/gantts.scss @@ -4,30 +4,30 @@ .gantt .rect_has_data { /* blocks that have data */ - fill: #5cb85c; + fill: $green-light; } .gantt .rect_has_data:hover { - fill: #449d44 + fill: $green-normal; } .gantt .rect_has_no_data { /* blocks without data */ - fill: #d9534d; + fill: $red-light; } .gantt .rect_has_no_data:hover { - fill: #c9302c; + fill: $red-normal; } .gantt .tooltip_has_data { /* color of symbol in tooltip if there is data */ - color: #449d44; + color: $green-dark; } .gantt .tooltip_has_no_data { /* color of symbol in tooltip if there is no data */ - color: #c9302c; + color: $red-dark; } .gantt div.tooltip { @@ -36,7 +36,7 @@ padding-left: 0; width: auto; border: 0; - border-left: thin solid #000000; + border-left: 1px solid $border-color; pointer-events: none; line-height: 12px; padding-top: 0; @@ -52,7 +52,7 @@ } .gantt .x_tick_today text { - fill: #ff0000 !important; + fill: $red-dark; } .gantt .ytitle { @@ -66,30 +66,30 @@ } .gantt .axis text { - fill: #777; + fill: $gl-text-color; } .gantt .vert_grid { fill: none; - stroke: #dddddd; + stroke: $border-color; stroke-width: 1px; } .gantt .vert_grid_emph { fill: none; - stroke: #dddddd; + stroke: $border-color; stroke-width: 2px; } .gantt .vert_grid_today { fill: none; - stroke: #ff0000; + stroke: $border-red-light; stroke-width: 2px; } .gantt .horz_grid { fill: none; - stroke: #dddddd; + stroke: $border-color; stroke-width: 1px; } @@ -98,11 +98,9 @@ } .gantt .subheading { - fill: #777; } .gantt .legend { dominant-baseline: middle; - fill: #777; } -- GitLab From eb4f198f80b330d74dff7b386fe7e58aa3d4767f Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 22:11:53 +0100 Subject: [PATCH 08/14] Add start date to issues --- db/migrate/20161116220000_add_start_date_to_issues.rb | 7 +++++++ db/schema.rb | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20161116220000_add_start_date_to_issues.rb diff --git a/db/migrate/20161116220000_add_start_date_to_issues.rb b/db/migrate/20161116220000_add_start_date_to_issues.rb new file mode 100644 index 000000000000..5f7e0a073e56 --- /dev/null +++ b/db/migrate/20161116220000_add_start_date_to_issues.rb @@ -0,0 +1,7 @@ +# rubocop:disable all +class AddStartDateToIssues < ActiveRecord::Migration + def change + add_column :issues, :start_date, :date + add_index :issues, :start_date + end +end diff --git a/db/schema.rb b/db/schema.rb index 886be4520a32..62e68ada0a32 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161109150329) do +ActiveRecord::Schema.define(version: 20161116220000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -490,6 +490,7 @@ t.integer "lock_version" t.text "title_html" t.text "description_html" + t.date "start_date" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -501,6 +502,7 @@ add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree + add_index "issues", ["start_date"], name: "index_issues_on_start_date", using: :btree add_index "issues", ["state"], name: "index_issues_on_state", using: :btree add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} -- GitLab From 8d35d42788f14c087bacafda1bf266ed05fe4004 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 22:29:36 +0100 Subject: [PATCH 09/14] Show start date in issue list --- app/views/projects/issues/_issue.html.haml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index c80210d6ff46..ccac5d93d53f 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -42,6 +42,11 @@ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do = icon('clock-o') = issue.milestone.title + - if issue.start_date + %span +   + = icon('hourglass-start') + = issue.start_date.to_s(:medium) - if issue.due_date %span{class: "#{'cred' if issue.overdue?}"}   -- GitLab From c682f52692b0d5cf651f26165ec3e261720b7190 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Wed, 16 Nov 2016 23:48:39 +0100 Subject: [PATCH 10/14] Gantt: Use start_date, remove issue description parsing --- app/assets/javascripts/gantts/gantt.js.es6 | 66 ++++++---------------- 1 file changed, 17 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 index 1020f2355de4..5d01add5a24e 100644 --- a/app/assets/javascripts/gantts/gantt.js.es6 +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -3,14 +3,6 @@ //= require moment (() => { - // Moment.js locale configuration - const MOMENTJS_LOCALE = '"en"'; - - // You are free to configure any string for gantt start/due dates, - // which are read in your issues descriptions - const GANTT_START_STRING = '"GanttStart: "'; - const GANTT_DUE_STRING = '"GanttDue: "'; - window.gl = window.gl || {}; window.gl.gantt = window.gl.gantt || {}; @@ -20,8 +12,6 @@ gl.gantt.Gantt = Vue.extend({ ], data () { return { - ganttStartString: GANTT_START_STRING, - ganttDueString: GANTT_DUE_STRING, dataset: [] } }, @@ -52,49 +42,27 @@ gl.gantt.Gantt = Vue.extend({ 'link': theIssue.web_url || (this.issueLinkPrefix + '/' + theIssue.id) } - // initializing issue start and due date - var startDate = null - var dueDate = null - - // reading lines from this issue description to search for ganttStartString and ganttDueString - if (theIssue.description != null) { - var lines = theIssue.description.split('\r\n') - for (var j = 0; j < lines.length; j++) { - // this description line starts with the ganttStartString - if (!lines[j].indexOf(this.ganttStartString)) { - // this issue start date for gantt view is set to the appropriate date - startDate = new Date(lines[j].replace(this.ganttStartString, '')) - } - - // this description line starts with the ganttDueString - if (!lines[j].indexOf(this.ganttDueString)) { - // this issue due date for gantt view is set to the appropriate date - dueDate = new Date(lines[j].replace(this.ganttDueString, '')) - } - } - } - - // if start date is still null, we set it from issue creation date - if (startDate == null) { - startDate = new Date(theIssue.created_at) - } + var today = new Date() - // if due date is still null we set it to the issue due date, or to the day after the issue creation date - if (dueDate == null) { - dueDate = theIssue.due_date - if (dueDate == null) { - // the issue due date is unset - dueDate = new Date(theIssue.created_at) - // the due date is calculated to the day after the issue creation date - dueDate.setDate(dueDate.getDate() + 1) - } else { - // the issue due date is used - dueDate = new Date(theIssue.due_date) + // get start date + var startDate = new Date(theIssue.start_date || theIssue.created_at); + + // get due date + // TODO: Use milestone due date if available + var dueDate; + if (theIssue.due_date) { + // the issue due date is used + dueDate = new Date(theIssue.due_date); + } else { + // the issue due date is unset => use start date + 1 day, but not earlier than tomorrow + dueDate = new Date(startDate); + if (dueDate < today) { + dueDate = new Date(today); } + dueDate.setDate(dueDate.getDate() + 1); } // determining if the issue is late or not - var today = new Date() var status = 1 if (dueDate < today) { status = 0 @@ -659,7 +627,7 @@ gl.gantt.Gantt = Vue.extend({ }, mounted: function () { // set Moment.js locale - moment.locale(MOMENTJS_LOCALE) + //moment.locale(MOMENTJS_LOCALE) // no locales included // build the dataset with the issues this.buildDataSet() -- GitLab From 40c30a52da4923c247691f5b4fb27dc2cdf27655 Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Thu, 17 Nov 2016 00:55:40 +0100 Subject: [PATCH 11/14] Add issue start date to issue and board sidebars It would be nice to have a generic date widget, which could handle both start and date fields as well as any possible future date fields. --- .../boards/filters/due_date_filters.js.es6 | 4 ++ .../javascripts/boards/models/issue.js.es6 | 2 + app/assets/javascripts/due_date_select.js.es6 | 53 ++++++++++++------- .../projects/boards/issues_controller.rb | 2 +- app/controllers/projects/issues_controller.rb | 2 +- .../boards/components/_sidebar.html.haml | 1 + .../components/sidebar/_start_date.html.haml | 33 ++++++++++++ app/views/shared/issuable/_sidebar.html.haml | 34 ++++++++++++ 8 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 app/views/projects/boards/components/sidebar/_start_date.html.haml diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 index 9eceac4eddde..fc990d1df11a 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 +++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6 @@ -3,3 +3,7 @@ Vue.filter('due-date', (value) => { const date = new Date(value); return $.datepicker.formatDate('M d, yy', date); }); +Vue.filter('start-date', (value) => { + const date = new Date(value); + return $.datepicker.formatDate('M d, yy', date); +}); diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 21d735e8231e..9b8ca531cbcf 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -4,6 +4,7 @@ class ListIssue { this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; + this.startDate = obj.start_date; this.dueDate = obj.due_date; this.subscribed = obj.subscribed; this.labels = []; @@ -53,6 +54,7 @@ class ListIssue { const data = { issue: { milestone_id: this.milestone ? this.milestone.id : null, + start_date: this.startDate, due_date: this.dueDate, assignee_id: this.assignee ? this.assignee.id : null, label_ids: this.labels.map( (label) => label.id ) diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index fd7f961aab98..711534676e69 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,21 +1,24 @@ /* eslint-disable */ (function(global) { class DueDateSelect { - constructor({ $dropdown, $loading } = {}) { + constructor({ $dropdown, $loading, date_prefix } = { }) { // date prefix is 'due' or 'start' const $dropdownParent = $dropdown.closest('.dropdown'); const $block = $dropdown.closest('.block'); this.$loading = $loading; this.$dropdown = $dropdown; + this.date_prefix = date_prefix || 'due'; this.$dropdownParent = $dropdownParent; - this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); + this.$datePicker = $dropdownParent.find('.js-' + this.date_prefix + '-date-calendar'); this.$block = $block; this.$selectbox = $dropdown.closest('.selectbox'); this.$value = $block.find('.value'); this.$valueContent = $block.find('.value-content'); - this.$sidebarValue = $('.js-due-date-sidebar-value', $block); - this.fieldName = $dropdown.data('field-name'), - this.abilityName = $dropdown.data('ability-name'), - this.issueUpdateURL = $dropdown.data('issue-update') + this.$sidebarValue = $('.js-' + this.date_prefix + '-date-sidebar-value', $block); + this.fieldName = $dropdown.data('field-name'); + this.propName = this.date_prefix + 'Date'; + this.abilityName = $dropdown.data('ability-name'); + this.abilityPropName = this.date_prefix + '_date'; + this.issueUpdateURL = $dropdown.data('issue-update'); this.rawSelectedDate = null; this.displayedDate = null; @@ -42,8 +45,8 @@ defaultDate: $("input[name='" + this.fieldName + "']").val(), altField: "input[name='" + this.fieldName + "']", onSelect: () => { - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val(); + if (this.$dropdown.hasClass('js-issue-boards-' + this.date_prefix + '-date')) { + gl.issueBoards.BoardsStore.detail.issue[this.propName] = $(`input[name='${this.fieldName}']`).val(); this.updateIssueBoardIssue(); } else { return this.saveDueDate(true); @@ -53,11 +56,11 @@ } initRemoveDueDate() { - this.$block.on('click', '.js-remove-due-date', (e) => { + this.$block.on('click', '.js-remove-' + this.date_prefix + '-date', (e) => { e.preventDefault(); - if (this.$dropdown.hasClass('js-issue-boards-due-date')) { - gl.issueBoards.BoardsStore.detail.issue.dueDate = ''; + if (this.$dropdown.hasClass('js-issue-boards-' + this.date_prefix + '-date')) { + gl.issueBoards.BoardsStore.detail.issue[this.propName] = ''; this.updateIssueBoardIssue(); } else { $("input[name='" + this.fieldName + "']").val(''); @@ -84,14 +87,14 @@ let dateObj = new Date(this.rawSelectedDate); this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); } else { - this.displayedDate = 'No due date'; + this.displayedDate = 'No ' + this.date_prefix + ' date'; } } prepSelectedDate() { const datePayload = {}; datePayload[this.abilityName] = {}; - datePayload[this.abilityName].due_date = this.rawSelectedDate; + datePayload[this.abilityName][this.abilityPropName] = this.rawSelectedDate; this.datePayload = datePayload; } @@ -114,8 +117,8 @@ data: this.datePayload, dataType: 'json', beforeSend: () => { - const selectedDateValue = this.datePayload[this.abilityName].due_date; - const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; + const selectedDateValue = this.datePayload[this.abilityName][this.abilityPropName]; + const displayedDateStyle = this.displayedDate !== 'No ' + this.date_prefix + ' date' ? 'bold' : 'no-value'; this.$loading.fadeIn(); @@ -129,8 +132,8 @@ this.$sidebarValue.html(this.displayedDate); return selectedDateValue.length ? - $('.js-remove-due-date-holder').removeClass('hidden') : - $('.js-remove-due-date-holder').addClass('hidden'); + $('.js-remove-' + this.date_prefix + '-date-holder').removeClass('hidden') : + $('.js-remove-' + this.date_prefix + '-date-holder').addClass('hidden'); } }).done((data) => { @@ -168,13 +171,23 @@ } initIssuableSelect() { - const $loading = $('.js-issuable-update .due_date').find('.block-loading').hide(); + const $startLoading = $('.js-issuable-update .start_date').find('.block-loading').hide(); + $('.js-start-date-select').each((i, dropdown) => { + const $dropdown = $(dropdown); + new DueDateSelect({ + $dropdown: $dropdown, + $loading: $startLoading, + date_prefix: 'start', + }); + }); + const $dueLoading = $('.js-issuable-update .due_date').find('.block-loading').hide(); $('.js-due-date-select').each((i, dropdown) => { const $dropdown = $(dropdown); new DueDateSelect({ - $dropdown, - $loading + $dropdown: $dropdown, + $loading: $dueLoading, + date_prefix: 'due', }); }); } diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index dc33e1405f2e..ad102cd870a9 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -73,7 +73,7 @@ def issue_params def serialize_as_json(resource) resource.as_json( labels: true, - only: [:iid, :title, :confidential, :due_date], + only: [:iid, :title, :confidential, :start_date, :due_date], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3f1a1d1c5116..750ea77e638b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -215,7 +215,7 @@ def redirect_old def issue_params params.require(:issue).permit( :title, :assignee_id, :position, :description, :confidential, - :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] + :milestone_id, :start_date, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end end diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index 2125c3387c42..e9145ae6f8fa 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -18,6 +18,7 @@ .js-issuable-update = render "projects/boards/components/sidebar/assignee" = render "projects/boards/components/sidebar/milestone" + = render "projects/boards/components/sidebar/start_date" = render "projects/boards/components/sidebar/due_date" = render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/notifications" diff --git a/app/views/projects/boards/components/sidebar/_start_date.html.haml b/app/views/projects/boards/components/sidebar/_start_date.html.haml new file mode 100644 index 000000000000..30b479f239ae --- /dev/null +++ b/app/views/projects/boards/components/sidebar/_start_date.html.haml @@ -0,0 +1,33 @@ +.block.start_date + .title + Start date + - if can?(current_user, :admin_issue, @project) + = icon("spinner spin", class: "block-loading") + = link_to "Edit", "#", class: "edit-link pull-right" + .value + .value-content + %span.no-value{ "v-if" => "!issue.startDate" } + No start date + %span.bold{ "v-if" => "issue.startDate" } + {{ issue.startDate | start-date }} + - if can?(current_user, :admin_issue, @project) + %span.no-value.js-remove-start-date-holder{ "v-if" => "issue.startDate" } + \- + %a.js-remove-start-date{ href: "#", role: "button" } + remove start date + - if can?(current_user, :admin_issue, @project) + .selectbox + %input{ type: "hidden", + name: "issue[start_date]", + ":value" => "issue.startDate" } + .dropdown + %button.dropdown-menu-toggle.js-start-date-select.js-issue-boards-start-date{ type: 'button', + data: { toggle: 'dropdown', field_name: "issue[start_date]", ability_name: "issue" }, + ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } + %span.dropdown-toggle-text Start date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-start-date + = dropdown_title('Start date') + = dropdown_content do + .js-start-date-calendar + diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 7363ead09ff7..b8f34d917a34 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -73,6 +73,40 @@ = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) + - if issuable.has_attribute?(:start_date) + .block.start_date + .sidebar-collapsed-icon + = icon('hourglass-start') + %span.js-start-date-sidebar-value + = issuable.start_date.try(:to_s, :medium) || 'None' + .title.hide-collapsed + Start date + = icon('spinner spin', class: 'block-loading') + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.hide-collapsed + %span.value-content + - if issuable.start_date + %span.bold= issuable.start_date.to_s(:medium) + - else + %span.no-value No start date + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + %span.no-value.js-remove-start-date-holder{ class: ("hidden" if issuable.start_date.nil?) } + \- + %a.js-remove-start-date{ href: "#", role: "button" } + remove start date + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) + .selectbox.hide-collapsed + = f.hidden_field :start_date, value: issuable.start_date + .dropdown + %button.dropdown-menu-toggle.js-start-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[start_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %span.dropdown-toggle-text Start date + = icon('chevron-down') + .dropdown-menu.dropdown-menu-start-date + = dropdown_title('Start date') + = dropdown_content do + .js-start-date-calendar + - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon -- GitLab From 52fc62dc53ec88f815440662d21b249b168958bb Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Thu, 17 Nov 2016 01:45:15 +0100 Subject: [PATCH 12/14] Gantt: Remove moment.js --- app/assets/javascripts/gantts/gantt.js.es6 | 39 +- .../javascripts/gantts/gantt_bundle.js.es6 | 1 - app/assets/javascripts/lib/moment.js | 7 - vendor/assets/javascripts/moment.js | 551 ------------------ 4 files changed, 4 insertions(+), 594 deletions(-) delete mode 100644 app/assets/javascripts/lib/moment.js delete mode 100644 vendor/assets/javascripts/moment.js diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 index 5d01add5a24e..85b1e6bd49fd 100644 --- a/app/assets/javascripts/gantts/gantt.js.es6 +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -1,6 +1,5 @@ //= require vue //= require d3 -//= require moment (() => { window.gl = window.gl || {}; @@ -133,7 +132,6 @@ gl.gantt.Gantt = Vue.extend({ .style('opacity', 0) var definedBlocks = null - var isDateOnlyFormat = null function chart (selection) { selection.each(function drawGraph (dataset) { @@ -181,9 +179,6 @@ gl.gantt.Gantt = Vue.extend({ var parseDateTime = d3.time.format('%Y-%m-%d %H:%M:%S') var parseDateRegEx = new RegExp(/^\d{4}-\d{2}-\d{2}$/) var parseDateTimeRegEx = new RegExp(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) - if (isDateOnlyFormat === null) { - isDateOnlyFormat = true - } dataset.forEach(function (d) { d.data.forEach(function (d1) { if (!(d1[0] instanceof Date)) { @@ -193,7 +188,6 @@ gl.gantt.Gantt = Vue.extend({ } else if (parseDateTimeRegEx.test(d1[0])) { // d1[0] is date with time data d1[0] = parseDateTime.parse(d1[0]) - isDateOnlyFormat = false } else { throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + '\'YYYY-MM-DD HH:MM:SS\'.') @@ -400,23 +394,10 @@ gl.gantt.Gantt = Vue.extend({ } else { output = '' } - if (isDateOnlyFormat) { - if (d[2] > d3.time.second.offset(d[0], 86400)) { - output = output + moment(parseDate(d[0])).format('l') + - ' - ' + moment(parseDate(d[2])).format('l') - } else { - output = output + moment(parseDate(d[0])).format('l') - } + if (d[2] > d3.time.second.offset(d[0], 86400)) { + output = output + dateFormat(d[0], 'mmm d, yyyy') + ' — ' + dateFormat(d[2], 'mmm d, yyyy'); } else { - if (d[2] > d3.time.second.offset(d[0], 86400)) { - output = output + moment(parseDateTime(d[0])).format('l') + ' ' + - moment(parseDateTime(d[0])).format('LTS') + ' - ' + - moment(parseDateTime(d[2])).format('l') + ' ' + - moment(parseDateTime(d[2])).format('LTS') - } else { - output = output + moment(parseDateTime(d[0])).format('LTS') + ' - ' + - moment(parseDateTime(d[2])).format('LTS') - } + output = output + dateFormat(d[0], 'mmm d, yyyy'); } return output }) @@ -526,16 +507,7 @@ gl.gantt.Gantt = Vue.extend({ } // create subtitle - var subtitleText = '' - if (isDateOnlyFormat) { - subtitleText = moment(parseDate(startDate)).format('MMMM Y') + ' - ' + - moment(parseDate(endDate)).format('MMMM Y') - } else { - subtitleText = moment(parseDateTime(startDate)).format('l') + ' ' + - moment(parseDateTime(startDate)).format('LTS') + ' - ' + - moment(parseDateTime(endDate)).format('l') + ' ' + - moment(parseDateTime(endDate)).format('LTS') - } + var subtitleText = dateFormat(startDate, 'mmmm yyyy') + ' — ' + dateFormat(endDate, 'mmmm yyyy'); svg.select('#g_title') .append('text') @@ -626,9 +598,6 @@ gl.gantt.Gantt = Vue.extend({ } }, mounted: function () { - // set Moment.js locale - //moment.locale(MOMENTJS_LOCALE) // no locales included - // build the dataset with the issues this.buildDataSet() diff --git a/app/assets/javascripts/gantts/gantt_bundle.js.es6 b/app/assets/javascripts/gantts/gantt_bundle.js.es6 index 78d143d03824..f1dfe6103324 100644 --- a/app/assets/javascripts/gantts/gantt_bundle.js.es6 +++ b/app/assets/javascripts/gantts/gantt_bundle.js.es6 @@ -7,7 +7,6 @@ // //= require vue //= require d3 -//= require moment //= require_tree . (function() { diff --git a/app/assets/javascripts/lib/moment.js b/app/assets/javascripts/lib/moment.js deleted file mode 100644 index a434a1e8476a..000000000000 --- a/app/assets/javascripts/lib/moment.js +++ /dev/null @@ -1,7 +0,0 @@ -/* eslint-disable */ - -/*= require moment */ - -(function() { - -}).call(this); diff --git a/vendor/assets/javascripts/moment.js b/vendor/assets/javascripts/moment.js deleted file mode 100644 index 097938e26b11..000000000000 --- a/vendor/assets/javascripts/moment.js +++ /dev/null @@ -1,551 +0,0 @@ -//! moment.js -//! version : 2.16.0 -//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -//! license : MIT -//! momentjs.com -!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return od.apply(null,arguments)} -// This is done to register the method called with moment() -// without creating circular dependencies. -function b(a){od=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){ -// IE8 will treat undefined and null as object if it wasn't for -// input != null -return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a) -// even if its not own property I'd still call it non-empty -return!1;return!0}function f(a){return"number"==typeof value||"[object Number]"===Object.prototype.toString.call(a)}function g(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function h(a,b){var c,d=[];for(c=0;c0)for(c in rd)d=rd[c],e=b[d],p(e)||(a[d]=e);return a} -// Moment prototype object -function r(b){q(this,b),this._d=new Date(null!=b._d?b._d.getTime():NaN), -// Prevent infinite loop in case updateOffset creates new moment -// objects. -sd===!1&&(sd=!0,a.updateOffset(this),sd=!1)}function s(a){return a instanceof r||null!=a&&null!=a._isAMomentObject}function t(a){return a<0?Math.ceil(a)||0:Math.floor(a)}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=t(b)),c} -// compare two arrays, return the number of differences -function v(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;d0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Dd[c]=Dd[c+"s"]=Dd[b]=a}function K(a){return"string"==typeof a?Dd[a]||Dd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)i(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Ed[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Ed[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)} -// MOMENTS -function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d} -// token: 'M' -// padded: ['MM', 2] -// ordinal: 'Mo' -// callback: function () { this.month() + 1 } -function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Id[a]=e),b&&(Id[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Id[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Fd);for(b=0,c=d.length;b=0&&Gd.test(a);)a=a.replace(Gd,c),Gd.lastIndex=0,d-=1;return a}function Z(a,b,c){$d[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return i($d,a)?$d[a](b._strict,b._locale):new RegExp(_(a))} -// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript -function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),f(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments)); -//the Date.UTC function remaps years 0-99 to 1900-1999 -return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b} -// start-of-first-week - start-of-year -function ua(a,b,c){var// first-week day -- which january is always in the first week (4 for iso, 1 for other) -d=7+b-c, -// first-week day local weekday -- which local weekday is fwd -e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1} -//http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday -function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7} -// HELPERS -// LOCALES -function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy} -// MOMENTS -function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")} -// HELPERS -function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:this._weekdays}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=k([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=je.call(this._minWeekdaysParse,g),e!==-1?e:(e=je.call(this._weekdaysParse,g),e!==-1?e:(e=je.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){ -// test the regex -if( -// make the regex if we don't have it already -e=k([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}} -// MOMENTS -function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN; -// behaves the same as moment#day except -// as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) -// as a setter, sunday should belong to the previous week. -if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(i(this,"_weekdaysRegex")||(this._weekdaysRegex=ue),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(i(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ve),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(i(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(i(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=we),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++) -// make the regex if we don't have it already -c=k([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for( -// Sorting makes sure if one weekday (or abbr) is a prefix of another it -// will match the longer piece. -g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")} -// FORMATTING -function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})} -// PARSING -function Ua(a,b){return b._meridiemParse} -// LOCALES -function Va(a){ -// IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays -// Using charAt should be more compatible. -return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a} -// pick the locale from the array -// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each -// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root -function Ya(a){for(var b,c,d,e,f=0;f0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1) -//the next array item is better than a shallower substring of this one -break;b--}f++}return null}function Za(a){var b=null; -// TODO: Find a better way to register and load all the locales in Node -if(!Be[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=xe._abbr,require("./locale/"+a), -// because defineLocale currently also sets the global locale, we -// want to undo that for lazy loaded locales -$a(b)}catch(a){}return Be[a]} -// This function will load locale and then set the global locale. If -// no arguments are passed in, it will simply return the current global -// locale key. -function $a(a,b){var c; -// moment.duration._locale = moment._locale = data; -return a&&(c=p(b)?bb(a):_a(a,b),c&&(xe=c)),xe._abbr}function _a(a,b){if(null!==b){var c=Ae;if(b.abbr=a,null!=Be[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Be[a]._config;else if(null!=b.parentLocale){if(null==Be[b.parentLocale])return Ce[b.parentLocale]||(Ce[b.parentLocale]=[]),Ce[b.parentLocale].push({name:a,config:b}),null;c=Be[b.parentLocale]._config} -// backwards compat for now: also set the locale -// make sure we set the locale AFTER all child locales have been -// created, so we won't end up with the child locale set. -return Be[a]=new C(B(c,b)),Ce[a]&&Ce[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Be[a]} -// useful for testing -return delete Be[a],null}function ab(a,b){if(null!=b){var c,d=Ae; -// MERGE -null!=Be[a]&&(d=Be[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Be[a],Be[a]=c, -// backwards compat for now: also set the locale -$a(a)}else -// pass null for config to unupdate, useful for tests -null!=Be[a]&&(null!=Be[a].parentLocale?Be[a]=Be[a].parentLocale:null!=Be[a]&&delete Be[a]);return Be[a]} -// returns locale data -function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return xe;if(!c(a)){if( -//short-circuit everything else -b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return wd(Be)}function db(a){var b,c=a._a;return c&&m(a).overflow===-2&&(b=c[be]<0||c[be]>11?be:c[ce]<1||c[ce]>ea(c[ae],c[be])?ce:c[de]<0||c[de]>24||24===c[de]&&(0!==c[ee]||0!==c[fe]||0!==c[ge])?de:c[ee]<0||c[ee]>59?ee:c[fe]<0||c[fe]>59?fe:c[ge]<0||c[ge]>999?ge:-1,m(a)._overflowDayOfYear&&(bce)&&(b=ce),m(a)._overflowWeeks&&b===-1&&(b=he),m(a)._overflowWeekday&&b===-1&&(b=ie),m(a).overflow=b),a} -// date from iso format -function eb(a){var b,c,d,e,f,g,h=a._i,i=De.exec(h)||Ee.exec(h);if(i){for(m(a).iso=!0,b=0,c=Ge.length;bpa(e)&&(m(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[be]=c.getUTCMonth(),a._a[ce]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b]; -// Zero out whatever was not defaulted, including time -for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b]; -// Check for 24:00:00.000 -24===a._a[de]&&0===a._a[ee]&&0===a._a[fe]&&0===a._a[ge]&&(a._nextDay=!0,a._a[de]=0),a._d=(a._useUTC?ta:sa).apply(null,f), -// Apply timezone offset from input. The actual utcOffset can be changed -// with parseZone. -null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[de]=24)}}function jb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4, -// TODO: We need to take the current isoWeekYear, but that depends on -// how we interpret now (local, utc, fixed offset). So create -// a now version of current config (take local/utc/offset flags, and -// create now). -c=gb(b.GG,a._a[ae],wa(sb(),1,4).year),d=gb(b.W,1),e=gb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(sb(),f,g);c=gb(b.gg,a._a[ae],j.year), -// Default to current week. -d=gb(b.w,j.week),null!=b.d?( -// weekday -- low day numbers are considered next week -e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?( -// local weekday -- counting starts from begining of week -e=b.e+f,(b.e<0||b.e>6)&&(i=!0)): -// default to begining of week -e=f}d<1||d>xa(c,f,g)?m(a)._overflowWeeks=!0:null!=i?m(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ae]=h.year,a._dayOfYear=h.dayOfYear)} -// date from string and format string -function kb(b){ -// TODO: Move this to another part of the creation flow to prevent circular deps -if(b._f===a.ISO_8601)return void eb(b);b._a=[],m(b).empty=!0; -// This array is used to make a Date, either with `new Date` or `Date.UTC` -var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Fd)||[],c=0;c0&&m(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length), -// don't parse if it's not a known token -Id[f]?(d?m(b).empty=!1:m(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&m(b).unusedTokens.push(f); -// add remaining unparsed input length to the string -m(b).charsLeftOver=i-j,h.length>0&&m(b).unusedInput.push(h), -// clear _12h flag if hour is <= 12 -b._a[de]<=12&&m(b).bigHour===!0&&b._a[de]>0&&(m(b).bigHour=void 0),m(b).parsedDateParts=b._a.slice(0),m(b).meridiem=b._meridiem, -// handle meridiem -b._a[de]=lb(b._locale,b._a[de],b._meridiem),ib(b),db(b)}function lb(a,b,c){var d; -// Fallback -return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b} -// date from string and array of format strings -function mb(a){var b,c,d,e,f;if(0===a._f.length)return m(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;e -// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset -// +0200, so we adjust the time as needed, to be valid. -// -// Keeping the time actually adds/subtracts (one hour) -// from the actual represented time. That is why we call updateOffset -// a second time. In case it wants us to change the offset again -// _changeInProgress == true case, then we have to adjust, because -// there is no such time in the given timezone. -function Db(b,c){var d,e=this._offset||0;if(!this.isValid())return null!=b?this:NaN;if(null!=b){if("string"==typeof b){if(b=Ab(Xd,b),null===b)return this}else Math.abs(b)<16&&(b=60*b);return!this._isUTC&&c&&(d=Cb(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Tb(this,Ob(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?e:Cb(this)}function Eb(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function Fb(a){return this.utcOffset(0,a)}function Gb(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Cb(this),"m")),this}function Hb(){if(null!=this._tzm)this.utcOffset(this._tzm);else if("string"==typeof this._i){var a=Ab(Wd,this._i);null!=a?this.utcOffset(a):this.utcOffset(0,!0)}return this}function Ib(a){return!!this.isValid()&&(a=a?sb(a).utcOffset():0,(this.utcOffset()-a)%60===0)}function Jb(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Kb(){if(!p(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=pb(a),a._a){var b=a._isUTC?k(a._a):sb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Lb(){return!!this.isValid()&&!this._isUTC}function Mb(){return!!this.isValid()&&this._isUTC}function Nb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Ob(a,b){var c,d,e,g=a, -// matching against regexp is expensive, do it on demand -h=null;// checks for null or undefined -return xb(a)?g={ms:a._milliseconds,d:a._days,M:a._months}:f(a)?(g={},b?g[b]=a:g.milliseconds=a):(h=Ne.exec(a))?(c="-"===h[1]?-1:1,g={y:0,d:u(h[ce])*c,h:u(h[de])*c,m:u(h[ee])*c,s:u(h[fe])*c,ms:u(yb(1e3*h[ge]))*c}):(h=Oe.exec(a))?(c="-"===h[1]?-1:1,g={y:Pb(h[2],c),M:Pb(h[3],c),w:Pb(h[4],c),d:Pb(h[5],c),h:Pb(h[6],c),m:Pb(h[7],c),s:Pb(h[8],c)}):null==g?g={}:"object"==typeof g&&("from"in g||"to"in g)&&(e=Rb(sb(g.from),sb(g.to)),g={},g.ms=e.milliseconds,g.M=e.months),d=new wb(g),xb(a)&&i(a,"_locale")&&(d._locale=a._locale),d}function Pb(a,b){ -// We'd normally use ~~inp for this, but unfortunately it also -// converts floats to ints. -// inp may be undefined, so careful calling replace on it. -var c=a&&parseFloat(a.replace(",",".")); -// apply sign while we're at it -return(isNaN(c)?0:c)*b}function Qb(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Rb(a,b){var c;return a.isValid()&&b.isValid()?(b=Bb(b,a),a.isBefore(b)?c=Qb(a,b):(c=Qb(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}} -// TODO: remove 'name' arg after deprecation is removed -function Sb(a,b){return function(c,d){var e,f; -//invert the arguments, but complain about it -return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ob(c,d),Tb(this,e,a),this}}function Tb(b,c,d,e){var f=c._milliseconds,g=yb(c._days),h=yb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Ub(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Vb(b,c){ -// We want to compare the start of today, vs this. -// Getting start-of-today depends on whether we're local/utc/offset or not. -var d=b||sb(),e=Bb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,sb(d)))}function Wb(){return new r(this)}function Xb(a,b){var c=s(a)?a:sb(a);return!(!this.isValid()||!c.isValid())&&(b=K(p(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()f&&(b=f),Fc.call(this,a,b,c,d,e))}function Fc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this} -// MOMENTS -function Gc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)} -// HELPERS -// MOMENTS -function Hc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Ic(a,b){b[ge]=u(1e3*("0."+a))} -// MOMENTS -function Jc(){return this._isUTC?"UTC":""}function Kc(){return this._isUTC?"Coordinated Universal Time":""}function Lc(a){return sb(1e3*a)}function Mc(){return sb.apply(null,arguments).parseZone()}function Nc(a){return a}function Oc(a,b,c,d){var e=bb(),f=k().set(d,b);return e[c](f,a)}function Pc(a,b,c){if(f(a)&&(b=a,a=void 0),a=a||"",null!=b)return Oc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Oc(a,d,c,"month");return e} -// () -// (5) -// (fmt, 5) -// (fmt) -// (true) -// (true, 5) -// (true, fmt, 5) -// (true, fmt) -function Qc(a,b,c,d){"boolean"==typeof a?(f(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,f(b)&&(c=b,b=void 0),b=b||"");var e=bb(),g=a?e._week.dow:0;if(null!=c)return Oc(b,(c+g)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Oc(b,(h+g)%7,d,"day");return i}function Rc(a,b){return Pc(a,b,"months")}function Sc(a,b){return Pc(a,b,"monthsShort")}function Tc(a,b,c){return Qc(a,b,c,"weekdays")}function Uc(a,b,c){return Qc(a,b,c,"weekdaysShort")}function Vc(a,b,c){return Qc(a,b,c,"weekdaysMin")}function Wc(){var a=this._data;return this._milliseconds=Ze(this._milliseconds),this._days=Ze(this._days),this._months=Ze(this._months),a.milliseconds=Ze(a.milliseconds),a.seconds=Ze(a.seconds),a.minutes=Ze(a.minutes),a.hours=Ze(a.hours),a.months=Ze(a.months),a.years=Ze(a.years),this}function Xc(a,b,c,d){var e=Ob(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()} -// supports only 2.0-style add(1, 's') or add(duration) -function Yc(a,b){return Xc(this,a,b,1)} -// supports only 2.0-style subtract(1, 's') or subtract(duration) -function Zc(a,b){return Xc(this,a,b,-1)}function $c(a){return a<0?Math.floor(a):Math.ceil(a)}function _c(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data; -// if we have a mix of positive and negative values, bubble down first -// check: https://github.com/moment/moment/issues/2166 -// The following code bubbles up values, see the tests for -// examples of what that means. -// convert days to months -// 12 months -> 1 year -return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*$c(bd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ad(g)),h+=e,g-=$c(bd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ad(a){ -// 400 years have 146097 days (taking into account leap year rules) -// 400 years have 12 months === 4800 -return 4800*a/146097}function bd(a){ -// the reverse of daysToMonths -return 146097*a/4800}function cd(a){var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ad(b),"month"===a?c:c/12;switch( -// handle milliseconds separately because of floating point math errors (issue #1867) -b=this._days+Math.round(bd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3; -// Math.floor prevents floating point math errors here -case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}} -// TODO: Use this.as('ms')? -function dd(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)}function ed(a){return function(){return this.as(a)}}function fd(a){return a=K(a),this[a+"s"]()}function gd(a){return function(){return this._data[a]}}function hd(){return t(this.days()/7)} -// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize -function id(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function jd(a,b,c){var d=Ob(a).abs(),e=of(d.as("s")),f=of(d.as("m")),g=of(d.as("h")),h=of(d.as("d")),i=of(d.as("M")),j=of(d.as("y")),k=e0,k[4]=c,id.apply(null,k)} -// This function allows you to set the rounding function for relative time strings -function kd(a){return void 0===a?of:"function"==typeof a&&(of=a,!0)} -// This function allows you to set a threshold for relative time strings -function ld(a,b){return void 0!==pf[a]&&(void 0===b?pf[a]:(pf[a]=b,!0))}function md(a){var b=this.localeData(),c=jd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function nd(){ -// for ISO strings we do not use the normal bubbling rules: -// * milliseconds bubble up until they become hours -// * days do not bubble at all -// * months bubble up until they become years -// This is because there is no context-free conversion between hours and days -// (think of clock changes) -// and also not between days and months (28-31 days per month) -var a,b,c,d=qf(this._milliseconds)/1e3,e=qf(this._days),f=qf(this._months); -// 3600 seconds -> 60 minutes -> 1 hour -a=t(d/60),b=t(a/60),d%=60,a%=60, -// 12 months -> 1 year -c=t(f/12),f%=12; -// inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js -var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var od,pd;pd=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d68?1900:2e3)}; -// MOMENTS -var pe=O("FullYear",!0); -// FORMATTING -U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"), -// ALIASES -J("week","w"),J("isoWeek","W"), -// PRIORITIES -M("week",5),M("isoWeek",5), -// PARSING -Z("w",Od),Z("ww",Od,Kd),Z("W",Od),Z("WW",Od,Kd),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var qe={dow:0,// Sunday is the first day of the week. -doy:6}; -// FORMATTING -U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"), -// ALIASES -J("day","d"),J("weekday","e"),J("isoWeekday","E"), -// PRIORITY -M("day",11),M("weekday",11),M("isoWeekday",11), -// PARSING -Z("d",Od),Z("e",Od),Z("E",Od),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict); -// if we didn't get a weekday name, mark the date as invalid -null!=e?b.d=e:m(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)}); -// LOCALES -var re="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),se="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),te="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ue=Zd,ve=Zd,we=Zd;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1), -// ALIASES -J("hour","h"), -// PRIORITY -M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Od),Z("h",Od),Z("HH",Od,Kd),Z("hh",Od,Kd),Z("hmm",Pd),Z("hmmss",Qd),Z("Hmm",Pd),Z("Hmmss",Qd),ba(["H","HH"],de),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[de]=u(a),m(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d)),m(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e)),m(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[de]=u(a.substr(0,d)),b[ee]=u(a.substr(d,2)),b[fe]=u(a.substr(e))});var xe,ye=/[ap]\.?m?\.?/i,ze=O("Hours",!0),Ae={calendar:xd,longDateFormat:yd,invalidDate:zd,ordinal:Ad,ordinalParse:Bd,relativeTime:Cd,months:le,monthsShort:me,week:qe,weekdays:re,weekdaysMin:te,weekdaysShort:se,meridiemParse:ye},Be={},Ce={},De=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ee=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Fe=/Z|[+-]\d\d(?::?\d\d)?/,Ge=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/], -// YYYYMM is NOT allowed by the standard -["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],He=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Ie=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=x("value provided is not in a recognized ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}), -// constant that refers to the ISO standard -a.ISO_8601=function(){};var Je=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=sb.apply(null,arguments);return this.isValid()&&a.isValid()?athis?this:a:o()}),Le=function(){return Date.now?Date.now():+new Date};zb("Z",":"),zb("ZZ",""), -// PARSING -Z("Z",Xd),Z("ZZ",Xd),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ab(Xd,a)}); -// HELPERS -// timezone chunker -// '+10:00' > ['10', '00'] -// '-1530' > ['-15', '30'] -var Me=/([\+\-]|\d\d)/gi; -// HOOKS -// This function will be called whenever a moment is mutated. -// It is intended to keep the offset in sync with the timezone. -a.updateOffset=function(){}; -// ASP.NET json date format regex -var Ne=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Oe=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Ob.fn=wb.prototype;var Pe=Sb(1,"add"),Qe=Sb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Re=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)}); -// FORMATTING -U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),zc("gggg","weekYear"),zc("ggggg","weekYear"),zc("GGGG","isoWeekYear"),zc("GGGGG","isoWeekYear"), -// ALIASES -J("weekYear","gg"),J("isoWeekYear","GG"), -// PRIORITY -M("weekYear",1),M("isoWeekYear",1), -// PARSING -Z("G",Vd),Z("g",Vd),Z("GG",Od,Kd),Z("gg",Od,Kd),Z("GGGG",Sd,Md),Z("gggg",Sd,Md),Z("GGGGG",Td,Nd),Z("ggggg",Td,Nd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}), -// FORMATTING -U("Q",0,"Qo","quarter"), -// ALIASES -J("quarter","Q"), -// PRIORITY -M("quarter",7), -// PARSING -Z("Q",Jd),ba("Q",function(a,b){b[be]=3*(u(a)-1)}), -// FORMATTING -U("D",["DD",2],"Do","date"), -// ALIASES -J("date","D"), -// PRIOROITY -M("date",9), -// PARSING -Z("D",Od),Z("DD",Od,Kd),Z("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),ba(["D","DD"],ce),ba("Do",function(a,b){b[ce]=u(a.match(Od)[0],10)}); -// MOMENTS -var Se=O("Date",!0); -// FORMATTING -U("DDD",["DDDD",3],"DDDo","dayOfYear"), -// ALIASES -J("dayOfYear","DDD"), -// PRIORITY -M("dayOfYear",4), -// PARSING -Z("DDD",Rd),Z("DDDD",Ld),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}), -// FORMATTING -U("m",["mm",2],0,"minute"), -// ALIASES -J("minute","m"), -// PRIORITY -M("minute",14), -// PARSING -Z("m",Od),Z("mm",Od,Kd),ba(["m","mm"],ee); -// MOMENTS -var Te=O("Minutes",!1); -// FORMATTING -U("s",["ss",2],0,"second"), -// ALIASES -J("second","s"), -// PRIORITY -M("second",15), -// PARSING -Z("s",Od),Z("ss",Od,Kd),ba(["s","ss"],fe); -// MOMENTS -var Ue=O("Seconds",!1); -// FORMATTING -U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}), -// ALIASES -J("millisecond","ms"), -// PRIORITY -M("millisecond",16), -// PARSING -Z("S",Rd,Jd),Z("SS",Rd,Kd),Z("SSS",Rd,Ld);var Ve;for(Ve="SSSS";Ve.length<=9;Ve+="S")Z(Ve,Ud);for(Ve="S";Ve.length<=9;Ve+="S")ba(Ve,Ic); -// MOMENTS -var We=O("Milliseconds",!1); -// FORMATTING -U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var Xe=r.prototype;Xe.add=Pe,Xe.calendar=Vb,Xe.clone=Wb,Xe.diff=bc,Xe.endOf=oc,Xe.format=gc,Xe.from=hc,Xe.fromNow=ic,Xe.to=jc,Xe.toNow=kc,Xe.get=R,Xe.invalidAt=xc,Xe.isAfter=Xb,Xe.isBefore=Yb,Xe.isBetween=Zb,Xe.isSame=$b,Xe.isSameOrAfter=_b,Xe.isSameOrBefore=ac,Xe.isValid=vc,Xe.lang=Re,Xe.locale=lc,Xe.localeData=mc,Xe.max=Ke,Xe.min=Je,Xe.parsingFlags=wc,Xe.set=S,Xe.startOf=nc,Xe.subtract=Qe,Xe.toArray=sc,Xe.toObject=tc,Xe.toDate=rc,Xe.toISOString=ec,Xe.inspect=fc,Xe.toJSON=uc,Xe.toString=dc,Xe.unix=qc,Xe.valueOf=pc,Xe.creationData=yc, -// Year -Xe.year=pe,Xe.isLeapYear=ra, -// Week Year -Xe.weekYear=Ac,Xe.isoWeekYear=Bc, -// Quarter -Xe.quarter=Xe.quarters=Gc, -// Month -Xe.month=ka,Xe.daysInMonth=la, -// Week -Xe.week=Xe.weeks=Ba,Xe.isoWeek=Xe.isoWeeks=Ca,Xe.weeksInYear=Dc,Xe.isoWeeksInYear=Cc, -// Day -Xe.date=Se,Xe.day=Xe.days=Ka,Xe.weekday=La,Xe.isoWeekday=Ma,Xe.dayOfYear=Hc, -// Hour -Xe.hour=Xe.hours=ze, -// Minute -Xe.minute=Xe.minutes=Te, -// Second -Xe.second=Xe.seconds=Ue, -// Millisecond -Xe.millisecond=Xe.milliseconds=We, -// Offset -Xe.utcOffset=Db,Xe.utc=Fb,Xe.local=Gb,Xe.parseZone=Hb,Xe.hasAlignedHourOffset=Ib,Xe.isDST=Jb,Xe.isLocal=Lb,Xe.isUtcOffset=Mb,Xe.isUtc=Nb,Xe.isUTC=Nb, -// Timezone -Xe.zoneAbbr=Jc,Xe.zoneName=Kc, -// Deprecations -Xe.dates=x("dates accessor is deprecated. Use date instead.",Se),Xe.months=x("months accessor is deprecated. Use month instead",ka),Xe.years=x("years accessor is deprecated. Use year instead",pe),Xe.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Eb),Xe.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Kb);var Ye=C.prototype;Ye.calendar=D,Ye.longDateFormat=E,Ye.invalidDate=F,Ye.ordinal=G,Ye.preparse=Nc,Ye.postformat=Nc,Ye.relativeTime=H,Ye.pastFuture=I,Ye.set=A, -// Month -Ye.months=fa,Ye.monthsShort=ga,Ye.monthsParse=ia,Ye.monthsRegex=na,Ye.monthsShortRegex=ma, -// Week -Ye.week=ya,Ye.firstDayOfYear=Aa,Ye.firstDayOfWeek=za, -// Day of Week -Ye.weekdays=Fa,Ye.weekdaysMin=Ha,Ye.weekdaysShort=Ga,Ye.weekdaysParse=Ja,Ye.weekdaysRegex=Na,Ye.weekdaysShortRegex=Oa,Ye.weekdaysMinRegex=Pa, -// Hours -Ye.isPM=Va,Ye.meridiem=Wa,$a("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}), -// Side effect imports -a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var Ze=Math.abs,$e=ed("ms"),_e=ed("s"),af=ed("m"),bf=ed("h"),cf=ed("d"),df=ed("w"),ef=ed("M"),ff=ed("y"),gf=gd("milliseconds"),hf=gd("seconds"),jf=gd("minutes"),kf=gd("hours"),lf=gd("days"),mf=gd("months"),nf=gd("years"),of=Math.round,pf={s:45,// seconds to minute -m:45,// minutes to hour -h:22,// hours to day -d:26,// days to month -M:11},qf=Math.abs,rf=wb.prototype; -// Deprecations -// Side effect imports -// FORMATTING -// PARSING -// Side effect imports -return rf.abs=Wc,rf.add=Yc,rf.subtract=Zc,rf.as=cd,rf.asMilliseconds=$e,rf.asSeconds=_e,rf.asMinutes=af,rf.asHours=bf,rf.asDays=cf,rf.asWeeks=df,rf.asMonths=ef,rf.asYears=ff,rf.valueOf=dd,rf._bubble=_c,rf.get=fd,rf.milliseconds=gf,rf.seconds=hf,rf.minutes=jf,rf.hours=kf,rf.days=lf,rf.weeks=hd,rf.months=mf,rf.years=nf,rf.humanize=md,rf.toISOString=nd,rf.toString=nd,rf.toJSON=nd,rf.locale=lc,rf.localeData=mc,rf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",nd),rf.lang=Re,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Vd),Z("X",Yd),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.16.0",b(sb),a.fn=Xe,a.min=ub,a.max=vb,a.now=Le,a.utc=k,a.unix=Lc,a.months=Rc,a.isDate=g,a.locale=$a,a.invalid=o,a.duration=Ob,a.isMoment=s,a.weekdays=Tc,a.parseZone=Mc,a.localeData=bb,a.isDuration=xb,a.monthsShort=Sc,a.weekdaysMin=Vc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Uc,a.normalizeUnits=K,a.relativeTimeRounding=kd,a.relativeTimeThreshold=ld,a.calendarFormat=Ub,a.prototype=Xe,a}); \ No newline at end of file -- GitLab From bcc28785e0b545d88d498809b52c6c6c445d520c Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Thu, 17 Nov 2016 01:51:00 +0100 Subject: [PATCH 13/14] Gantt: Remove useless tooltip --- app/assets/javascripts/gantts/gantt.js.es6 | 37 ---------------------- 1 file changed, 37 deletions(-) diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 index 85b1e6bd49fd..b8782bd80eb0 100644 --- a/app/assets/javascripts/gantts/gantt.js.es6 +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -126,11 +126,6 @@ gl.gantt.Gantt = Vue.extend({ // "curDisplayFirstDataset+maxDisplayDatasets" var curDisplayFirstDataset = 0 - // global div for tooltip - var div = d3.select('body').append('div') - .attr('class', 'tooltip') - .style('opacity', 0) - var definedBlocks = null function chart (selection) { @@ -382,38 +377,6 @@ gl.gantt.Gantt = Vue.extend({ } return 'rect_has_no_data' }) - .on('mouseover', function (d, i) { - var matrix = this.getScreenCTM().translate(+this.getAttribute('x'), +this.getAttribute('y')) - div.transition() - .duration(200) - .style('opacity', 0.9) - div.html(function () { - var output = '' - if (d[1] === 1) { - output = '' - } else { - output = '' - } - if (d[2] > d3.time.second.offset(d[0], 86400)) { - output = output + dateFormat(d[0], 'mmm d, yyyy') + ' — ' + dateFormat(d[2], 'mmm d, yyyy'); - } else { - output = output + dateFormat(d[0], 'mmm d, yyyy'); - } - return output - }) - .style('left', function () { - return window.pageXOffset + matrix.e + 'px' - }) - .style('top', function () { - return window.pageYOffset + matrix.f - 11 + 'px' - }) - .style('height', dataHeight + 11 + 'px') - }) - .on('mouseout', function () { - div.transition() - .duration(500) - .style('opacity', 0) - }) // rework ticks and grid for better visual structure function isYear (t) { -- GitLab From 703c343b6bae5976105ee541dc125815020f74eb Mon Sep 17 00:00:00 2001 From: Josef Kufner Date: Thu, 17 Nov 2016 02:01:24 +0100 Subject: [PATCH 14/14] Gantt: Fix indenting in gantt.js (whitespace change only) --- app/assets/javascripts/gantts/gantt.js.es6 | 1010 ++++++++++---------- 1 file changed, 505 insertions(+), 505 deletions(-) diff --git a/app/assets/javascripts/gantts/gantt.js.es6 b/app/assets/javascripts/gantts/gantt.js.es6 index b8782bd80eb0..5cda0ff26d23 100644 --- a/app/assets/javascripts/gantts/gantt.js.es6 +++ b/app/assets/javascripts/gantts/gantt.js.es6 @@ -5,569 +5,569 @@ window.gl = window.gl || {}; window.gl.gantt = window.gl.gantt || {}; -gl.gantt.Gantt = Vue.extend({ - props: [ - 'issues', 'issueLinkPrefix' - ], - data () { - return { - dataset: [] - } - }, - watch: { - issues: function (newIssues) { - this.buildDataSet() - this.refreshChart() - } - }, - methods: { - buildDataSet: function (event) { - // clearing the dataset to build it from issues list - this.dataset = [] - - // looping on issues - for (var i = this.issues.length - 1; i >= 0; i--) { - var theIssue = this.issues[i] - - // stripping issue title to the first 40 characters - var title = "#" + theIssue.id + ": " + theIssue.title - if (title.length > 40) { - title = title.substring(0, 40) + '...' - } - - // creating the dataset - var aDataset = { - 'measure': title, - 'link': theIssue.web_url || (this.issueLinkPrefix + '/' + theIssue.id) - } - - var today = new Date() - - // get start date - var startDate = new Date(theIssue.start_date || theIssue.created_at); - - // get due date - // TODO: Use milestone due date if available - var dueDate; - if (theIssue.due_date) { - // the issue due date is used - dueDate = new Date(theIssue.due_date); - } else { - // the issue due date is unset => use start date + 1 day, but not earlier than tomorrow - dueDate = new Date(startDate); - if (dueDate < today) { - dueDate = new Date(today); - } - dueDate.setDate(dueDate.getDate() + 1); - } - - // determining if the issue is late or not - var status = 1 - if (dueDate < today) { - status = 0 - } - - // formatting start and due dates for visavail - var fDueDate = dueDate.getUTCFullYear() + '-' + this.pad(dueDate.getUTCMonth() + 1) + '-' + this.pad(dueDate.getUTCDate()) - var fStartDate = startDate.getUTCFullYear() + '-' + this.pad(startDate.getUTCMonth() + 1) + '-' + this.pad(startDate.getUTCDate()) - aDataset.data = [ - [ fStartDate, status, fDueDate ] - ] - - // adding the dataset built to the main dataset list - this.dataset.push(aDataset) + gl.gantt.Gantt = Vue.extend({ + props: [ + 'issues', 'issueLinkPrefix' + ], + data () { + return { + dataset: [] } }, - // thank you Florian Roscheck for this, you made an awesome work I only needed to tweak a little - visavailChart: function (event) { - // define chart layout - var margin = { - // top margin includes title and legend - top: 70, - - // right margin should provide space for last horz. axis title - right: 40, - - bottom: 20, - - // left margin should provide space for y axis titles - left: 250 + watch: { + issues: function (newIssues) { + this.buildDataSet() + this.refreshChart() } - - // height of horizontal data bars - var dataHeight = 18 - - // spacing between horizontal data bars - var lineSpacing = 14 - - // vertical space for heading - var paddingTopHeading = -50 - - // vertical overhang of vertical grid lines on bottom - var paddingBottom = 10 - - // space for y axis titles - var paddingLeft = -250 - - var width = window.innerWidth - margin.left - margin.right - 20 - - // title of chart is drawn or not (default: yes) - var drawTitle = 0 - - // year ticks to be emphasized or not (default: yes) - var emphasizeYearTicks = 1 - - // define chart pagination - // max. no. of datasets that is displayed, 0: all (default: all) - var maxDisplayDatasets = 0 - - // dataset that is displayed first in the current - // display, chart will show datasets "curDisplayFirstDataset" to - // "curDisplayFirstDataset+maxDisplayDatasets" - var curDisplayFirstDataset = 0 - - var definedBlocks = null - - function chart (selection) { - selection.each(function drawGraph (dataset) { - // check which subset of datasets have to be displayed - var maxPages = 0 - var startSet - var endSet - if (maxDisplayDatasets !== 0) { - startSet = curDisplayFirstDataset - if (curDisplayFirstDataset + maxDisplayDatasets > dataset.length) { - endSet = dataset.length - } else { - endSet = curDisplayFirstDataset + maxDisplayDatasets - } - maxPages = Math.ceil(dataset.length / maxDisplayDatasets) + }, + methods: { + buildDataSet: function (event) { + // clearing the dataset to build it from issues list + this.dataset = [] + + // looping on issues + for (var i = this.issues.length - 1; i >= 0; i--) { + var theIssue = this.issues[i] + + // stripping issue title to the first 40 characters + var title = "#" + theIssue.id + ": " + theIssue.title + if (title.length > 40) { + title = title.substring(0, 40) + '...' + } + + // creating the dataset + var aDataset = { + 'measure': title, + 'link': theIssue.web_url || (this.issueLinkPrefix + '/' + theIssue.id) + } + + var today = new Date() + + // get start date + var startDate = new Date(theIssue.start_date || theIssue.created_at); + + // get due date + // TODO: Use milestone due date if available + var dueDate; + if (theIssue.due_date) { + // the issue due date is used + dueDate = new Date(theIssue.due_date); } else { - startSet = 0 - endSet = dataset.length + // the issue due date is unset => use start date + 1 day, but not earlier than tomorrow + dueDate = new Date(startDate); + if (dueDate < today) { + dueDate = new Date(today); + } + dueDate.setDate(dueDate.getDate() + 1); } - - // append data attribute in HTML for pagination interface - selection.attr('data-max-pages', maxPages) - - var noOfDatasets = endSet - startSet - var height = dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 - - // check how data is arranged - if (definedBlocks === null) { - definedBlocks = 0 - for (var i = 0; i < dataset.length; i++) { - if (dataset[i].data[0].length === 3) { - definedBlocks = 1 - break + + // determining if the issue is late or not + var status = 1 + if (dueDate < today) { + status = 0 + } + + // formatting start and due dates for visavail + var fDueDate = dueDate.getUTCFullYear() + '-' + this.pad(dueDate.getUTCMonth() + 1) + '-' + this.pad(dueDate.getUTCDate()) + var fStartDate = startDate.getUTCFullYear() + '-' + this.pad(startDate.getUTCMonth() + 1) + '-' + this.pad(startDate.getUTCDate()) + aDataset.data = [ + [ fStartDate, status, fDueDate ] + ] + + // adding the dataset built to the main dataset list + this.dataset.push(aDataset) + } + }, + // thank you Florian Roscheck for this, you made an awesome work I only needed to tweak a little + visavailChart: function (event) { + // define chart layout + var margin = { + // top margin includes title and legend + top: 70, + + // right margin should provide space for last horz. axis title + right: 40, + + bottom: 20, + + // left margin should provide space for y axis titles + left: 250 + } + + // height of horizontal data bars + var dataHeight = 18 + + // spacing between horizontal data bars + var lineSpacing = 14 + + // vertical space for heading + var paddingTopHeading = -50 + + // vertical overhang of vertical grid lines on bottom + var paddingBottom = 10 + + // space for y axis titles + var paddingLeft = -250 + + var width = window.innerWidth - margin.left - margin.right - 20 + + // title of chart is drawn or not (default: yes) + var drawTitle = 0 + + // year ticks to be emphasized or not (default: yes) + var emphasizeYearTicks = 1 + + // define chart pagination + // max. no. of datasets that is displayed, 0: all (default: all) + var maxDisplayDatasets = 0 + + // dataset that is displayed first in the current + // display, chart will show datasets "curDisplayFirstDataset" to + // "curDisplayFirstDataset+maxDisplayDatasets" + var curDisplayFirstDataset = 0 + + var definedBlocks = null + + function chart (selection) { + selection.each(function drawGraph (dataset) { + // check which subset of datasets have to be displayed + var maxPages = 0 + var startSet + var endSet + if (maxDisplayDatasets !== 0) { + startSet = curDisplayFirstDataset + if (curDisplayFirstDataset + maxDisplayDatasets > dataset.length) { + endSet = dataset.length } else { - if (definedBlocks) { - throw new Error('Detected different data formats in input data. Format can either be ' + - 'continuous data format or time gap data format but not both.') - } + endSet = curDisplayFirstDataset + maxDisplayDatasets } + maxPages = Math.ceil(dataset.length / maxDisplayDatasets) + } else { + startSet = 0 + endSet = dataset.length } - } - - // parse data text strings to JavaScript date stamps - var parseDate = d3.time.format('%Y-%m-%d') - var parseDateTime = d3.time.format('%Y-%m-%d %H:%M:%S') - var parseDateRegEx = new RegExp(/^\d{4}-\d{2}-\d{2}$/) - var parseDateTimeRegEx = new RegExp(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) - dataset.forEach(function (d) { - d.data.forEach(function (d1) { - if (!(d1[0] instanceof Date)) { - if (parseDateRegEx.test(d1[0])) { - // d1[0] is date without time data - d1[0] = parseDate.parse(d1[0]) - } else if (parseDateTimeRegEx.test(d1[0])) { - // d1[0] is date with time data - d1[0] = parseDateTime.parse(d1[0]) + + // append data attribute in HTML for pagination interface + selection.attr('data-max-pages', maxPages) + + var noOfDatasets = endSet - startSet + var height = dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + + // check how data is arranged + if (definedBlocks === null) { + definedBlocks = 0 + for (var i = 0; i < dataset.length; i++) { + if (dataset[i].data[0].length === 3) { + definedBlocks = 1 + break } else { - throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + - '\'YYYY-MM-DD HH:MM:SS\'.') + if (definedBlocks) { + throw new Error('Detected different data formats in input data. Format can either be ' + + 'continuous data format or time gap data format but not both.') + } } - - if (!definedBlocks) { - d1[2] = d3.time.second.offset(d1[0], d.interval_s) - } else { - if (parseDateRegEx.test(d1[2])) { - // d1[2] is date without time data - d1[2] = parseDate.parse(d1[2]) - } else if (parseDateTimeRegEx.test(d1[2])) { - // d1[2] is date with time data - d1[2] = parseDateTime.parse(d1[2]) + } + } + + // parse data text strings to JavaScript date stamps + var parseDate = d3.time.format('%Y-%m-%d') + var parseDateTime = d3.time.format('%Y-%m-%d %H:%M:%S') + var parseDateRegEx = new RegExp(/^\d{4}-\d{2}-\d{2}$/) + var parseDateTimeRegEx = new RegExp(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) + dataset.forEach(function (d) { + d.data.forEach(function (d1) { + if (!(d1[0] instanceof Date)) { + if (parseDateRegEx.test(d1[0])) { + // d1[0] is date without time data + d1[0] = parseDate.parse(d1[0]) + } else if (parseDateTimeRegEx.test(d1[0])) { + // d1[0] is date with time data + d1[0] = parseDateTime.parse(d1[0]) } else { throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + - '\'YYYY-MM-DD HH:MM:SS\'.') + '\'YYYY-MM-DD HH:MM:SS\'.') + } + + if (!definedBlocks) { + d1[2] = d3.time.second.offset(d1[0], d.interval_s) + } else { + if (parseDateRegEx.test(d1[2])) { + // d1[2] is date without time data + d1[2] = parseDate.parse(d1[2]) + } else if (parseDateTimeRegEx.test(d1[2])) { + // d1[2] is date with time data + d1[2] = parseDateTime.parse(d1[2]) + } else { + throw new Error('Date/time format not recognized. Pick between \'YYYY-MM-DD\' or ' + + '\'YYYY-MM-DD HH:MM:SS\'.') + } } } - } + }) }) - }) - - // cluster data by dates to form time blocks - dataset.forEach(function (series, seriesI) { - var tmpData = [] - var dataLength = series.data.length - series.data.forEach(function (d, i) { - if (i !== 0 && i < dataLength) { - if (d[1] === tmpData[tmpData.length - 1][1]) { - // the value has not changed since the last date - if (definedBlocks) { - if (tmpData[tmpData.length - 1][2].getTime() === d[0].getTime()) { - // end of old and start of new block are the same + + // cluster data by dates to form time blocks + dataset.forEach(function (series, seriesI) { + var tmpData = [] + var dataLength = series.data.length + series.data.forEach(function (d, i) { + if (i !== 0 && i < dataLength) { + if (d[1] === tmpData[tmpData.length - 1][1]) { + // the value has not changed since the last date + if (definedBlocks) { + if (tmpData[tmpData.length - 1][2].getTime() === d[0].getTime()) { + // end of old and start of new block are the same + tmpData[tmpData.length - 1][2] = d[2] + tmpData[tmpData.length - 1][3] = 1 + } else { + tmpData.push(d) + } + } else { tmpData[tmpData.length - 1][2] = d[2] tmpData[tmpData.length - 1][3] = 1 - } else { - tmpData.push(d) } } else { - tmpData[tmpData.length - 1][2] = d[2] - tmpData[tmpData.length - 1][3] = 1 + // the value has changed since the last date + d[3] = 0 + if (!definedBlocks) { + // extend last block until new block starts + tmpData[tmpData.length - 1][2] = d[0] + } + tmpData.push(d) } - } else { - // the value has changed since the last date + } else if (i === 0) { d[3] = 0 - if (!definedBlocks) { - // extend last block until new block starts - tmpData[tmpData.length - 1][2] = d[0] - } tmpData.push(d) } - } else if (i === 0) { - d[3] = 0 - tmpData.push(d) - } + }) + dataset[seriesI].disp_data = tmpData }) - dataset[seriesI].disp_data = tmpData - }) - - // determine start and end dates among all nested datasets - var startDate = 0 - var endDate = 0 - - dataset.forEach(function (series, seriesI) { - if (series.disp_data.length > 0) { - if (startDate === 0) { - startDate = series.disp_data[0][0] - endDate = series.disp_data[series.disp_data.length - 1][2] - } else { - if (series.disp_data[0][0] < startDate) { + + // determine start and end dates among all nested datasets + var startDate = 0 + var endDate = 0 + + dataset.forEach(function (series, seriesI) { + if (series.disp_data.length > 0) { + if (startDate === 0) { startDate = series.disp_data[0][0] - } - if (series.disp_data[series.disp_data.length - 1][2] > endDate) { endDate = series.disp_data[series.disp_data.length - 1][2] + } else { + if (series.disp_data[0][0] < startDate) { + startDate = series.disp_data[0][0] + } + if (series.disp_data[series.disp_data.length - 1][2] > endDate) { + endDate = series.disp_data[series.disp_data.length - 1][2] + } } } + }) + + // define scales + var xScale = d3.time.scale() + .domain([startDate, endDate]) + .range([0, width]) + .clamp(1) + + // define axes + var xAxis = d3.svg.axis() + .scale(xScale) + .orient('top') + + // create SVG element + var svg = d3.select(this).append('svg') + .attr('width', width + margin.left + margin.right - 20) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') + + // create basic element groups + svg.append('g').attr('id', 'g_title') + svg.append('g').attr('id', 'g_axis') + svg.append('g').attr('id', 'g_data') + + // create y axis labels + svg.select('#g_axis').selectAll('text') + .data(dataset.slice(startSet, endSet)) + .enter() + .append('a') + .attr('xlink:href', function (d) { + return d.link + }) + .append('text') + .attr('x', paddingLeft) + .attr('y', lineSpacing + dataHeight / 2) + .text(function (d) { + return d.measure + }) + .attr('transform', function (d, i) { + return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' + }) + .attr('class', 'ytitle') + + // create vertical grid + svg.select('#g_axis').selectAll('line.vert_grid').data(xScale.ticks()) + .enter() + .append('line') + .attr({ + 'class': 'vert_grid', + 'x1': function (d) { + return xScale(d) + }, + 'x2': function (d) { + return xScale(d) + }, + 'y1': 0, + 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom + }) + + // create horizontal grid + svg.select('#g_axis').selectAll('line.horz_grid').data(dataset) + .enter() + .append('line') + .attr({ + 'class': 'horz_grid', + 'x1': 0, + 'x2': width, + 'y1': function (d, i) { + return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 + }, + 'y2': function (d, i) { + return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 + } + }) + + // create x axis + svg.select('#g_axis').append('g') + .attr('class', 'axis') + .call(xAxis) + + // make y groups for different data series + var g = svg.select('#g_data').selectAll('.g_data') + .data(dataset.slice(startSet, endSet)) + .enter() + .append('a') + .attr('xlink:href', function (d) { + return d.link + }) + .append('g') + .attr('transform', function (d, i) { + return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' + }) + .attr('class', 'dataset') + + // add data series + g.selectAll('rect') + .data(function (d) { + return d.disp_data + }) + .enter() + .append('rect') + .attr('x', function (d) { + return xScale(d[0]) + }) + .attr('y', lineSpacing) + .attr('width', function (d) { + return (xScale(d[2]) - xScale(d[0])) + }) + .attr('height', dataHeight) + .attr('class', function (d) { + if (d[1] === 1) { + return 'rect_has_data' + } + return 'rect_has_no_data' + }) + + // rework ticks and grid for better visual structure + function isYear (t) { + return +t === +(new Date(t.getFullYear(), 0, 1, 0, 0, 0)) } - }) - - // define scales - var xScale = d3.time.scale() - .domain([startDate, endDate]) - .range([0, width]) - .clamp(1) - - // define axes - var xAxis = d3.svg.axis() - .scale(xScale) - .orient('top') - - // create SVG element - var svg = d3.select(this).append('svg') - .attr('width', width + margin.left + margin.right - 20) - .attr('height', height + margin.top + margin.bottom) - .append('g') - .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') - - // create basic element groups - svg.append('g').attr('id', 'g_title') - svg.append('g').attr('id', 'g_axis') - svg.append('g').attr('id', 'g_data') - - // create y axis labels - svg.select('#g_axis').selectAll('text') - .data(dataset.slice(startSet, endSet)) - .enter() - .append('a') - .attr('xlink:href', function (d) { - return d.link - }) - .append('text') - .attr('x', paddingLeft) - .attr('y', lineSpacing + dataHeight / 2) - .text(function (d) { - return d.measure - }) - .attr('transform', function (d, i) { - return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' - }) - .attr('class', 'ytitle') - - // create vertical grid - svg.select('#g_axis').selectAll('line.vert_grid').data(xScale.ticks()) - .enter() - .append('line') - .attr({ - 'class': 'vert_grid', - 'x1': function (d) { - return xScale(d) - }, - 'x2': function (d) { - return xScale(d) - }, - 'y1': 0, - 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom - }) - - // create horizontal grid - svg.select('#g_axis').selectAll('line.horz_grid').data(dataset) - .enter() - .append('line') - .attr({ - 'class': 'horz_grid', - 'x1': 0, - 'x2': width, - 'y1': function (d, i) { - return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 - }, - 'y2': function (d, i) { - return ((lineSpacing + dataHeight) * i) + lineSpacing + dataHeight / 2 + + function isMonth (t) { + return +t === +(new Date(t.getFullYear(), t.getMonth(), 1, 0, 0, 0)) + } + + var xTicks = xScale.ticks() + var isYearTick = xTicks.map(isYear) + var isMonthTick = xTicks.map(isMonth) + // year emphasis + // ensure year emphasis is only active if years are the biggest clustering unit + if (emphasizeYearTicks && + !(isYearTick.every(function (d) { return d === true })) && + isMonthTick.every(function (d) { return d === true })) { + d3.selectAll('g.tick').each(function (d, i) { + if (isYearTick[i]) { + d3.select(this) + .attr({ + 'class': 'x_tick_emph' + }) } }) - - // create x axis - svg.select('#g_axis').append('g') - .attr('class', 'axis') - .call(xAxis) - - // make y groups for different data series - var g = svg.select('#g_data').selectAll('.g_data') - .data(dataset.slice(startSet, endSet)) - .enter() - .append('a') - .attr('xlink:href', function (d) { - return d.link - }) - .append('g') - .attr('transform', function (d, i) { - return 'translate(0,' + ((lineSpacing + dataHeight) * i) + ')' - }) - .attr('class', 'dataset') - - // add data series - g.selectAll('rect') - .data(function (d) { - return d.disp_data - }) - .enter() - .append('rect') - .attr('x', function (d) { - return xScale(d[0]) - }) - .attr('y', lineSpacing) - .attr('width', function (d) { - return (xScale(d[2]) - xScale(d[0])) - }) - .attr('height', dataHeight) - .attr('class', function (d) { - if (d[1] === 1) { - return 'rect_has_data' + d3.selectAll('.vert_grid').each(function (d, i) { + if (isYearTick[i]) { + d3.select(this) + .attr({ + 'class': 'vert_grid_emph' + }) } - return 'rect_has_no_data' }) - - // rework ticks and grid for better visual structure - function isYear (t) { - return +t === +(new Date(t.getFullYear(), 0, 1, 0, 0, 0)) - } - - function isMonth (t) { - return +t === +(new Date(t.getFullYear(), t.getMonth(), 1, 0, 0, 0)) - } - - var xTicks = xScale.ticks() - var isYearTick = xTicks.map(isYear) - var isMonthTick = xTicks.map(isMonth) - // year emphasis - // ensure year emphasis is only active if years are the biggest clustering unit - if (emphasizeYearTicks && - !(isYearTick.every(function (d) { return d === true })) && - isMonthTick.every(function (d) { return d === true })) { + } + + // today emphasis + var todayDate = new Date() + var emphasizedToday = false + todayDate.setHours(0, 0, 0, 0) d3.selectAll('g.tick').each(function (d, i) { - if (isYearTick[i]) { + if (d.getTime() === todayDate.getTime()) { + // tick exists for today, emphasizing it d3.select(this) .attr({ - 'class': 'x_tick_emph' + 'class': 'x_tick_today' }) } }) d3.selectAll('.vert_grid').each(function (d, i) { - if (isYearTick[i]) { + if (d.getTime() === todayDate.getTime()) { + // vertical line exists in the grid for today, emphasizing it d3.select(this) .attr({ - 'class': 'vert_grid_emph' + 'class': 'vert_grid_today', + 'stroke-dasharray': '10, 5' }) + emphasizedToday = true } }) - } - - // today emphasis - var todayDate = new Date() - var emphasizedToday = false - todayDate.setHours(0, 0, 0, 0) - d3.selectAll('g.tick').each(function (d, i) { - if (d.getTime() === todayDate.getTime()) { - // tick exists for today, emphasizing it - d3.select(this) - .attr({ - 'class': 'x_tick_today' - }) - } - }) - d3.selectAll('.vert_grid').each(function (d, i) { - if (d.getTime() === todayDate.getTime()) { - // vertical line exists in the grid for today, emphasizing it - d3.select(this) + + // not emphasized today yet + if (emphasizedToday === false) { + var interpolatedX = xScale.interpolate(d3.interpolate)(todayDate) + var lastInterpolatedX = xScale.interpolate(d3.interpolate)(endDate) + if (interpolatedX === lastInterpolatedX && todayDate > endDate) { + // today is out of range (not displayed in the chart) + interpolatedX += 18 + } + // adding the emphasized today vertical line + svg.select('#g_axis') + .append('line') .attr({ 'class': 'vert_grid_today', - 'stroke-dasharray': '10, 5' + 'stroke-dasharray': '10, 5', + 'x1': interpolatedX, + 'x2': interpolatedX, + 'y1': 0, + 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom }) - emphasizedToday = true } - }) - - // not emphasized today yet - if (emphasizedToday === false) { - var interpolatedX = xScale.interpolate(d3.interpolate)(todayDate) - var lastInterpolatedX = xScale.interpolate(d3.interpolate)(endDate) - if (interpolatedX === lastInterpolatedX && todayDate > endDate) { - // today is out of range (not displayed in the chart) - interpolatedX += 18 + + // create title + if (drawTitle) { + svg.select('#g_title') + .append('text') + .attr('x', paddingLeft) + .attr('y', paddingTopHeading) + .text('Data Availability Plot') + .attr('class', 'heading') } - // adding the emphasized today vertical line - svg.select('#g_axis') - .append('line') - .attr({ - 'class': 'vert_grid_today', - 'stroke-dasharray': '10, 5', - 'x1': interpolatedX, - 'x2': interpolatedX, - 'y1': 0, - 'y2': dataHeight * noOfDatasets + lineSpacing * noOfDatasets - 1 + paddingBottom - }) - } - - // create title - if (drawTitle) { + + // create subtitle + var subtitleText = dateFormat(startDate, 'mmmm yyyy') + ' — ' + dateFormat(endDate, 'mmmm yyyy'); + svg.select('#g_title') .append('text') .attr('x', paddingLeft) + .attr('y', paddingTopHeading + 17) + .text(subtitleText) + .attr('class', 'subheading') + + // create legend + var legend = svg.select('#g_title') + .append('g') + .attr('id', 'g_legend') + .attr('transform', 'translate(0,-12)') + + legend.append('rect') + .attr('x', width + margin.right - 150) .attr('y', paddingTopHeading) - .text('Data Availability Plot') - .attr('class', 'heading') - } - - // create subtitle - var subtitleText = dateFormat(startDate, 'mmmm yyyy') + ' — ' + dateFormat(endDate, 'mmmm yyyy'); - - svg.select('#g_title') - .append('text') - .attr('x', paddingLeft) - .attr('y', paddingTopHeading + 17) - .text(subtitleText) - .attr('class', 'subheading') - - // create legend - var legend = svg.select('#g_title') - .append('g') - .attr('id', 'g_legend') - .attr('transform', 'translate(0,-12)') - - legend.append('rect') - .attr('x', width + margin.right - 150) - .attr('y', paddingTopHeading) - .attr('height', 15) - .attr('width', 15) - .attr('class', 'rect_has_data') - - legend.append('text') - .attr('x', width + margin.right - 150 + 20) - .attr('y', paddingTopHeading + 8.5) - .text('On time issues') - .attr('class', 'legend') - - legend.append('rect') - .attr('x', width + margin.right - 150) - .attr('y', paddingTopHeading + 17) - .attr('height', 15) - .attr('width', 15) - .attr('class', 'rect_has_no_data') - - legend.append('text') - .attr('x', width + margin.right - 150 + 20) - .attr('y', paddingTopHeading + 8.5 + 15 + 2) - .text('Late issues') - .attr('class', 'legend') - }) - } - - chart.width = function (_) { - if (!arguments.length) return width - width = _ - return chart - } - - chart.drawTitle = function (_) { - if (!arguments.length) return drawTitle - drawTitle = _ - return chart - } - - chart.maxDisplayDatasets = function (_) { - if (!arguments.length) return maxDisplayDatasets - maxDisplayDatasets = _ - return chart - } - - chart.curDisplayFirstDataset = function (_) { - if (!arguments.length) return curDisplayFirstDataset - curDisplayFirstDataset = _ - return chart - } - - chart.emphasizeYearTicks = function (_) { - if (!arguments.length) return emphasizeYearTicks - emphasizeYearTicks = _ + .attr('height', 15) + .attr('width', 15) + .attr('class', 'rect_has_data') + + legend.append('text') + .attr('x', width + margin.right - 150 + 20) + .attr('y', paddingTopHeading + 8.5) + .text('On time issues') + .attr('class', 'legend') + + legend.append('rect') + .attr('x', width + margin.right - 150) + .attr('y', paddingTopHeading + 17) + .attr('height', 15) + .attr('width', 15) + .attr('class', 'rect_has_no_data') + + legend.append('text') + .attr('x', width + margin.right - 150 + 20) + .attr('y', paddingTopHeading + 8.5 + 15 + 2) + .text('Late issues') + .attr('class', 'legend') + }) + } + + chart.width = function (_) { + if (!arguments.length) return width + width = _ + return chart + } + + chart.drawTitle = function (_) { + if (!arguments.length) return drawTitle + drawTitle = _ + return chart + } + + chart.maxDisplayDatasets = function (_) { + if (!arguments.length) return maxDisplayDatasets + maxDisplayDatasets = _ + return chart + } + + chart.curDisplayFirstDataset = function (_) { + if (!arguments.length) return curDisplayFirstDataset + curDisplayFirstDataset = _ + return chart + } + + chart.emphasizeYearTicks = function (_) { + if (!arguments.length) return emphasizeYearTicks + emphasizeYearTicks = _ + return chart + } + return chart + }, + refreshChart: function (event) { + var chart = this.visavailChart().width(document.body.clientWidth - 290) + + d3.select('#gantt-app') // FIXME: Duplicate selector + .datum(this.dataset) + .call(chart) + }, + pad: function (number) { + var r = String(number) + if (r.length === 1) { + r = '0' + r + } + return r } - - return chart }, - refreshChart: function (event) { - var chart = this.visavailChart().width(document.body.clientWidth - 290) - - d3.select('#gantt-app') // FIXME: Duplicate selector - .datum(this.dataset) - .call(chart) - }, - pad: function (number) { - var r = String(number) - if (r.length === 1) { - r = '0' + r - } - return r + mounted: function () { + // build the dataset with the issues + this.buildDataSet() + + // refresh the gantt graph + this.refreshChart() } - }, - mounted: function () { - // build the dataset with the issues - this.buildDataSet() - - // refresh the gantt graph - this.refreshChart() - } -}); + }); })(); -- GitLab