42698 lines
1.6 MiB
42698 lines
1.6 MiB
/******/ (() => { // webpackBootstrap
|
|
/******/ var __webpack_modules__ = ({
|
|
|
|
/***/ 464
|
|
(module) {
|
|
|
|
!function(t,e){ true?module.exports=e():0}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",c="month",f="quarter",h="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,c),s=n-i<0,u=e.clone().add(r+(s?-1:1),c);return+(-(r+(n-i)/(s?i-u:u-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(t){return{M:c,y:h,w:o,d:a,D:d,h:u,m:s,s:i,ms:r,Q:f}[t]||String(t||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},g="en",D={};D[g]=M;var p="$isDayjsObject",S=function(t){return t instanceof _||!(!t||!t[p])},w=function t(e,n,r){var i;if(!e)return g;if("string"==typeof e){var s=e.toLowerCase();D[s]&&(i=s),n&&(D[s]=n,i=s);var u=e.split("-");if(!i&&u.length>1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},O=function(t,e){if(S(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},b=v;b.l=w,b.i=S,b.w=function(t,e){return O(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=w(t.locale,null,!0),this.parse(t),this.$x=this.$x||t.x||{},this[p]=!0}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(b.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return b},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=O(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return O(t)<this.startOf(e)},m.isBefore=function(t,e){return this.endOf(e)<O(t)},m.$g=function(t,e,n){return b.u(t)?this[e]:this.set(n,t)},m.unix=function(){return Math.floor(this.valueOf()/1e3)},m.valueOf=function(){return this.$d.getTime()},m.startOf=function(t,e){var n=this,r=!!b.u(e)||e,f=b.p(t),l=function(t,e){var i=b.w(n.$u?Date.UTC(n.$y,e,t):new Date(n.$y,e,t),n);return r?i:i.endOf(a)},$=function(t,e){return b.w(n.toDate()[t].apply(n.toDate("s"),(r?[0,0,0,0]:[23,59,59,999]).slice(e)),n)},y=this.$W,M=this.$M,m=this.$D,v="set"+(this.$u?"UTC":"");switch(f){case h:return r?l(1,0):l(31,11);case c:return r?l(1,M):l(0,M+1);case o:var g=this.$locale().weekStart||0,D=(y<g?y+7:y)-g;return l(r?m-D:m+(6-D),M);case a:case d:return $(v+"Hours",0);case u:return $(v+"Minutes",1);case s:return $(v+"Seconds",2);case i:return $(v+"Milliseconds",3);default:return this.clone()}},m.endOf=function(t){return this.startOf(t,!1)},m.$set=function(t,e){var n,o=b.p(t),f="set"+(this.$u?"UTC":""),l=(n={},n[a]=f+"Date",n[d]=f+"Date",n[c]=f+"Month",n[h]=f+"FullYear",n[u]=f+"Hours",n[s]=f+"Minutes",n[i]=f+"Seconds",n[r]=f+"Milliseconds",n)[o],$=o===a?this.$D+(e-this.$W):e;if(o===c||o===h){var y=this.clone().set(d,1);y.$d[l]($),y.init(),this.$d=y.set(d,Math.min(this.$D,y.daysInMonth())).$d}else l&&this.$d[l]($);return this.init(),this},m.set=function(t,e){return this.clone().$set(t,e)},m.get=function(t){return this[b.p(t)]()},m.add=function(r,f){var d,l=this;r=Number(r);var $=b.p(f),y=function(t){var e=O(l);return b.w(e.date(e.date()+Math.round(t*r)),l)};if($===c)return this.set(c,this.$M+r);if($===h)return this.set(h,this.$y+r);if($===a)return y(1);if($===o)return y(7);var M=(d={},d[s]=e,d[u]=n,d[i]=t,d)[$]||1,m=this.$d.getTime()+r*M;return b.w(m,this)},m.subtract=function(t,e){return this.add(-1*t,e)},m.format=function(t){var e=this,n=this.$locale();if(!this.isValid())return n.invalidDate||l;var r=t||"YYYY-MM-DDTHH:mm:ssZ",i=b.z(this),s=this.$H,u=this.$m,a=this.$M,o=n.weekdays,c=n.months,f=n.meridiem,h=function(t,n,i,s){return t&&(t[n]||t(e,r))||i[n].slice(0,s)},d=function(t){return b.s(s%12||12,t,"0")},$=f||function(t,e,n){var r=t<12?"AM":"PM";return n?r.toLowerCase():r};return r.replace(y,(function(t,r){return r||function(t){switch(t){case"YY":return String(e.$y).slice(-2);case"YYYY":return b.s(e.$y,4,"0");case"M":return a+1;case"MM":return b.s(a+1,2,"0");case"MMM":return h(n.monthsShort,a,c,3);case"MMMM":return h(c,a);case"D":return e.$D;case"DD":return b.s(e.$D,2,"0");case"d":return String(e.$W);case"dd":return h(n.weekdaysMin,e.$W,o,2);case"ddd":return h(n.weekdaysShort,e.$W,o,3);case"dddd":return o[e.$W];case"H":return String(s);case"HH":return b.s(s,2,"0");case"h":return d(1);case"hh":return d(2);case"a":return $(s,u,!0);case"A":return $(s,u,!1);case"m":return String(u);case"mm":return b.s(u,2,"0");case"s":return String(e.$s);case"ss":return b.s(e.$s,2,"0");case"SSS":return b.s(e.$ms,3,"0");case"Z":return i}return null}(t)||i.replace(":","")}))},m.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},m.diff=function(r,d,l){var $,y=this,M=b.p(d),m=O(r),v=(m.utcOffset()-this.utcOffset())*e,g=this-m,D=function(){return b.m(y,m)};switch(M){case h:$=D()/12;break;case c:$=D();break;case f:$=D()/3;break;case o:$=(g-v)/6048e5;break;case a:$=(g-v)/864e5;break;case u:$=g/n;break;case s:$=g/e;break;case i:$=g/t;break;default:$=g}return l?$:b.a($)},m.daysInMonth=function(){return this.endOf(c).$D},m.$locale=function(){return D[this.$L]},m.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=w(t,e,!0);return r&&(n.$L=r),n},m.clone=function(){return b.w(this.$d,this)},m.toDate=function(){return new Date(this.valueOf())},m.toJSON=function(){return this.isValid()?this.toISOString():null},m.toISOString=function(){return this.$d.toISOString()},m.toString=function(){return this.$d.toUTCString()},M}(),k=_.prototype;return O.prototype=k,[["$ms",r],["$s",i],["$m",s],["$H",u],["$W",a],["$M",c],["$y",h],["$D",d]].forEach((function(t){k[t[1]]=function(e){return this.$g(e,t[0],t[1])}})),O.extend=function(t,e){return t.$i||(t(e,_,O),t.$i=!0),O},O.locale=w,O.isDayjs=S,O.unix=function(t){return O(1e3*t)},O.en=D[g],O.Ls=D,O.p={},O}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 258
|
|
(module) {
|
|
|
|
!function(e,t){ true?module.exports=t():0}(this,(function(){"use strict";return function(e,t){var r=t.prototype,n=r.format;r.format=function(e){var t=this,r=this.$locale();if(!this.isValid())return n.bind(this)(e);var s=this.$utils(),a=(e||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,(function(e){switch(e){case"Q":return Math.ceil((t.$M+1)/3);case"Do":return r.ordinal(t.$D);case"gggg":return t.weekYear();case"GGGG":return t.isoWeekYear();case"wo":return r.ordinal(t.week(),"W");case"w":case"ww":return s.s(t.week(),"w"===e?1:2,"0");case"W":case"WW":return s.s(t.isoWeek(),"W"===e?1:2,"0");case"k":case"kk":return s.s(String(0===t.$H?24:t.$H),"k"===e?1:2,"0");case"X":return Math.floor(t.$d.getTime()/1e3);case"x":return t.$d.getTime();case"z":return"["+t.offsetName()+"]";case"zzz":return"["+t.offsetName("long")+"]";default:return e}}));return n.bind(this)(a)}}}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 630
|
|
(module) {
|
|
|
|
!function(e,t){ true?module.exports=t():0}(this,(function(){"use strict";var e={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},t=/(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,n=/\d/,r=/\d\d/,i=/\d\d?/,o=/\d*[^-_:/,()\s\d]+/,s={},a=function(e){return(e=+e)+(e>68?1900:2e3)};var f=function(e){return function(t){this[e]=+t}},h=[/[+-]\d\d:?(\d\d)?|Z/,function(e){(this.zone||(this.zone={})).offset=function(e){if(!e)return 0;if("Z"===e)return 0;var t=e.match(/([+-]|\d\d)/g),n=60*t[1]+(+t[2]||0);return 0===n?0:"+"===t[0]?-n:n}(e)}],u=function(e){var t=s[e];return t&&(t.indexOf?t:t.s.concat(t.f))},d=function(e,t){var n,r=s.meridiem;if(r){for(var i=1;i<=24;i+=1)if(e.indexOf(r(i,0,t))>-1){n=i>12;break}}else n=e===(t?"pm":"PM");return n},c={A:[o,function(e){this.afternoon=d(e,!1)}],a:[o,function(e){this.afternoon=d(e,!0)}],Q:[n,function(e){this.month=3*(e-1)+1}],S:[n,function(e){this.milliseconds=100*+e}],SS:[r,function(e){this.milliseconds=10*+e}],SSS:[/\d{3}/,function(e){this.milliseconds=+e}],s:[i,f("seconds")],ss:[i,f("seconds")],m:[i,f("minutes")],mm:[i,f("minutes")],H:[i,f("hours")],h:[i,f("hours")],HH:[i,f("hours")],hh:[i,f("hours")],D:[i,f("day")],DD:[r,f("day")],Do:[o,function(e){var t=s.ordinal,n=e.match(/\d+/);if(this.day=n[0],t)for(var r=1;r<=31;r+=1)t(r).replace(/\[|\]/g,"")===e&&(this.day=r)}],w:[i,f("week")],ww:[r,f("week")],M:[i,f("month")],MM:[r,f("month")],MMM:[o,function(e){var t=u("months"),n=(u("monthsShort")||t.map((function(e){return e.slice(0,3)}))).indexOf(e)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[o,function(e){var t=u("months").indexOf(e)+1;if(t<1)throw new Error;this.month=t%12||t}],Y:[/[+-]?\d+/,f("year")],YY:[r,function(e){this.year=a(e)}],YYYY:[/\d{4}/,f("year")],Z:h,ZZ:h};function l(n){var r,i;r=n,i=s&&s.formats;for(var o=(n=r.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(t,n,r){var o=r&&r.toUpperCase();return n||i[r]||e[r]||i[o].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(e,t,n){return t||n.slice(1)}))}))).match(t),a=o.length,f=0;f<a;f+=1){var h=o[f],u=c[h],d=u&&u[0],l=u&&u[1];o[f]=l?{regex:d,parser:l}:h.replace(/^\[|\]$/g,"")}return function(e){for(var t={},n=0,r=0;n<a;n+=1){var i=o[n];if("string"==typeof i)r+=i.length;else{var s=i.regex,f=i.parser,h=e.slice(r),u=s.exec(h)[0];f.call(t,u),e=e.replace(u,"")}}return function(e){var t=e.afternoon;if(void 0!==t){var n=e.hours;t?n<12&&(e.hours+=12):12===n&&(e.hours=0),delete e.afternoon}}(t),t}}return function(e,t,n){n.p.customParseFormat=!0,e&&e.parseTwoDigitYear&&(a=e.parseTwoDigitYear);var r=t.prototype,i=r.parse;r.parse=function(e){var t=e.date,r=e.utc,o=e.args;this.$u=r;var a=o[1];if("string"==typeof a){var f=!0===o[2],h=!0===o[3],u=f||h,d=o[2];h&&(d=o[2]),s=this.$locale(),!f&&d&&(s=n.Ls[d]),this.$d=function(e,t,n,r){try{if(["x","X"].indexOf(t)>-1)return new Date(("X"===t?1e3:1)*e);var i=l(t)(e),o=i.year,s=i.month,a=i.day,f=i.hours,h=i.minutes,u=i.seconds,d=i.milliseconds,c=i.zone,m=i.week,M=new Date,Y=a||(o||s?1:M.getDate()),p=o||M.getFullYear(),v=0;o&&!s||(v=s>0?s-1:M.getMonth());var D,w=f||0,g=h||0,y=u||0,L=d||0;return c?new Date(Date.UTC(p,v,Y,w,g,y,L+60*c.offset*1e3)):n?new Date(Date.UTC(p,v,Y,w,g,y,L)):(D=new Date(p,v,Y,w,g,y,L),m&&(D=r(D).week(m).toDate()),D)}catch(e){return new Date("")}}(t,a,r,n),this.init(),d&&!0!==d&&(this.$L=this.locale(d).$L),u&&t!=this.format(a)&&(this.$d=new Date("")),s={}}else if(a instanceof Array)for(var c=a.length,m=1;m<=c;m+=1){o[1]=a[m-1];var M=n.apply(this,o);if(M.isValid()){this.$d=M.$d,this.$L=M.$L,this.init();break}m===c&&(this.$d=new Date(""))}else i.call(this,e)}}}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 562
|
|
(module) {
|
|
|
|
!function(r,e){ true?module.exports=e():0}(this,(function(){"use strict";return function(r,e,t){r=r||{};var n=e.prototype,o={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:"s",r:44,d:"second"},{l:"m",r:89},{l:"mm",r:44,d:"minute"},{l:"h",r:89},{l:"hh",r:21,d:"hour"},{l:"d",r:35},{l:"dd",r:25,d:"day"},{l:"M",r:45},{l:"MM",r:10,d:"month"},{l:"y",r:17},{l:"yy",d:"year"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(f=d?t(e).diff(i,y.d,!0):i.diff(e,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(f));if(s=f>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(""+p)),a="string"==typeof v?v.replace("%d",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return"function"==typeof M?M(a):M.replace("%s",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 168
|
|
(module) {
|
|
|
|
!function(t,e){ true?module.exports=e():0}(this,(function(){"use strict";var t={year:0,month:1,day:2,hour:3,minute:4,second:5},e={};return function(n,i,o){var r,a=function(t,n,i){void 0===i&&(i={});var o=new Date(t),r=function(t,n){void 0===n&&(n={});var i=n.timeZoneName||"short",o=t+"|"+i,r=e[o];return r||(r=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:t,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",timeZoneName:i}),e[o]=r),r}(n,i);return r.formatToParts(o)},u=function(e,n){for(var i=a(e,n),r=[],u=0;u<i.length;u+=1){var f=i[u],s=f.type,m=f.value,c=t[s];c>=0&&(r[c]=parseInt(m,10))}var d=r[3],l=24===d?0:d,h=r[0]+"-"+r[1]+"-"+r[2]+" "+l+":"+r[4]+":"+r[5]+":000",v=+e;return(o.utc(h).valueOf()-(v-=v%1e3))/6e4},f=i.prototype;f.tz=function(t,e){void 0===t&&(t=r);var n,i=this.utcOffset(),a=this.toDate(),u=a.toLocaleString("en-US",{timeZone:t}),f=Math.round((a-new Date(u))/1e3/60),s=15*-Math.round(a.getTimezoneOffset()/15)-f;if(!Number(s))n=this.utcOffset(0,e);else if(n=o(u,{locale:this.$L}).$set("millisecond",this.$ms).utcOffset(s,!0),e){var m=n.utcOffset();n=n.add(i-m,"minute")}return n.$x.$timezone=t,n},f.offsetName=function(t){var e=this.$x.$timezone||o.tz.guess(),n=a(this.valueOf(),e,{timeZoneName:t}).find((function(t){return"timezonename"===t.type.toLowerCase()}));return n&&n.value};var s=f.startOf;f.startOf=function(t,e){if(!this.$x||!this.$x.$timezone)return s.call(this,t,e);var n=o(this.format("YYYY-MM-DD HH:mm:ss:SSS"),{locale:this.$L});return s.call(n,t,e).tz(this.$x.$timezone,!0)},o.tz=function(t,e,n){var i=n&&e,a=n||e||r,f=u(+o(),a);if("string"!=typeof t)return o(t).tz(a);var s=function(t,e,n){var i=t-60*e*1e3,o=u(i,n);if(e===o)return[i,e];var r=u(i-=60*(o-e)*1e3,n);return o===r?[i,o]:[t-60*Math.min(o,r)*1e3,Math.max(o,r)]}(o.utc(t,i).valueOf(),f,a),m=s[0],c=s[1],d=o(m).utcOffset(c);return d.$x.$timezone=a,d},o.tz.guess=function(){return Intl.DateTimeFormat().resolvedOptions().timeZone},o.tz.setDefault=function(t){r=t}}}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 657
|
|
(module) {
|
|
|
|
!function(t,i){ true?module.exports=i():0}(this,(function(){"use strict";var t="minute",i=/[+-]\d\d(?::?\d\d)?/g,e=/([+-]|\d\d)/g;return function(s,f,n){var u=f.prototype;n.utc=function(t){var i={date:t,utc:!0,args:arguments};return new f(i)},u.utc=function(i){var e=n(this.toDate(),{locale:this.$L,utc:!0});return i?e.add(this.utcOffset(),t):e},u.local=function(){return n(this.toDate(),{locale:this.$L,utc:!1})};var r=u.parse;u.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),r.call(this,t)};var o=u.init;u.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds()}else o.call(this)};var a=u.utcOffset;u.utcOffset=function(s,f){var n=this.$utils().u;if(n(s))return this.$u?0:n(this.$offset)?a.call(this):this.$offset;if("string"==typeof s&&(s=function(t){void 0===t&&(t="");var s=t.match(i);if(!s)return null;var f=(""+s[0]).match(e)||["-",0,0],n=f[0],u=60*+f[1]+ +f[2];return 0===u?0:"+"===n?u:-u}(s),null===s))return this;var u=Math.abs(s)<=16?60*s:s;if(0===u)return this.utc(f);var r=this.clone();if(f)return r.$offset=u,r.$u=!1,r;var o=this.$u?this.toDate().getTimezoneOffset():-1*this.utcOffset();return(r=this.local().add(u+o,t)).$offset=u,r.$x.$localOffset=o,r};var h=u.format;u.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return h.call(this,i)},u.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+(this.$x.$localOffset||this.$d.getTimezoneOffset());return this.$d.valueOf()-6e4*t},u.isUTC=function(){return!!this.$u},u.toISOString=function(){return this.toDate().toISOString()},u.toString=function(){return this.toDate().toUTCString()};var l=u.toDate;u.toDate=function(t){return"s"===t&&this.$offset?n(this.format("YYYY-MM-DD HH:mm:ss:SSS")).toDate():l.call(this)};var c=u.diff;u.diff=function(t,i,e){if(t&&this.$u===t.$u)return c.call(this,t,i,e);var s=this.local(),f=n(t).local();return c.call(s,f,i,e)}}}));
|
|
|
|
/***/ },
|
|
|
|
/***/ 869
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/**
|
|
* matchesSelector v2.0.2
|
|
* matchesSelector( element, '.selector' )
|
|
* MIT license
|
|
*/
|
|
|
|
/*jshint browser: true, strict: true, undef: true, unused: true */
|
|
|
|
( function( window, factory ) {
|
|
/*global define: false, module: false */
|
|
'use strict';
|
|
// universal module definition
|
|
if ( true ) {
|
|
// AMD
|
|
!(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
|
(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
|
|
__WEBPACK_AMD_DEFINE_FACTORY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( window, function factory() {
|
|
'use strict';
|
|
|
|
var matchesMethod = ( function() {
|
|
var ElemProto = window.Element.prototype;
|
|
// check for the standard method name first
|
|
if ( ElemProto.matches ) {
|
|
return 'matches';
|
|
}
|
|
// check un-prefixed
|
|
if ( ElemProto.matchesSelector ) {
|
|
return 'matchesSelector';
|
|
}
|
|
// check vendor prefixes
|
|
var prefixes = [ 'webkit', 'moz', 'ms', 'o' ];
|
|
|
|
for ( var i=0; i < prefixes.length; i++ ) {
|
|
var prefix = prefixes[i];
|
|
var method = prefix + 'MatchesSelector';
|
|
if ( ElemProto[ method ] ) {
|
|
return method;
|
|
}
|
|
}
|
|
})();
|
|
|
|
return function matchesSelector( elem, selector ) {
|
|
return elem[ matchesMethod ]( selector );
|
|
};
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 240
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/**
|
|
* EvEmitter v1.1.0
|
|
* Lil' event emitter
|
|
* MIT License
|
|
*/
|
|
|
|
/* jshint unused: true, undef: true, strict: true */
|
|
|
|
( function( global, factory ) {
|
|
// universal module definition
|
|
/* jshint strict: false */ /* globals define, module, window */
|
|
if ( true ) {
|
|
// AMD - RequireJS
|
|
!(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
|
(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
|
|
__WEBPACK_AMD_DEFINE_FACTORY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( typeof window != 'undefined' ? window : this, function() {
|
|
|
|
"use strict";
|
|
|
|
function EvEmitter() {}
|
|
|
|
var proto = EvEmitter.prototype;
|
|
|
|
proto.on = function( eventName, listener ) {
|
|
if ( !eventName || !listener ) {
|
|
return;
|
|
}
|
|
// set events hash
|
|
var events = this._events = this._events || {};
|
|
// set listeners array
|
|
var listeners = events[ eventName ] = events[ eventName ] || [];
|
|
// only add once
|
|
if ( listeners.indexOf( listener ) == -1 ) {
|
|
listeners.push( listener );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.once = function( eventName, listener ) {
|
|
if ( !eventName || !listener ) {
|
|
return;
|
|
}
|
|
// add event
|
|
this.on( eventName, listener );
|
|
// set once flag
|
|
// set onceEvents hash
|
|
var onceEvents = this._onceEvents = this._onceEvents || {};
|
|
// set onceListeners object
|
|
var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {};
|
|
// set flag
|
|
onceListeners[ listener ] = true;
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.off = function( eventName, listener ) {
|
|
var listeners = this._events && this._events[ eventName ];
|
|
if ( !listeners || !listeners.length ) {
|
|
return;
|
|
}
|
|
var index = listeners.indexOf( listener );
|
|
if ( index != -1 ) {
|
|
listeners.splice( index, 1 );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.emitEvent = function( eventName, args ) {
|
|
var listeners = this._events && this._events[ eventName ];
|
|
if ( !listeners || !listeners.length ) {
|
|
return;
|
|
}
|
|
// copy over to avoid interference if .off() in listener
|
|
listeners = listeners.slice(0);
|
|
args = args || [];
|
|
// once stuff
|
|
var onceListeners = this._onceEvents && this._onceEvents[ eventName ];
|
|
|
|
for ( var i=0; i < listeners.length; i++ ) {
|
|
var listener = listeners[i]
|
|
var isOnce = onceListeners && onceListeners[ listener ];
|
|
if ( isOnce ) {
|
|
// remove listener
|
|
// remove before trigger to prevent recursion
|
|
this.off( eventName, listener );
|
|
// unset once flag
|
|
delete onceListeners[ listener ];
|
|
}
|
|
// trigger listener
|
|
listener.apply( this, args );
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
proto.allOff = function() {
|
|
delete this._events;
|
|
delete this._onceEvents;
|
|
};
|
|
|
|
return EvEmitter;
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 346
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/**
|
|
* Fizzy UI utils v2.0.7
|
|
* MIT license
|
|
*/
|
|
|
|
/*jshint browser: true, undef: true, unused: true, strict: true */
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/*jshint strict: false */ /*globals define, module, require */
|
|
|
|
if ( true ) {
|
|
// AMD
|
|
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
|
|
__webpack_require__(869)
|
|
], __WEBPACK_AMD_DEFINE_RESULT__ = (function( matchesSelector ) {
|
|
return factory( window, matchesSelector );
|
|
}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( window, function factory( window, matchesSelector ) {
|
|
|
|
'use strict';
|
|
|
|
var utils = {};
|
|
|
|
// ----- extend ----- //
|
|
|
|
// extends objects
|
|
utils.extend = function( a, b ) {
|
|
for ( var prop in b ) {
|
|
a[ prop ] = b[ prop ];
|
|
}
|
|
return a;
|
|
};
|
|
|
|
// ----- modulo ----- //
|
|
|
|
utils.modulo = function( num, div ) {
|
|
return ( ( num % div ) + div ) % div;
|
|
};
|
|
|
|
// ----- makeArray ----- //
|
|
|
|
var arraySlice = Array.prototype.slice;
|
|
|
|
// turn element or nodeList into an array
|
|
utils.makeArray = function( obj ) {
|
|
if ( Array.isArray( obj ) ) {
|
|
// use object if already an array
|
|
return obj;
|
|
}
|
|
// return empty array if undefined or null. #6
|
|
if ( obj === null || obj === undefined ) {
|
|
return [];
|
|
}
|
|
|
|
var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number';
|
|
if ( isArrayLike ) {
|
|
// convert nodeList to array
|
|
return arraySlice.call( obj );
|
|
}
|
|
|
|
// array of single index
|
|
return [ obj ];
|
|
};
|
|
|
|
// ----- removeFrom ----- //
|
|
|
|
utils.removeFrom = function( ary, obj ) {
|
|
var index = ary.indexOf( obj );
|
|
if ( index != -1 ) {
|
|
ary.splice( index, 1 );
|
|
}
|
|
};
|
|
|
|
// ----- getParent ----- //
|
|
|
|
utils.getParent = function( elem, selector ) {
|
|
while ( elem.parentNode && elem != document.body ) {
|
|
elem = elem.parentNode;
|
|
if ( matchesSelector( elem, selector ) ) {
|
|
return elem;
|
|
}
|
|
}
|
|
};
|
|
|
|
// ----- getQueryElement ----- //
|
|
|
|
// use element as selector string
|
|
utils.getQueryElement = function( elem ) {
|
|
if ( typeof elem == 'string' ) {
|
|
return document.querySelector( elem );
|
|
}
|
|
return elem;
|
|
};
|
|
|
|
// ----- handleEvent ----- //
|
|
|
|
// enable .ontype to trigger from .addEventListener( elem, 'type' )
|
|
utils.handleEvent = function( event ) {
|
|
var method = 'on' + event.type;
|
|
if ( this[ method ] ) {
|
|
this[ method ]( event );
|
|
}
|
|
};
|
|
|
|
// ----- filterFindElements ----- //
|
|
|
|
utils.filterFindElements = function( elems, selector ) {
|
|
// make array of elems
|
|
elems = utils.makeArray( elems );
|
|
var ffElems = [];
|
|
|
|
elems.forEach( function( elem ) {
|
|
// check that elem is an actual element
|
|
if ( !( elem instanceof HTMLElement ) ) {
|
|
return;
|
|
}
|
|
// add elem if no selector
|
|
if ( !selector ) {
|
|
ffElems.push( elem );
|
|
return;
|
|
}
|
|
// filter & find items if we have a selector
|
|
// filter
|
|
if ( matchesSelector( elem, selector ) ) {
|
|
ffElems.push( elem );
|
|
}
|
|
// find children
|
|
var childElems = elem.querySelectorAll( selector );
|
|
// concat childElems to filterFound array
|
|
for ( var i=0; i < childElems.length; i++ ) {
|
|
ffElems.push( childElems[i] );
|
|
}
|
|
});
|
|
|
|
return ffElems;
|
|
};
|
|
|
|
// ----- debounceMethod ----- //
|
|
|
|
utils.debounceMethod = function( _class, methodName, threshold ) {
|
|
threshold = threshold || 100;
|
|
// original method
|
|
var method = _class.prototype[ methodName ];
|
|
var timeoutName = methodName + 'Timeout';
|
|
|
|
_class.prototype[ methodName ] = function() {
|
|
var timeout = this[ timeoutName ];
|
|
clearTimeout( timeout );
|
|
|
|
var args = arguments;
|
|
var _this = this;
|
|
this[ timeoutName ] = setTimeout( function() {
|
|
method.apply( _this, args );
|
|
delete _this[ timeoutName ];
|
|
}, threshold );
|
|
};
|
|
};
|
|
|
|
// ----- docReady ----- //
|
|
|
|
utils.docReady = function( callback ) {
|
|
var readyState = document.readyState;
|
|
if ( readyState == 'complete' || readyState == 'interactive' ) {
|
|
// do async to allow for other scripts to run. metafizzy/flickity#441
|
|
setTimeout( callback );
|
|
} else {
|
|
document.addEventListener( 'DOMContentLoaded', callback );
|
|
}
|
|
};
|
|
|
|
// ----- htmlInit ----- //
|
|
|
|
// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/
|
|
utils.toDashed = function( str ) {
|
|
return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) {
|
|
return $1 + '-' + $2;
|
|
}).toLowerCase();
|
|
};
|
|
|
|
var console = window.console;
|
|
/**
|
|
* allow user to initialize classes via [data-namespace] or .js-namespace class
|
|
* htmlInit( Widget, 'widgetName' )
|
|
* options are parsed from data-namespace-options
|
|
*/
|
|
utils.htmlInit = function( WidgetClass, namespace ) {
|
|
utils.docReady( function() {
|
|
var dashedNamespace = utils.toDashed( namespace );
|
|
var dataAttr = 'data-' + dashedNamespace;
|
|
var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' );
|
|
var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace );
|
|
var elems = utils.makeArray( dataAttrElems )
|
|
.concat( utils.makeArray( jsDashElems ) );
|
|
var dataOptionsAttr = dataAttr + '-options';
|
|
var jQuery = window.jQuery;
|
|
|
|
elems.forEach( function( elem ) {
|
|
var attr = elem.getAttribute( dataAttr ) ||
|
|
elem.getAttribute( dataOptionsAttr );
|
|
var options;
|
|
try {
|
|
options = attr && JSON.parse( attr );
|
|
} catch ( error ) {
|
|
// log error, do not initialize
|
|
if ( console ) {
|
|
console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className +
|
|
': ' + error );
|
|
}
|
|
return;
|
|
}
|
|
// initialize
|
|
var instance = new WidgetClass( elem, options );
|
|
// make available via $().data('namespace')
|
|
if ( jQuery ) {
|
|
jQuery.data( elem, namespace, instance );
|
|
}
|
|
});
|
|
|
|
});
|
|
};
|
|
|
|
// ----- ----- //
|
|
|
|
return utils;
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 928
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
|
|
* getSize v2.0.3
|
|
* measure size of elements
|
|
* MIT license
|
|
*/
|
|
|
|
/* jshint browser: true, strict: true, undef: true, unused: true */
|
|
/* globals console: false */
|
|
|
|
( function( window, factory ) {
|
|
/* jshint strict: false */ /* globals define, module */
|
|
if ( true ) {
|
|
// AMD
|
|
!(__WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
|
(__WEBPACK_AMD_DEFINE_FACTORY__.call(exports, __webpack_require__, exports, module)) :
|
|
__WEBPACK_AMD_DEFINE_FACTORY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
})( window, function factory() {
|
|
'use strict';
|
|
|
|
// -------------------------- helpers -------------------------- //
|
|
|
|
// get a number from a string, not a percentage
|
|
function getStyleSize( value ) {
|
|
var num = parseFloat( value );
|
|
// not a percent like '100%', and a number
|
|
var isValid = value.indexOf('%') == -1 && !isNaN( num );
|
|
return isValid && num;
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
var logError = typeof console == 'undefined' ? noop :
|
|
function( message ) {
|
|
console.error( message );
|
|
};
|
|
|
|
// -------------------------- measurements -------------------------- //
|
|
|
|
var measurements = [
|
|
'paddingLeft',
|
|
'paddingRight',
|
|
'paddingTop',
|
|
'paddingBottom',
|
|
'marginLeft',
|
|
'marginRight',
|
|
'marginTop',
|
|
'marginBottom',
|
|
'borderLeftWidth',
|
|
'borderRightWidth',
|
|
'borderTopWidth',
|
|
'borderBottomWidth'
|
|
];
|
|
|
|
var measurementsLength = measurements.length;
|
|
|
|
function getZeroSize() {
|
|
var size = {
|
|
width: 0,
|
|
height: 0,
|
|
innerWidth: 0,
|
|
innerHeight: 0,
|
|
outerWidth: 0,
|
|
outerHeight: 0
|
|
};
|
|
for ( var i=0; i < measurementsLength; i++ ) {
|
|
var measurement = measurements[i];
|
|
size[ measurement ] = 0;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
// -------------------------- getStyle -------------------------- //
|
|
|
|
/**
|
|
* getStyle, get style of element, check for Firefox bug
|
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=548397
|
|
*/
|
|
function getStyle( elem ) {
|
|
var style = getComputedStyle( elem );
|
|
if ( !style ) {
|
|
logError( 'Style returned ' + style +
|
|
'. Are you running this code in a hidden iframe on Firefox? ' +
|
|
'See https://bit.ly/getsizebug1' );
|
|
}
|
|
return style;
|
|
}
|
|
|
|
// -------------------------- setup -------------------------- //
|
|
|
|
var isSetup = false;
|
|
|
|
var isBoxSizeOuter;
|
|
|
|
/**
|
|
* setup
|
|
* check isBoxSizerOuter
|
|
* do on first getSize() rather than on page load for Firefox bug
|
|
*/
|
|
function setup() {
|
|
// setup once
|
|
if ( isSetup ) {
|
|
return;
|
|
}
|
|
isSetup = true;
|
|
|
|
// -------------------------- box sizing -------------------------- //
|
|
|
|
/**
|
|
* Chrome & Safari measure the outer-width on style.width on border-box elems
|
|
* IE11 & Firefox<29 measures the inner-width
|
|
*/
|
|
var div = document.createElement('div');
|
|
div.style.width = '200px';
|
|
div.style.padding = '1px 2px 3px 4px';
|
|
div.style.borderStyle = 'solid';
|
|
div.style.borderWidth = '1px 2px 3px 4px';
|
|
div.style.boxSizing = 'border-box';
|
|
|
|
var body = document.body || document.documentElement;
|
|
body.appendChild( div );
|
|
var style = getStyle( div );
|
|
// round value for browser zoom. desandro/masonry#928
|
|
isBoxSizeOuter = Math.round( getStyleSize( style.width ) ) == 200;
|
|
getSize.isBoxSizeOuter = isBoxSizeOuter;
|
|
|
|
body.removeChild( div );
|
|
}
|
|
|
|
// -------------------------- getSize -------------------------- //
|
|
|
|
function getSize( elem ) {
|
|
setup();
|
|
|
|
// use querySeletor if elem is string
|
|
if ( typeof elem == 'string' ) {
|
|
elem = document.querySelector( elem );
|
|
}
|
|
|
|
// do not proceed on non-objects
|
|
if ( !elem || typeof elem != 'object' || !elem.nodeType ) {
|
|
return;
|
|
}
|
|
|
|
var style = getStyle( elem );
|
|
|
|
// if hidden, everything is 0
|
|
if ( style.display == 'none' ) {
|
|
return getZeroSize();
|
|
}
|
|
|
|
var size = {};
|
|
size.width = elem.offsetWidth;
|
|
size.height = elem.offsetHeight;
|
|
|
|
var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box';
|
|
|
|
// get all measurements
|
|
for ( var i=0; i < measurementsLength; i++ ) {
|
|
var measurement = measurements[i];
|
|
var value = style[ measurement ];
|
|
var num = parseFloat( value );
|
|
// any 'auto', 'medium' value will be 0
|
|
size[ measurement ] = !isNaN( num ) ? num : 0;
|
|
}
|
|
|
|
var paddingWidth = size.paddingLeft + size.paddingRight;
|
|
var paddingHeight = size.paddingTop + size.paddingBottom;
|
|
var marginWidth = size.marginLeft + size.marginRight;
|
|
var marginHeight = size.marginTop + size.marginBottom;
|
|
var borderWidth = size.borderLeftWidth + size.borderRightWidth;
|
|
var borderHeight = size.borderTopWidth + size.borderBottomWidth;
|
|
|
|
var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter;
|
|
|
|
// overwrite width and height if we can get it from style
|
|
var styleWidth = getStyleSize( style.width );
|
|
if ( styleWidth !== false ) {
|
|
size.width = styleWidth +
|
|
// add padding and border unless it's already including it
|
|
( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth );
|
|
}
|
|
|
|
var styleHeight = getStyleSize( style.height );
|
|
if ( styleHeight !== false ) {
|
|
size.height = styleHeight +
|
|
// add padding and border unless it's already including it
|
|
( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight );
|
|
}
|
|
|
|
size.innerWidth = size.width - ( paddingWidth + borderWidth );
|
|
size.innerHeight = size.height - ( paddingHeight + borderHeight );
|
|
|
|
size.outerWidth = size.width + marginWidth;
|
|
size.outerHeight = size.height + marginHeight;
|
|
|
|
return size;
|
|
}
|
|
|
|
return getSize;
|
|
|
|
});
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 177
|
|
() {
|
|
|
|
"use strict";
|
|
jsVectorMap.addMap("world",{insets:[{width:900,top:0,left:0,height:440.70631074413296,bbox:[{y:-12671671.123330014,x:-20004297.151525836},{y:6930392.025135122,x:20026572.39474939}]}],paths:{BD:{path:"M651.84,230.21l-0.6,-2.0l-1.36,-1.71l-2.31,-0.11l-0.41,0.48l0.2,0.94l-0.53,0.99l-0.72,-0.36l-0.68,0.35l-1.2,-0.36l-0.37,-2.0l-0.81,-1.86l0.39,-1.46l-0.22,-0.47l-1.14,-0.53l0.29,-0.5l1.48,-0.94l0.03,-0.65l-1.55,-1.22l0.55,-1.14l1.61,0.94l1.04,0.15l0.18,1.54l0.34,0.35l5.64,0.63l-0.84,1.64l-1.22,0.34l-0.77,1.51l0.07,0.47l1.37,1.37l0.67,-0.19l0.42,-1.39l1.21,3.84l-0.03,1.21l-0.33,-0.15l-0.4,0.28Z",name:"Bangladesh"},BE:{path:"M429.29,144.05l1.91,0.24l2.1,-0.63l2.63,1.99l-0.21,1.66l-0.69,0.4l-0.18,1.2l-1.66,-1.13l-1.39,0.15l-2.73,-2.7l-1.17,-0.18l-0.16,-0.52l1.54,-0.5Z",name:"Belgium"},BF:{path:"M421.42,247.64l-0.11,0.95l0.34,1.16l1.4,1.71l0.07,1.1l0.32,0.37l2.55,0.51l-0.04,1.28l-0.38,0.53l-1.07,0.21l-0.72,1.18l-0.63,0.21l-3.22,-0.25l-0.94,0.39l-5.4,-0.05l-0.39,0.38l0.16,2.73l-1.23,-0.43l-1.17,0.1l-0.89,0.57l-2.27,-1.72l-0.13,-1.11l0.61,-0.96l0.02,-0.93l1.87,-1.98l0.44,-1.81l0.43,-0.39l1.28,0.26l1.05,-0.52l0.47,-0.73l1.84,-1.09l0.55,-0.83l2.2,-1.0l1.15,-0.3l0.72,0.45l1.13,-0.01Z",name:"Burkina Faso"},BG:{path:"M491.65,168.18l-0.86,0.88l-0.91,2.17l0.48,1.34l-1.6,-0.24l-2.55,0.95l-0.28,1.51l-1.8,0.22l-2.0,-1.0l-1.92,0.79l-1.42,-0.07l-0.15,-1.63l-1.05,-0.97l0.0,-0.8l1.2,-1.57l0.01,-0.56l-1.14,-1.23l-0.05,-0.94l0.88,0.97l0.88,-0.2l1.91,0.47l3.68,0.16l1.42,-0.81l2.72,-0.66l2.55,1.24Z",name:"Bulgaria"},BA:{path:"M463.49,163.65l2.1,0.5l1.72,-0.03l1.52,0.68l-0.36,0.78l0.08,0.45l1.04,1.02l-0.25,0.98l-1.81,1.15l-0.38,1.38l-1.67,-0.87l-0.89,-1.2l-2.11,-1.83l-1.63,-2.22l0.23,-0.57l0.48,0.38l0.55,-0.06l0.43,-0.51l0.94,-0.06Z",name:"Bosnia and Herz."},BN:{path:"M707.48,273.58l0.68,-0.65l1.41,-0.91l-0.15,1.63l-0.81,-0.05l-0.61,0.58l-0.53,-0.6Z",name:"Brunei"},BO:{path:"M263.83,340.69l-3.09,-0.23l-0.38,0.23l-0.7,1.52l-1.31,-1.53l-3.28,-0.64l-2.37,2.4l-1.31,0.26l-0.88,-3.26l-1.3,-2.86l0.74,-2.37l-0.13,-0.43l-1.2,-1.01l-0.37,-1.89l-1.08,-1.55l1.45,-2.56l-0.96,-2.33l0.47,-1.06l-0.34,-0.73l0.91,-1.32l0.16,-3.84l0.5,-1.18l-1.81,-3.41l2.46,0.07l0.8,-0.85l3.4,-1.91l2.66,-0.35l-0.19,1.38l0.3,1.07l-0.05,1.97l2.72,2.27l2.88,0.49l0.89,0.86l1.79,0.58l0.98,0.7l1.71,0.05l1.17,0.61l0.6,2.7l-0.7,0.54l0.96,2.99l0.37,0.28l4.3,0.1l-0.25,1.2l0.27,1.02l1.43,0.9l0.5,1.35l-0.41,1.86l-0.65,1.08l0.12,1.35l-2.69,-1.65l-2.4,-0.03l-4.36,0.76l-1.49,2.5l-0.11,1.52l-0.75,2.37Z",name:"Bolivia"},JP:{path:"M781.12,166.87l1.81,0.68l1.62,-0.97l0.39,2.42l-3.35,0.75l-2.23,2.88l-3.63,-1.9l-0.56,0.2l-1.26,3.05l-2.16,0.03l-0.29,-2.51l1.08,-2.03l2.45,-0.16l0.37,-0.33l1.25,-5.94l2.47,2.71l2.03,1.12ZM773.56,187.34l-0.91,2.22l0.37,1.52l-1.14,1.75l-3.02,1.26l-4.58,0.27l-3.34,3.01l-1.25,-0.8l-0.09,-1.9l-0.46,-0.38l-4.35,0.62l-3.0,1.32l-2.85,0.05l-0.37,0.27l0.13,0.44l2.32,1.89l-1.54,4.34l-1.26,0.9l-0.79,-0.7l0.56,-2.27l-0.21,-0.45l-1.47,-0.75l-0.74,-1.4l2.12,-0.84l1.26,-1.7l2.45,-1.42l1.83,-1.91l4.78,-0.81l2.6,0.57l0.44,-0.21l2.39,-4.66l1.29,1.06l0.5,0.01l5.1,-4.02l1.69,-3.73l-0.38,-3.4l0.9,-1.61l2.14,-0.44l1.23,3.72l-0.07,2.18l-2.23,2.84l-0.04,3.16ZM757.78,196.26l0.19,0.56l-1.01,1.21l-1.16,-0.68l-1.28,0.65l-0.69,1.45l-1.02,-0.5l0.01,-0.93l1.14,-1.38l1.57,0.14l0.85,-0.98l1.4,0.46Z",name:"Japan"},BI:{path:"M495.45,295.49l-1.08,-2.99l1.14,-0.11l0.64,-1.19l0.76,0.09l0.65,1.83l-2.1,2.36Z",name:"Burundi"},BJ:{path:"M429.57,255.75l-0.05,0.8l0.5,1.34l-0.42,0.86l0.17,0.79l-1.81,2.12l-0.57,1.76l-0.08,5.42l-1.41,0.2l-0.48,-1.36l0.11,-5.71l-0.52,-0.7l-0.2,-1.35l-1.48,-1.48l0.21,-0.9l0.89,-0.43l0.42,-0.92l1.27,-0.36l1.22,-1.34l0.61,-0.0l1.62,1.24Z",name:"Benin"},BT:{path:"M650.32,213.86l0.84,0.71l-0.12,1.1l-3.76,-0.11l-1.57,0.4l-1.93,-0.87l1.48,-1.96l1.13,-0.57l1.63,0.57l1.33,0.08l0.99,0.65Z",name:"Bhutan"},JM:{path:"M228.38,239.28l-0.8,0.4l-2.26,-1.06l0.84,-0.23l2.14,0.3l1.17,0.56l-1.08,0.03Z",name:"Jamaica"},BW:{path:"M483.92,330.07l2.27,4.01l2.83,2.86l0.96,0.31l0.78,2.43l2.13,0.61l1.02,0.76l-3.0,1.64l-2.32,2.02l-1.54,2.69l-1.52,0.45l-0.64,1.94l-1.34,0.52l-1.85,-0.12l-1.21,-0.74l-1.35,-0.3l-1.22,0.62l-0.75,1.37l-2.31,1.9l-1.4,0.21l-0.35,-0.59l0.16,-1.75l-1.48,-2.54l-0.62,-0.43l-0.0,-7.1l2.08,-0.08l0.39,-0.4l0.07,-8.9l5.19,-0.93l0.8,0.89l0.51,0.07l1.5,-0.95l2.21,-0.49Z",name:"Botswana"},BR:{path:"M259.98,275.05l3.24,0.7l0.65,-0.53l4.55,-1.32l1.08,-1.06l-0.02,-0.63l0.55,-0.05l0.28,0.28l-0.26,0.87l0.22,0.48l0.73,0.32l0.4,0.81l-0.62,0.86l-0.4,2.13l0.82,2.56l1.69,1.43l1.43,0.2l3.17,-1.68l3.18,0.3l0.65,-0.75l-0.27,-0.92l1.9,-0.09l2.39,0.99l1.06,-0.61l0.84,0.78l1.2,-0.18l1.18,-1.06l0.84,-1.94l1.36,-2.11l0.37,-0.05l1.89,5.45l1.33,0.59l0.05,1.28l-1.77,1.94l0.02,0.56l1.02,0.87l4.07,0.36l0.08,2.16l0.66,0.29l1.74,-1.5l6.97,2.32l1.02,1.22l-0.35,1.18l0.49,0.5l2.81,-0.74l4.77,1.3l3.75,-0.08l3.57,2.0l3.29,2.86l1.93,0.72l2.12,0.12l0.71,0.62l1.21,4.51l-0.95,3.98l-4.72,5.06l-1.64,2.92l-1.72,2.05l-0.8,0.3l-0.72,2.03l0.18,4.75l-0.94,5.53l-0.81,1.13l-0.43,3.36l-2.55,3.5l-0.4,2.51l-1.86,1.04l-0.67,1.53l-2.54,0.01l-3.94,1.01l-1.83,1.2l-2.87,0.82l-3.03,2.19l-2.2,2.83l-0.36,2.0l0.4,1.58l-0.44,2.6l-0.51,1.2l-1.77,1.54l-2.75,4.78l-3.83,3.42l-1.24,2.74l-1.18,1.15l-0.36,-0.83l0.95,-1.14l0.01,-0.5l-1.52,-1.97l-4.56,-3.32l-1.03,-0.0l-2.38,-2.02l-0.81,-0.0l5.34,-5.45l3.77,-2.58l0.22,-2.46l-1.35,-1.81l-0.91,0.07l0.58,-2.33l0.01,-1.54l-1.11,-0.83l-1.75,0.3l-0.44,-3.11l-0.52,-0.95l-1.88,-0.88l-1.24,0.47l-2.17,-0.41l0.15,-3.21l-0.62,-1.34l0.66,-0.73l-0.22,-1.34l0.66,-1.13l0.44,-2.04l-0.61,-1.83l-1.4,-0.86l-0.2,-0.75l0.34,-1.39l-0.38,-0.5l-4.52,-0.1l-0.72,-2.22l0.59,-0.42l-0.03,-1.1l-0.5,-0.87l-0.32,-1.7l-1.45,-0.76l-1.63,-0.02l-1.05,-0.72l-1.6,-0.48l-1.13,-0.99l-2.69,-0.4l-2.47,-2.06l0.13,-4.35l-0.45,-0.45l-3.46,0.5l-3.44,1.94l-0.6,0.74l-2.9,-0.17l-1.47,0.42l-0.72,-0.18l0.15,-3.52l-0.63,-0.34l-1.94,1.41l-1.87,-0.06l-0.83,-1.18l-1.37,-0.26l0.21,-1.01l-1.35,-1.49l-0.88,-1.91l0.56,-0.6l-0.0,-0.81l1.29,-0.62l0.22,-0.43l-0.22,-1.19l0.61,-0.91l0.15,-0.99l2.65,-1.58l1.99,-0.47l0.42,-0.36l2.06,0.11l0.42,-0.33l1.19,-8.0l-0.41,-1.56l-1.1,-1.0l0.01,-1.33l1.91,-0.42l0.08,-0.96l-0.33,-0.43l-1.14,-0.2l-0.02,-0.83l4.47,0.05l0.82,-0.67l0.82,1.81l0.8,0.07l1.15,1.1l2.26,-0.05l0.71,-0.83l2.78,-0.96l0.48,-1.13l1.6,-0.64l0.24,-0.47l-0.48,-0.82l-1.83,-0.19l-0.36,-3.22Z",name:"Brazil"},BS:{path:"M226.4,223.87l-0.48,-1.15l-0.84,-0.75l0.36,-1.11l0.95,1.95l0.01,1.06ZM225.56,216.43l-1.87,0.29l-0.04,-0.22l0.74,-0.14l1.17,0.06Z",name:"Bahamas"},BY:{path:"M493.84,128.32l0.29,0.7l0.49,0.23l1.19,-0.38l2.09,0.72l0.19,1.26l-0.45,1.24l1.57,2.26l0.89,0.59l0.17,0.81l1.58,0.56l0.4,0.5l-0.53,0.41l-1.87,-0.11l-0.73,0.38l-0.13,0.52l1.04,2.74l-1.91,0.26l-0.89,0.99l-0.11,1.18l-2.73,-0.04l-0.53,-0.62l-0.52,-0.08l-0.75,0.46l-0.91,-0.42l-1.92,-0.07l-2.75,-0.79l-2.6,-0.28l-2.0,0.07l-1.5,0.92l-0.67,0.07l-0.08,-1.22l-0.59,-1.19l1.36,-0.88l0.01,-1.35l-0.7,-1.41l-0.07,-1.0l2.16,-0.02l2.72,-1.3l0.75,-2.04l1.91,-1.04l0.2,-0.41l-0.19,-1.25l3.8,-1.78l2.3,0.77Z",name:"Belarus"},BZ:{path:"M198.03,244.38l0.1,-4.49l0.69,-0.06l0.74,-1.3l0.34,0.28l-0.4,1.3l0.17,0.58l-0.34,2.25l-1.3,1.42Z",name:"Belize"},RU:{path:"M491.55,115.25l2.55,-1.85l-0.01,-0.65l-2.2,-1.5l7.32,-6.76l1.03,-2.11l-0.13,-0.49l-3.46,-2.52l0.86,-2.7l-2.11,-2.81l1.56,-3.67l-2.77,-4.52l2.15,-2.99l-0.08,-0.55l-3.65,-2.73l0.3,-2.54l1.81,-0.37l4.26,-1.77l2.42,-1.45l4.06,2.61l6.79,1.04l9.34,4.85l1.78,1.88l0.14,2.46l-2.55,2.02l-3.9,1.06l-11.07,-3.14l-2.06,0.53l-0.13,0.7l3.94,2.94l0.31,5.86l0.26,0.36l5.14,2.24l0.58,-0.29l0.32,-1.94l-1.35,-1.78l1.13,-1.09l6.13,2.42l2.11,-0.98l0.18,-0.56l-1.51,-2.67l5.41,-3.76l2.07,0.22l2.26,1.41l0.57,-0.16l1.46,-2.87l-0.05,-0.44l-1.92,-2.32l1.12,-2.32l-1.32,-2.27l5.87,1.16l1.04,1.75l-2.59,0.43l-0.33,0.4l0.02,2.36l2.46,1.83l3.87,-0.91l0.86,-2.8l13.69,-5.65l0.99,0.11l-1.92,2.06l0.23,0.67l3.11,0.45l2.0,-1.48l4.56,-0.12l3.64,-1.73l2.65,2.44l0.56,-0.01l2.85,-2.88l-0.01,-0.57l-2.35,-2.29l0.9,-1.01l7.14,1.3l3.41,1.36l9.05,4.97l0.51,-0.11l1.67,-2.27l-0.05,-0.53l-2.43,-2.21l-0.06,-0.78l-0.34,-0.36l-2.52,-0.36l0.64,-1.93l-1.32,-3.46l-0.06,-1.21l4.48,-4.06l1.69,-4.29l1.6,-0.81l6.23,1.18l0.44,2.21l-2.29,3.64l0.06,0.5l1.47,1.39l0.76,3.0l-0.56,6.03l2.69,2.82l-0.96,2.57l-4.86,5.95l0.23,0.64l2.86,0.61l0.42,-0.17l0.93,-1.4l2.64,-1.03l0.87,-2.24l2.09,-1.96l0.07,-0.5l-1.36,-2.28l1.09,-2.69l-0.32,-0.55l-2.47,-0.33l-0.5,-2.06l1.94,-4.38l-0.06,-0.42l-2.96,-3.4l4.12,-2.88l0.16,-0.4l-0.51,-2.93l0.54,-0.05l1.13,2.25l-0.96,4.35l0.27,0.47l2.68,0.84l0.5,-0.51l-1.02,-2.99l3.79,-1.66l5.01,-0.24l4.53,2.61l0.48,-0.06l0.07,-0.48l-2.18,-3.82l-0.23,-4.67l3.98,-0.9l5.97,0.21l5.49,-0.64l0.27,-0.65l-1.83,-2.31l2.56,-2.9l2.87,-0.17l4.8,-2.47l6.54,-0.67l1.03,-1.42l6.25,-0.45l2.32,1.11l5.53,-2.7l4.5,0.08l0.39,-0.28l0.66,-2.15l2.26,-2.12l5.69,-2.11l3.21,1.29l-2.46,0.94l-0.25,0.42l0.34,0.35l5.41,0.77l0.61,2.33l0.58,0.25l2.2,-1.22l7.13,0.07l5.51,2.47l1.79,1.72l-0.53,2.24l-9.16,4.15l-1.97,1.52l0.16,0.71l6.77,1.91l2.16,-0.78l1.13,2.74l0.67,0.11l1.01,-1.15l3.81,-0.73l7.7,0.77l0.54,1.99l0.36,0.29l10.47,0.71l0.43,-0.38l0.13,-3.23l4.87,0.78l3.95,-0.02l3.83,2.4l1.03,2.71l-1.35,1.79l0.02,0.5l3.15,3.64l4.07,1.96l0.53,-0.18l2.23,-4.47l3.95,1.93l4.16,-1.21l4.73,1.39l2.05,-1.26l3.94,0.62l0.43,-0.55l-1.68,-4.02l2.89,-1.8l22.31,3.03l2.16,2.75l6.55,3.51l10.29,-0.81l4.82,0.73l1.85,1.66l-0.29,3.08l0.25,0.41l3.08,1.26l3.56,-0.88l4.35,-0.11l4.8,0.87l4.57,-0.47l4.23,3.79l0.43,0.07l3.1,-1.4l0.16,-0.6l-1.88,-2.62l0.85,-1.52l7.71,1.21l5.22,-0.26l7.09,2.09l9.59,5.22l6.35,4.11l-0.2,2.38l1.88,1.41l0.6,-0.42l-0.48,-2.53l6.15,0.57l4.4,3.51l-1.97,1.43l-4.0,0.41l-0.36,0.39l-0.06,3.79l-0.74,0.62l-2.07,-0.11l-1.91,-1.39l-3.14,-1.11l-0.78,-1.85l-2.72,-0.68l-2.63,0.49l-1.04,-1.1l0.46,-1.31l-0.5,-0.51l-3.0,0.98l-0.22,0.58l0.99,1.7l-1.21,1.48l-3.04,1.68l-3.12,-0.28l-0.4,0.23l0.09,0.46l2.2,2.09l1.46,3.2l1.15,1.1l0.24,1.33l-0.42,0.67l-4.63,-0.77l-6.96,2.9l-2.19,0.44l-7.6,5.06l-0.84,1.45l-3.61,-2.37l-6.24,2.82l-0.94,-1.15l-0.53,-0.08l-2.28,1.52l-3.2,-0.49l-0.44,0.27l-0.78,2.37l-3.05,3.78l0.09,1.47l0.29,0.36l2.54,0.72l-0.29,4.53l-1.97,0.11l-0.35,0.26l-1.07,2.94l0.8,1.45l-3.91,1.58l-1.05,3.95l-3.48,0.77l-0.3,0.3l-0.72,3.29l-3.09,2.65l-0.7,-1.74l-2.44,-12.44l1.16,-4.71l2.04,-2.06l0.22,-1.64l3.8,-0.86l4.46,-4.61l4.28,-3.81l4.48,-3.01l2.17,-5.63l-0.42,-0.54l-3.04,0.33l-1.77,3.31l-5.86,3.86l-1.86,-4.25l-0.45,-0.23l-6.46,1.3l-6.47,6.44l-0.01,0.55l1.58,1.74l-8.24,1.17l0.15,-2.2l-0.34,-0.42l-3.89,-0.56l-3.25,1.81l-7.62,-0.62l-8.45,1.19l-17.71,15.41l0.22,0.7l3.74,0.41l1.36,2.17l2.43,0.76l1.88,-1.68l2.4,0.2l3.4,3.54l0.08,2.6l-1.95,3.42l-0.21,3.9l-1.1,5.06l-3.71,4.54l-0.87,2.21l-8.29,8.89l-3.19,1.7l-1.32,0.03l-1.45,-1.36l-0.49,-0.04l-2.27,1.5l0.41,-3.65l-0.59,-2.47l1.75,-0.89l2.91,0.53l0.42,-0.2l1.68,-3.03l0.87,-3.46l0.97,-1.18l1.32,-2.88l-0.45,-0.56l-4.14,0.95l-2.19,1.25l-3.41,-0.0l-1.06,-2.93l-2.97,-2.3l-4.28,-1.06l-1.75,-5.07l-2.66,-5.01l-2.29,-1.29l-3.75,-1.01l-3.44,0.08l-3.18,0.62l-2.24,1.77l0.05,0.66l1.18,0.69l0.02,1.43l-1.33,1.05l-2.26,3.51l-0.04,1.43l-3.16,1.84l-2.82,-1.16l-3.01,0.23l-1.35,-1.07l-1.5,-0.35l-3.9,2.31l-3.22,0.52l-2.27,0.79l-3.05,-0.51l-2.21,0.03l-1.48,-1.6l-2.6,-1.63l-2.63,-0.43l-5.46,1.01l-3.23,-1.25l-0.72,-2.57l-5.2,-1.24l-2.75,-1.36l-0.5,0.12l-2.59,3.45l0.84,2.1l-2.06,1.93l-3.41,-0.77l-2.42,-0.12l-1.83,-1.54l-2.53,-0.05l-2.42,-0.98l-3.86,1.57l-4.72,2.78l-3.3,0.75l-1.55,-1.92l-3.0,0.41l-1.11,-1.33l-1.62,-0.59l-1.31,-1.94l-1.38,-0.6l-3.7,0.79l-3.31,-1.83l-0.51,0.11l-0.99,1.29l-5.29,-8.05l-2.96,-2.48l0.65,-0.77l0.01,-0.51l-0.5,-0.11l-6.2,3.21l-1.84,0.15l0.15,-1.39l-0.26,-0.42l-3.22,-1.17l-2.46,0.7l-0.69,-3.16l-0.32,-0.31l-4.5,-0.75l-2.47,1.47l-6.19,1.27l-1.29,0.86l-9.51,1.3l-1.15,1.17l-0.03,0.53l1.47,1.9l-1.89,0.69l-0.22,0.56l0.31,0.6l-2.11,1.44l0.03,0.68l3.75,2.12l-0.39,0.98l-3.23,-0.13l-0.86,0.86l-3.09,-1.59l-3.97,0.07l-2.66,1.35l-8.32,-3.56l-4.07,0.06l-5.39,3.68l-0.39,2.0l-2.03,-1.5l-0.59,0.13l-2.0,3.59l0.57,0.93l-1.28,2.16l0.06,0.48l2.13,2.17l1.95,0.04l1.37,1.82l-0.23,1.46l0.25,0.43l0.83,0.33l-0.8,1.31l-2.49,0.62l-2.49,3.2l0.0,0.49l2.17,2.78l-0.15,2.18l2.5,3.24l-1.58,1.59l-0.7,-0.13l-1.63,-1.72l-2.29,-0.84l-0.94,-1.31l-2.34,-0.63l-1.48,0.4l-0.43,-0.47l-3.51,-1.48l-5.76,-1.01l-0.45,0.19l-2.89,-2.34l-2.9,-1.2l-1.53,-1.29l1.29,-0.43l2.08,-2.61l-0.05,-0.55l-0.89,-0.79l3.05,-1.06l0.27,-0.42l-0.07,-0.69l-0.49,-0.35l-1.73,0.39l0.04,-0.68l1.04,-0.72l2.66,-0.48l0.4,-1.32l-0.5,-1.6l0.92,-1.54l0.03,-1.17l-0.29,-0.37l-3.69,-1.06l-1.41,0.02l-1.42,-1.41l-2.19,0.38l-2.77,-1.01l-0.03,-0.59l-0.89,-1.43l-2.0,-0.32l-0.11,-0.54l0.49,-0.53l0.01,-0.53l-1.6,-1.9l-3.58,0.02l-0.88,0.73l-0.46,-0.07l-1.0,-2.79l2.22,-0.02l0.97,-0.74l0.07,-0.57l-0.9,-1.04l-1.35,-0.48l-0.11,-0.7l-0.95,-0.58l-1.38,-1.99l0.46,-0.98l-0.51,-1.96l-2.45,-0.84l-1.21,0.3l-0.46,-0.76l-2.46,-0.83l-0.72,-1.87l-0.21,-1.69l-0.99,-0.85l0.85,-1.17l-0.7,-3.21l1.66,-1.97l-0.16,-0.79ZM749.2,170.72l-0.6,0.4l-0.13,0.16l-0.01,-0.51l0.74,-0.05ZM874.85,67.94l-5.63,0.48l-0.26,-0.84l3.15,-1.89l1.94,0.01l3.19,1.16l-2.39,1.09ZM797.39,48.49l-2.0,1.36l-3.8,-0.42l-4.25,-1.8l0.35,-0.97l9.69,1.83ZM783.67,46.12l-1.63,3.09l-8.98,-0.13l-4.09,1.14l-4.54,-2.97l1.16,-3.01l3.05,-0.89l6.5,0.22l8.54,2.56ZM778.2,134.98l-0.56,-0.9l0.27,-0.12l0.29,1.01ZM778.34,135.48l0.94,3.53l-0.05,3.38l1.05,3.39l2.18,5.0l-2.89,-0.83l-0.49,0.26l-1.54,4.65l2.42,3.5l-0.04,1.13l-1.24,-1.24l-0.61,0.06l-1.09,1.61l-0.28,-1.61l0.27,-3.1l-0.28,-3.4l0.58,-2.47l0.11,-4.39l-1.46,-3.36l0.21,-4.32l2.15,-1.46l0.07,-0.34ZM771.95,56.61l1.76,-1.42l2.89,-0.42l3.28,1.71l0.14,0.6l-3.27,0.03l-4.81,-0.5ZM683.76,31.09l-13.01,1.93l4.03,-6.35l1.82,-0.56l1.73,0.34l5.99,2.98l-0.56,1.66ZM670.85,27.93l-5.08,0.64l-6.86,-1.57l-3.99,-2.05l-2.1,-4.16l-2.6,-0.87l5.72,-3.5l5.2,-1.28l4.69,2.85l5.59,5.4l-0.56,4.53ZM564.15,68.94l-0.64,0.17l-7.85,-0.57l-0.86,-2.04l-4.28,-1.17l-0.28,-1.94l2.27,-0.89l0.25,-0.39l-0.08,-2.38l4.81,-3.97l-0.15,-0.7l-1.47,-0.38l5.3,-3.81l0.15,-0.44l-0.58,-1.94l5.28,-2.51l8.21,-3.27l8.28,-0.96l4.35,-1.94l4.6,-0.64l1.36,1.61l-1.34,1.28l-16.43,4.94l-7.97,4.88l-7.74,9.63l0.66,4.14l4.16,3.27ZM548.81,18.48l-5.5,1.18l-0.58,1.02l-2.59,0.84l-2.13,-1.07l1.12,-1.42l-0.3,-0.65l-2.33,-0.07l1.68,-0.36l3.47,-0.06l0.42,1.29l0.66,0.16l1.38,-1.34l2.15,-0.88l2.94,1.01l-0.39,0.36ZM477.37,133.15l-4.08,0.05l-2.56,-0.32l0.33,-0.87l3.17,-1.03l3.24,0.96l-0.09,1.23Z",name:"Russia"},RW:{path:"M497.0,288.25l0.71,1.01l-0.11,1.09l-1.63,0.03l-1.04,1.39l-0.83,-0.11l0.51,-1.2l0.08,-1.34l0.42,-0.41l0.7,0.14l1.19,-0.61Z",name:"Rwanda"},RS:{path:"M469.4,163.99l0.42,-0.5l-0.01,-0.52l-1.15,-1.63l1.43,-0.62l1.33,0.12l1.17,1.06l0.46,1.13l1.34,0.64l0.35,1.35l1.46,0.9l0.76,-0.29l0.2,0.69l-0.48,0.78l0.22,1.12l1.05,1.22l-0.77,0.8l-0.37,1.52l-1.21,0.08l0.24,-0.64l-0.39,-0.54l-2.08,-1.64l-0.9,0.05l-0.48,0.94l-2.12,-1.37l0.53,-1.6l-1.11,-1.37l0.51,-1.1l-0.41,-0.57Z",name:"Serbia"},LT:{path:"M486.93,129.3l0.17,1.12l-1.81,0.98l-0.72,2.02l-2.47,1.18l-2.1,-0.02l-0.73,-1.05l-1.06,-0.3l-0.09,-1.87l-3.56,-1.13l-0.43,-2.36l2.48,-0.94l4.12,0.22l2.25,-0.31l0.52,0.69l1.24,0.21l2.19,1.56Z",name:"Lithuania"},LU:{path:"M436.08,149.45l-0.48,-0.07l0.3,-1.28l0.27,0.4l-0.09,0.96Z",name:"Luxembourg"},LR:{path:"M399.36,265.97l0.18,1.54l-0.48,0.99l0.08,0.47l2.47,1.8l-0.33,2.8l-2.65,-1.13l-5.78,-4.61l0.58,-1.32l2.1,-2.33l0.86,-0.22l0.77,1.14l-0.14,0.85l0.59,0.87l1.0,0.14l0.76,-0.99Z",name:"Liberia"},RO:{path:"M487.53,154.23l0.6,0.24l2.87,3.98l-0.17,2.69l0.45,1.42l1.32,0.81l1.35,-0.42l0.76,0.36l0.02,0.31l-0.83,0.45l-0.59,-0.22l-0.54,0.3l-0.62,3.3l-1.0,-0.22l-2.07,-1.13l-2.95,0.71l-1.25,0.76l-3.51,-0.15l-1.89,-0.47l-0.87,0.16l-0.82,-1.3l0.29,-0.26l-0.06,-0.64l-1.09,-0.34l-0.56,0.5l-1.05,-0.64l-0.39,-1.39l-1.36,-0.65l-0.35,-1.0l-0.83,-0.75l1.54,-0.54l2.66,-4.21l2.4,-1.24l2.96,0.34l1.48,0.73l0.79,-0.45l1.78,-0.3l0.75,-0.74l0.79,0.0Z",name:"Romania"},GW:{path:"M386.23,253.6l-0.29,0.84l0.15,0.6l-2.21,0.59l-0.86,0.96l-1.04,-0.83l-1.09,-0.23l-0.54,-1.06l-0.66,-0.49l2.41,-0.48l4.13,0.1Z",name:"Guinea-Bissau"},GT:{path:"M195.08,249.77l-2.48,-0.37l-1.03,-0.45l-1.14,-0.89l0.3,-0.99l-0.24,-0.68l0.96,-1.66l2.98,-0.01l0.4,-0.37l-0.19,-1.28l-1.67,-1.4l0.51,-0.4l0.0,-1.05l3.85,0.02l-0.21,4.53l0.4,0.43l1.46,0.38l-1.48,0.98l-0.35,0.7l0.12,0.57l-2.2,1.96Z",name:"Guatemala"},GR:{path:"M487.07,174.59l-0.59,1.43l-0.37,0.21l-2.84,-0.35l-3.03,0.77l-0.18,0.68l1.28,1.23l-0.61,0.23l-1.14,0.0l-1.2,-1.39l-0.63,0.03l-0.53,1.01l0.56,1.76l1.03,1.19l-0.56,0.38l-0.05,0.62l2.52,2.12l0.02,0.87l-1.78,-0.59l-0.48,0.56l0.5,1.0l-1.07,0.2l-0.3,0.53l0.75,2.01l-0.98,0.02l-1.84,-1.12l-1.37,-4.2l-2.21,-2.95l-0.11,-0.56l1.04,-1.28l0.2,-0.95l0.85,-0.66l0.03,-0.46l1.32,-0.21l1.01,-0.64l1.22,0.05l0.65,-0.56l2.26,-0.0l1.82,-0.75l1.85,1.0l2.28,-0.28l0.35,-0.39l0.01,-0.77l0.34,0.22ZM480.49,192.16l0.58,0.4l-0.68,-0.12l0.11,-0.28ZM482.52,192.82l2.51,0.06l0.24,0.32l-1.99,0.13l-0.77,-0.51Z",name:"Greece"},GQ:{path:"M448.79,279.62l0.02,2.22l-4.09,0.0l0.69,-2.27l3.38,0.05Z",name:"Eq. Guinea"},GY:{path:"M277.42,270.07l-0.32,1.83l-1.32,0.57l-0.23,0.46l-0.28,2.0l1.11,1.82l0.83,0.19l0.32,1.25l1.13,1.62l-1.21,-0.19l-1.08,0.71l-1.77,0.5l-0.44,0.46l-0.86,-0.09l-1.32,-1.01l-0.77,-2.27l0.36,-1.9l0.68,-1.23l-0.57,-1.17l-0.74,-0.43l0.12,-1.16l-0.9,-0.69l-1.1,0.09l-1.31,-1.48l0.53,-0.72l-0.04,-0.84l1.99,-0.86l0.05,-0.59l-0.71,-0.78l0.14,-0.57l1.66,-1.24l1.36,0.77l1.41,1.49l0.06,1.15l0.37,0.38l0.8,0.05l2.06,1.86Z",name:"Guyana"},GE:{path:"M521.71,168.93l5.29,0.89l4.07,2.01l1.41,-0.44l2.07,0.56l0.68,1.1l1.07,0.55l-0.12,0.59l0.98,1.29l-1.01,-0.13l-1.81,-0.83l-0.94,0.47l-3.23,0.43l-2.29,-1.39l-2.33,0.05l0.21,-0.97l-0.76,-2.26l-1.45,-1.12l-1.43,-0.39l-0.41,-0.42Z",name:"Georgia"},GB:{path:"M412.61,118.72l-2.19,3.22l-0.0,0.45l5.13,-0.3l-0.53,2.37l-2.2,3.12l0.29,0.63l2.37,0.21l2.33,4.3l1.76,0.69l2.2,5.12l2.94,0.77l-0.23,1.62l-1.15,0.88l-0.1,0.52l0.82,1.42l-1.86,1.43l-3.3,-0.02l-4.12,0.87l-1.04,-0.58l-0.47,0.06l-1.51,1.41l-2.12,-0.34l-1.86,1.18l-0.6,-0.29l3.19,-3.0l2.16,-0.69l0.28,-0.41l-0.34,-0.36l-3.73,-0.53l-0.4,-0.76l2.2,-0.87l0.17,-0.61l-1.26,-1.67l0.36,-1.7l3.38,0.28l0.43,-0.33l0.37,-1.99l-1.79,-2.49l-3.11,-0.72l-0.38,-0.59l0.79,-1.35l-0.04,-0.46l-0.82,-0.97l-0.61,0.01l-0.68,0.84l-0.1,-2.34l-1.23,-1.88l0.85,-3.47l1.77,-2.68l1.85,0.26l2.17,-0.22ZM406.26,132.86l-1.01,1.77l-1.57,-0.59l-1.16,0.01l0.37,-1.54l-0.39,-1.39l1.45,-0.1l2.3,1.84Z",name:"United Kingdom"},GA:{path:"M453.24,279.52l-0.08,0.98l0.7,1.29l2.36,0.24l-0.98,2.63l1.18,1.79l0.25,1.78l-0.29,1.52l-0.6,0.93l-1.84,-0.09l-1.23,-1.11l-0.66,0.23l-0.15,0.84l-1.42,0.26l-1.02,0.7l-0.11,0.52l0.77,1.35l-1.34,0.97l-3.94,-4.3l-1.44,-2.45l0.06,-0.6l0.54,-0.81l1.05,-3.46l4.17,-0.07l0.4,-0.4l-0.02,-2.66l2.39,0.21l1.25,-0.27Z",name:"Gabon"},GN:{path:"M391.8,254.11l0.47,0.8l1.11,-0.32l0.98,0.7l1.07,0.2l2.26,-1.22l0.64,0.44l1.13,1.56l-0.48,1.4l0.8,0.3l-0.08,0.48l0.46,0.68l-0.35,1.36l1.05,2.61l-1.0,0.69l0.03,1.41l-0.72,-0.06l-1.08,1.0l-0.24,-0.27l0.07,-1.11l-1.05,-1.54l-1.79,0.21l-0.35,-2.01l-1.6,-2.18l-2.0,-0.0l-1.31,0.54l-1.95,2.18l-1.86,-2.19l-1.2,-0.78l-0.3,-1.11l-0.8,-0.85l0.65,-0.72l0.81,-0.03l1.64,-0.8l0.23,-1.87l2.67,0.64l0.89,-0.3l1.21,0.15Z",name:"Guinea"},GM:{path:"M379.31,251.39l0.1,-0.35l2.43,-0.07l0.74,-0.61l0.51,-0.03l0.77,0.49l-1.03,-0.3l-1.87,0.9l-1.65,-0.04ZM384.03,250.91l0.91,0.05l0.75,-0.24l-0.59,0.31l-1.08,-0.13Z",name:"Gambia"},GL:{path:"M353.02,1.2l14.69,4.67l-3.68,1.89l-22.97,0.86l-0.36,0.27l0.12,0.43l1.55,1.18l8.79,-0.66l7.48,2.07l4.86,-1.77l1.66,1.73l-2.53,3.19l-0.01,0.48l0.46,0.15l6.35,-2.2l12.06,-2.31l7.24,1.13l1.09,1.99l-9.79,4.01l-1.44,1.32l-7.87,0.98l-0.35,0.41l0.38,0.38l5.07,0.24l-2.53,3.58l-2.07,3.81l0.08,6.05l2.57,3.11l-3.22,0.2l-4.12,1.66l-0.05,0.72l4.45,2.65l0.51,3.75l-2.3,0.4l-0.25,0.64l2.79,3.69l-4.82,0.31l-0.36,0.29l0.16,0.44l2.62,1.8l-0.59,1.22l-3.3,0.7l-3.45,0.01l-0.29,0.68l3.03,3.12l0.02,1.34l-4.4,-1.73l-1.72,1.35l0.15,0.66l3.31,1.15l3.13,2.71l0.81,3.16l-3.85,0.75l-4.89,-4.26l-0.47,-0.03l-0.17,0.44l0.79,2.86l-2.71,2.21l-0.13,0.44l0.37,0.27l8.73,0.34l-12.32,6.64l-7.24,1.48l-2.94,0.08l-2.69,1.75l-3.43,4.41l-5.24,2.84l-1.73,0.18l-7.12,2.1l-2.15,2.52l-0.13,2.99l-1.19,2.45l-4.01,3.09l-0.14,0.44l0.97,2.9l-2.28,6.48l-3.1,0.2l-3.83,-3.07l-4.86,-0.02l-2.25,-1.93l-1.7,-3.79l-4.3,-4.84l-1.21,-2.49l-0.44,-3.8l-3.32,-3.63l0.84,-2.86l-1.56,-1.7l2.28,-4.6l3.83,-1.74l1.03,-1.96l0.52,-3.47l-0.59,-0.41l-4.17,2.21l-2.07,0.58l-2.72,-1.28l-0.15,-2.71l0.85,-2.09l2.01,-0.06l5.06,1.2l0.46,-0.23l-0.14,-0.49l-6.54,-4.47l-2.67,0.55l-1.58,-0.86l2.56,-4.01l-0.03,-0.48l-1.5,-1.74l-4.98,-8.5l-3.13,-1.96l0.03,-1.88l-0.24,-0.37l-6.85,-3.02l-5.36,-0.38l-12.7,0.58l-2.78,-1.57l-3.66,-2.77l5.73,-1.45l5.0,-0.28l0.38,-0.38l-0.35,-0.41l-10.67,-1.38l-5.3,-2.06l0.25,-1.54l18.41,-5.26l1.22,-2.27l-0.25,-0.55l-6.14,-1.86l1.68,-1.77l8.55,-4.03l3.59,-0.63l0.3,-0.54l-0.88,-2.27l5.47,-1.47l7.65,-0.95l7.55,-0.05l3.04,1.85l6.48,-3.27l5.81,2.22l3.56,0.5l5.16,1.94l0.5,-0.21l-0.17,-0.52l-5.71,-3.13l0.28,-2.13l8.12,-3.6l8.7,0.28l3.35,-2.34l8.71,-0.6l19.93,0.8Z",name:"Greenland"},KW:{path:"M540.81,207.91l0.37,0.86l-0.17,0.76l0.6,1.53l-0.95,0.04l-0.82,-1.28l-1.57,-0.18l1.31,-1.88l1.22,0.17Z",name:"Kuwait"},GH:{path:"M420.53,257.51l-0.01,0.72l0.96,1.2l0.24,3.73l0.59,0.95l-0.51,2.1l0.19,1.41l1.02,2.21l-6.97,2.84l-1.8,-0.57l0.04,-0.89l-1.02,-2.04l0.61,-2.65l1.07,-2.32l-0.96,-6.47l5.01,0.07l0.94,-0.39l0.61,0.11Z",name:"Ghana"},OM:{path:"M568.09,230.93l-0.91,1.67l-1.22,0.04l-0.6,0.76l-0.41,1.51l0.27,1.58l-1.16,0.05l-1.56,0.97l-0.76,1.74l-1.62,0.05l-0.98,0.65l-0.17,1.15l-0.89,0.52l-1.49,-0.18l-2.4,0.94l-2.47,-5.4l7.35,-2.71l1.67,-5.23l-1.12,-2.09l0.05,-0.83l0.67,-1.0l0.07,-1.05l0.9,-0.42l-0.05,-2.07l0.7,-0.01l1.0,1.62l1.51,1.08l3.3,0.84l1.73,2.29l0.81,0.37l-1.23,2.35l-0.99,0.79Z",name:"Oman"},_2:{path:"M531.15,258.94l1.51,0.12l5.13,-0.95l5.3,-1.48l-0.01,4.4l-2.67,3.39l-1.85,0.01l-8.04,-2.94l-2.55,-3.17l1.12,-1.71l2.04,2.34Z",name:"Somaliland"},_1:{path:"M472.77,172.64l-1.08,-1.29l0.96,-0.77l0.29,-0.83l1.98,1.64l-0.36,0.67l-1.79,0.58Z",name:"Kosovo"},_0:{path:"M504.91,192.87l0.34,0.01l0.27,-0.07l-0.29,0.26l-0.31,-0.2Z",name:"N. Cyprus"},JO:{path:"M518.64,201.38l-5.14,1.56l-0.19,0.65l2.16,2.39l-0.89,1.14l-1.71,0.34l-1.71,1.8l-2.34,-0.37l1.21,-4.32l0.56,-4.07l2.8,0.94l4.46,-2.71l0.79,2.66Z",name:"Jordan"},HR:{path:"M455.59,162.84l1.09,0.07l-0.82,0.94l-0.27,-1.01ZM456.96,162.92l0.62,-0.41l1.73,0.45l0.42,-0.4l-0.01,-0.59l0.86,-0.52l0.2,-1.05l1.63,-0.68l2.57,1.68l2.07,0.6l0.87,-0.31l1.05,1.57l-0.52,0.63l-1.05,-0.56l-1.68,0.04l-2.1,-0.5l-1.29,0.06l-0.57,0.49l-0.59,-0.47l-0.62,0.16l-0.46,1.7l1.79,2.42l2.79,2.75l-1.18,-0.87l-2.21,-0.87l-1.67,-1.78l0.13,-0.63l-1.05,-1.19l-0.32,-1.27l-1.42,-0.43Z",name:"Croatia"},HT:{path:"M237.05,238.38l-1.16,0.43l-0.91,-0.55l0.05,-0.2l2.02,0.31ZM237.53,238.43l1.06,0.12l-0.05,0.01l-1.01,-0.12ZM239.25,238.45l0.79,-0.51l0.06,-0.62l-1.02,-1.0l0.02,-0.82l-0.3,-0.4l-0.93,-0.32l3.16,0.45l0.02,1.84l-0.48,0.34l-0.08,0.58l0.54,0.72l-1.78,-0.26Z",name:"Haiti"},HU:{path:"M462.08,157.89l0.65,-1.59l-0.09,-0.44l0.64,-0.0l0.39,-0.34l0.1,-0.69l1.75,0.87l2.32,-0.37l0.43,-0.66l3.49,-0.78l0.69,-0.78l0.57,-0.14l2.57,0.93l0.67,-0.23l1.03,0.65l0.08,0.37l-1.42,0.71l-2.59,4.14l-1.8,0.53l-1.68,-0.1l-2.74,1.23l-1.85,-0.54l-2.54,-1.66l-0.66,-1.1Z",name:"Hungary"},HN:{path:"M199.6,249.52l-1.7,-1.21l0.06,-0.94l3.04,-2.14l2.37,0.28l1.27,-0.09l1.1,-0.52l1.3,0.28l1.14,-0.25l1.38,0.37l2.23,1.37l-2.36,0.93l-1.23,-0.39l-0.88,1.3l-1.28,0.99l-0.98,-0.22l-0.42,0.52l-0.96,0.05l-0.36,0.41l0.04,0.88l-0.52,0.6l-0.3,0.04l-0.3,-0.55l-0.66,-0.31l0.11,-0.67l-0.48,-0.65l-0.87,-0.26l-0.73,0.2Z",name:"Honduras"},PR:{path:"M256.17,238.73l-0.26,0.27l-2.83,0.05l-0.07,-0.55l1.95,-0.1l1.22,0.33Z",name:"Puerto Rico"},PS:{path:"M509.21,203.07l0.1,-0.06l-0.02,0.03l-0.09,0.03ZM509.36,202.91l-0.02,-0.63l-0.33,-0.16l0.31,-1.09l0.24,0.1l-0.2,1.78Z",name:"Palestine"},PT:{path:"M401.84,187.38l-0.64,0.47l-1.13,-0.35l-0.91,0.17l0.28,-1.78l-0.24,-1.78l-1.25,-0.56l-0.45,-0.84l0.17,-1.66l1.01,-1.18l0.69,-2.92l-0.04,-1.39l-0.59,-1.9l1.3,-0.85l0.84,1.35l3.1,-0.3l0.46,0.99l-1.05,0.94l-0.03,2.16l-0.41,0.57l-0.08,1.1l-0.79,0.18l-0.26,0.59l0.91,1.6l-0.63,1.75l0.76,1.09l-1.1,1.52l0.07,1.05Z",name:"Portugal"},PY:{path:"M274.9,336.12l0.74,1.52l-0.16,3.45l0.32,0.41l2.64,0.5l1.11,-0.47l1.4,0.59l0.36,0.6l0.53,3.42l1.27,0.4l0.98,-0.38l0.51,0.27l-0.0,1.18l-1.21,5.32l-2.09,1.9l-1.8,0.4l-4.71,-0.98l2.2,-3.63l-0.32,-1.5l-2.78,-1.28l-3.03,-1.94l-2.07,-0.44l-4.34,-4.06l0.91,-2.9l0.08,-1.42l1.07,-2.04l4.13,-0.72l2.18,0.03l2.05,1.17l0.03,0.59Z",name:"Paraguay"},PA:{path:"M213.8,263.68l0.26,-1.52l-0.36,-0.26l-0.01,-0.49l0.44,-0.1l0.93,1.4l1.26,0.03l0.77,0.49l1.38,-0.23l2.51,-1.11l0.86,-0.72l3.45,0.85l1.4,1.18l0.41,1.74l-0.21,0.34l-0.53,-0.12l-0.47,0.29l-0.16,0.6l-0.68,-1.28l0.45,-0.49l-0.19,-0.66l-0.47,-0.13l-0.54,-0.84l-1.5,-0.75l-1.1,0.16l-0.75,0.99l-1.62,0.84l-0.18,0.96l0.85,0.97l-0.58,0.45l-0.69,0.08l-0.34,-1.18l-1.27,0.03l-0.71,-1.05l-2.59,-0.46Z",name:"Panama"},PG:{path:"M808.58,298.86l2.54,2.56l-0.13,0.26l-0.33,0.12l-0.87,-0.78l-1.22,-2.16ZM801.41,293.04l0.5,0.29l0.26,0.27l-0.49,-0.35l-0.27,-0.21ZM803.17,294.58l0.59,0.5l0.08,1.06l-0.29,-0.91l-0.38,-0.65ZM796.68,298.41l0.52,0.75l1.43,-0.19l2.27,-1.81l-0.01,-1.43l1.12,0.16l-0.04,1.1l-0.7,1.28l-1.12,0.18l-0.62,0.79l-2.46,1.11l-1.17,-0.0l-3.08,-1.25l3.41,0.0l0.45,-0.68ZM789.15,303.55l2.31,1.8l1.59,2.61l1.34,0.13l-0.06,0.66l0.31,0.43l1.06,0.24l0.06,0.65l2.25,1.05l-1.22,0.13l-0.72,-0.63l-4.56,-0.65l-3.22,-2.87l-1.49,-2.34l-3.27,-1.1l-2.38,0.72l-1.59,0.86l-0.2,0.42l0.27,1.55l-1.55,0.68l-1.36,-0.4l-2.21,-0.09l-0.08,-15.41l8.39,2.93l2.95,2.4l0.6,1.64l4.02,1.49l0.31,0.68l-1.76,0.21l-0.33,0.52l0.55,1.68Z",name:"Papua New Guinea"},PE:{path:"M244.96,295.21l-1.26,-0.07l-0.57,0.42l-1.93,0.45l-2.98,1.75l-0.36,1.36l-0.58,0.8l0.12,1.37l-1.24,0.59l-0.22,1.22l-0.62,0.84l1.04,2.27l1.28,1.44l-0.41,0.84l0.32,0.57l1.48,0.13l1.16,1.37l2.21,0.07l1.63,-1.08l-0.13,3.02l0.3,0.4l1.14,0.29l1.31,-0.34l1.9,3.59l-0.48,0.85l-0.17,3.85l-0.94,1.59l0.35,0.75l-0.47,1.07l0.98,1.97l-2.1,3.82l-0.98,0.5l-2.17,-1.28l-0.39,-1.16l-4.95,-2.58l-4.46,-2.79l-1.84,-1.51l-0.91,-1.84l0.3,-0.96l-2.11,-3.33l-4.82,-9.68l-1.04,-1.2l-0.87,-1.94l-3.4,-2.48l0.58,-1.18l-1.13,-2.23l0.66,-1.49l1.45,-1.15l-0.6,0.98l0.07,0.92l0.47,0.36l1.74,0.03l0.97,1.17l0.54,0.07l1.42,-1.03l0.6,-1.84l1.42,-2.02l3.04,-1.04l2.73,-2.62l0.86,-1.74l-0.1,-1.87l1.44,1.02l0.9,1.25l1.06,0.59l1.7,2.73l1.86,0.31l1.45,-0.61l0.96,0.39l1.36,-0.19l1.45,0.89l-1.4,2.21l0.31,0.61l0.59,0.05l0.47,0.5Z",name:"Peru"},PK:{path:"M615.09,192.34l-1.83,1.81l-2.6,0.39l-3.73,-0.68l-1.58,1.33l-0.09,0.42l1.77,4.39l1.7,1.23l-1.69,1.27l-0.12,2.14l-2.33,2.64l-1.6,2.8l-2.46,2.67l-3.03,-0.07l-2.76,2.83l0.05,0.6l1.5,1.11l0.26,1.9l1.44,1.5l0.37,1.68l-5.01,-0.01l-1.78,1.7l-1.42,-0.52l-0.76,-1.87l-2.27,-2.15l-11.61,0.86l0.71,-2.34l3.43,-1.32l0.25,-0.44l-0.21,-1.24l-1.2,-0.65l-0.28,-2.46l-2.29,-1.14l-1.28,-1.94l2.82,0.94l2.62,-0.38l1.42,0.33l0.76,-0.56l1.71,0.19l3.25,-1.14l0.27,-0.36l0.08,-2.19l1.18,-1.32l1.68,0.0l0.58,-0.82l1.6,-0.3l1.19,0.16l0.98,-0.78l0.02,-1.88l0.93,-1.47l1.48,-0.66l0.19,-0.55l-0.66,-1.25l2.04,-0.11l0.69,-1.01l-0.02,-1.16l1.11,-1.06l-0.17,-1.78l-0.49,-1.03l1.15,-0.98l5.42,-0.91l2.6,-0.82l1.6,1.16l0.97,2.34l3.45,0.97Z",name:"Pakistan"},PH:{path:"M737.01,263.84l0.39,2.97l-0.44,1.18l-0.55,-1.53l-0.67,-0.14l-1.17,1.28l0.65,2.09l-0.42,0.69l-2.48,-1.23l-0.57,-1.49l0.65,-1.03l-0.1,-0.54l-1.59,-1.19l-0.56,0.08l-0.65,0.87l-1.23,0.0l-1.58,0.97l0.83,-1.8l2.56,-1.42l0.65,0.84l0.45,0.13l1.9,-0.69l0.56,-1.11l1.5,-0.06l0.38,-0.43l-0.09,-1.19l1.21,0.71l0.36,2.02ZM733.59,256.58l0.05,0.75l0.08,0.26l-0.8,-0.42l-0.18,-0.71l0.85,0.12ZM734.08,256.1l-0.12,-1.12l-1.0,-1.27l1.36,0.03l0.53,0.73l0.51,2.04l-1.27,-0.4ZM733.76,257.68l0.38,0.98l-0.32,0.15l-0.07,-1.13ZM724.65,238.43l1.46,0.7l0.72,-0.31l-0.32,1.17l0.79,1.71l-0.57,1.84l-1.53,1.04l-0.39,2.25l0.56,2.04l1.63,0.57l1.16,-0.27l2.71,1.23l-0.19,1.08l0.76,0.84l-0.08,0.36l-1.4,-0.9l-0.88,-1.27l-0.66,0.0l-0.38,0.55l-1.6,-1.31l-2.15,0.36l-0.87,-0.39l0.07,-0.61l0.66,-0.55l-0.01,-0.62l-0.75,-0.59l-0.72,0.44l-0.74,-0.87l-0.39,-2.49l0.32,0.27l0.66,-0.28l0.26,-3.97l0.7,-2.02l1.14,0.0ZM731.03,258.87l-0.88,0.85l-1.19,1.94l-1.05,-1.19l0.93,-1.1l0.32,-1.47l0.52,-0.06l-0.27,1.15l0.22,0.45l0.49,-0.12l1.0,-1.32l-0.08,0.85ZM726.83,255.78l0.83,0.38l1.17,-0.0l-0.02,0.48l-2.0,1.4l0.03,-2.26ZM724.81,252.09l-0.38,1.27l-1.42,-1.95l1.2,0.05l0.6,0.63ZM716.55,261.82l1.1,-0.95l0.03,-0.03l-0.28,0.36l-0.85,0.61ZM719.22,259.06l0.04,-0.06l0.8,-1.53l0.16,0.75l-1.0,0.84Z",name:"Philippines"},PL:{path:"M468.44,149.42l-1.11,-1.54l-1.86,-0.33l-0.48,-1.05l-1.72,-0.37l-0.65,0.69l-0.72,-0.36l0.11,-0.61l-0.33,-0.46l-1.75,-0.27l-1.04,-0.93l-0.94,-1.94l0.16,-1.22l-0.62,-1.8l-0.78,-1.07l0.57,-1.04l-0.48,-1.43l1.41,-0.83l6.91,-2.71l2.14,0.5l0.52,0.91l5.51,0.44l4.55,-0.05l1.07,0.31l0.48,0.84l0.15,1.58l0.65,1.2l-0.01,0.99l-1.27,0.58l-0.19,0.54l0.73,1.48l0.08,1.55l1.2,2.76l-0.17,0.58l-1.23,0.44l-2.27,2.72l0.18,0.95l-1.97,-1.03l-1.98,0.4l-1.36,-0.28l-1.24,0.58l-1.07,-0.97l-1.16,0.24Z",name:"Poland"},ZM:{path:"M481.47,313.3l0.39,0.31l2.52,0.14l0.99,1.17l2.01,0.35l1.4,-0.64l0.69,1.17l1.78,0.33l1.84,2.35l2.23,0.18l0.4,-0.43l-0.21,-2.74l-0.62,-0.3l-0.48,0.32l-1.98,-1.17l0.72,-5.29l-0.51,-1.18l0.57,-1.3l3.68,-0.62l0.26,0.63l1.21,0.63l0.9,-0.22l2.16,0.67l1.33,0.71l1.07,1.02l0.56,1.87l-0.88,2.7l0.43,2.09l-0.73,0.87l-0.76,2.37l0.59,0.68l-6.6,1.83l-0.29,0.44l0.19,1.45l-1.68,0.35l-1.43,1.02l-0.38,0.87l-0.87,0.26l-3.48,3.69l-4.16,-0.53l-1.52,-1.0l-1.77,-0.13l-1.83,0.52l-3.04,-3.4l0.11,-7.59l4.82,0.03l0.39,-0.49l-0.18,-0.76l0.33,-0.83l-0.4,-1.36l0.24,-1.05Z",name:"Zambia"},EH:{path:"M384.42,230.28l0.25,-0.79l1.06,-1.29l0.8,-3.51l3.38,-2.78l0.7,-1.81l0.06,4.84l-1.98,0.2l-0.94,1.59l0.39,3.56l-3.7,-0.01ZM392.01,218.1l0.7,-1.8l1.77,-0.24l2.09,0.34l0.95,-0.62l1.28,-0.07l-0.0,2.51l-6.79,-0.12Z",name:"W. Sahara"},EE:{path:"M485.71,115.04l2.64,0.6l2.56,0.11l-1.6,1.91l0.61,3.54l-0.81,0.87l-1.78,-0.01l-3.22,-1.76l-1.8,0.45l0.21,-1.53l-0.58,-0.41l-0.69,0.34l-1.26,-1.03l-0.17,-1.63l2.83,-0.92l3.05,-0.52Z",name:"Estonia"},EG:{path:"M492.06,205.03l1.46,0.42l2.95,-1.64l2.04,-0.21l1.53,0.3l0.59,1.19l0.69,0.04l0.41,-0.64l1.81,0.58l1.95,0.16l1.04,-0.51l1.42,4.08l-2.03,4.54l-1.66,-1.77l-1.76,-3.85l-0.64,-0.12l-0.36,0.67l1.04,2.88l3.44,6.95l1.78,3.04l2.03,2.65l-0.36,0.53l0.23,2.01l2.7,2.19l-28.41,0.0l0.0,-18.96l-0.73,-2.2l0.59,-1.56l-0.32,-1.26l0.68,-0.99l3.06,-0.04l4.82,1.52Z",name:"Egypt"},ZA:{path:"M467.14,373.21l-0.13,-1.96l-0.68,-1.56l0.7,-0.68l-0.13,-2.33l-4.56,-8.19l0.77,-0.86l0.6,0.45l0.69,1.31l2.83,0.72l1.5,-0.26l2.24,-1.39l0.19,-9.55l1.35,2.3l-0.21,1.5l0.61,1.2l0.4,0.19l1.79,-0.27l2.6,-2.07l0.69,-1.32l0.96,-0.48l2.19,1.04l2.04,0.13l1.77,-0.65l0.85,-2.12l1.38,-0.33l1.59,-2.76l2.15,-1.89l3.41,-1.87l2.0,0.45l1.02,-0.28l0.99,0.2l1.75,5.29l-0.38,3.25l-0.81,-0.23l-1.0,0.46l-0.87,1.68l-0.05,1.16l1.97,1.84l1.47,-0.29l0.69,-1.18l1.09,0.01l-0.76,3.69l-0.58,1.09l-2.2,1.79l-3.17,4.76l-2.8,2.83l-3.57,2.88l-2.53,1.05l-1.22,0.14l-0.51,0.7l-1.18,-0.32l-1.39,0.5l-2.59,-0.52l-1.61,0.33l-1.18,-0.11l-2.55,1.1l-2.1,0.44l-1.6,1.07l-0.85,0.05l-0.93,-0.89l-0.93,-0.15l-0.97,-1.13l-0.25,0.05ZM491.45,364.19l0.62,-0.93l1.48,-0.59l1.18,-2.19l-0.07,-0.49l-1.99,-1.69l-1.66,0.56l-1.43,1.14l-1.34,1.73l0.02,0.51l1.88,2.11l1.31,-0.16Z",name:"South Africa"},EC:{path:"M231.86,285.53l0.29,1.59l-0.69,1.45l-2.61,2.51l-3.13,1.11l-1.53,2.18l-0.49,1.68l-1.0,0.73l-1.02,-1.11l-1.78,-0.16l0.67,-1.15l-0.24,-0.86l1.25,-2.13l-0.54,-1.09l-0.67,-0.08l-0.72,0.87l-0.87,-0.64l0.35,-0.69l-0.36,-1.96l0.81,-0.51l0.45,-1.51l0.92,-1.57l-0.07,-0.97l2.65,-1.33l2.75,1.35l0.77,1.05l2.12,0.35l0.76,-0.32l1.96,1.21Z",name:"Ecuador"},AL:{path:"M470.32,171.8l0.74,0.03l0.92,0.89l-0.17,1.95l0.36,1.28l1.01,0.82l-1.82,2.83l-0.19,-0.61l-1.25,-0.89l-0.18,-1.2l0.53,-2.82l-0.54,-1.47l0.6,-0.83Z",name:"Albania"},AO:{path:"M461.55,300.03l1.26,3.15l1.94,2.36l2.47,-0.53l1.25,0.32l0.44,-0.18l0.93,-1.92l1.31,-0.08l0.41,-0.44l0.47,-0.0l-0.1,0.41l0.39,0.49l2.65,-0.02l0.03,1.19l0.48,1.01l-0.34,1.52l0.18,1.55l0.83,1.04l-0.13,2.85l0.54,0.39l3.96,-0.41l-0.1,1.79l0.39,1.05l-0.24,1.43l-4.7,-0.03l-0.4,0.39l-0.12,8.13l2.92,3.49l-3.83,0.88l-5.89,-0.36l-1.88,-1.24l-10.47,0.22l-1.3,-1.01l-1.85,-0.16l-2.4,0.77l-0.15,-1.06l0.33,-2.16l1.0,-3.45l1.35,-3.2l2.24,-2.8l0.33,-2.06l-0.13,-1.53l-0.8,-1.08l-1.21,-2.87l0.87,-1.62l-1.27,-4.12l-1.17,-1.53l2.47,-0.63l7.03,0.03ZM451.71,298.87l-0.47,-1.25l1.25,-1.11l0.32,0.3l-0.99,1.03l-0.12,1.03Z",name:"Angola"},KZ:{path:"M552.8,172.89l0.46,-1.27l-0.48,-1.05l-2.96,-1.19l-1.06,-2.58l-1.37,-0.87l-0.03,-0.3l1.95,0.23l0.45,-0.38l0.08,-1.96l1.75,-0.41l2.1,0.45l0.48,-0.33l0.45,-3.04l-0.45,-2.09l-0.41,-0.31l-2.42,0.15l-2.36,-0.73l-2.87,1.37l-2.17,0.61l-0.85,-0.34l0.13,-1.61l-1.6,-2.12l-2.02,-0.08l-1.78,-1.82l1.29,-2.18l-0.57,-0.95l1.62,-2.91l2.21,1.63l0.63,-0.27l0.29,-2.22l4.92,-3.43l3.71,-0.08l8.4,3.6l2.92,-1.36l3.77,-0.06l3.11,1.66l0.51,-0.11l0.6,-0.81l3.31,0.13l0.39,-0.25l0.63,-1.57l-0.17,-0.5l-3.5,-1.98l1.87,-1.27l-0.13,-1.03l1.98,-0.72l0.18,-0.62l-1.59,-2.06l0.81,-0.82l9.23,-1.18l1.33,-0.88l6.18,-1.26l2.26,-1.42l4.08,0.68l0.73,3.33l0.51,0.3l2.48,-0.8l2.79,1.02l-0.17,1.56l0.43,0.44l2.55,-0.24l4.89,-2.53l0.03,0.32l3.15,2.61l5.56,8.47l0.65,0.02l1.12,-1.46l3.15,1.74l3.76,-0.78l1.15,0.49l1.14,1.8l1.84,0.76l0.99,1.29l3.35,-0.25l1.02,1.52l-1.6,1.81l-1.93,0.28l-0.34,0.38l-0.11,3.05l-1.13,1.16l-4.75,-1.0l-0.46,0.27l-1.76,5.47l-1.1,0.59l-4.91,1.23l-0.27,0.54l2.1,4.97l-1.37,0.63l-0.23,0.41l0.13,1.13l-0.88,-0.25l-1.42,-1.13l-7.89,-0.4l-0.92,0.31l-3.73,-1.22l-1.42,0.63l-0.53,1.66l-3.72,-0.94l-1.85,0.43l-0.76,1.4l-4.65,2.62l-1.13,2.08l-0.44,0.01l-0.92,-1.4l-2.87,-0.09l-0.45,-2.14l-0.38,-0.32l-0.8,-0.01l0.0,-2.96l-3.0,-2.22l-7.31,0.58l-2.35,-2.68l-6.71,-3.69l-6.45,1.83l-0.29,0.39l0.1,10.85l-0.7,0.08l-1.62,-2.17l-1.83,-0.96l-3.11,0.59l-0.64,0.51Z",name:"Kazakhstan"},ET:{path:"M516.04,247.79l1.1,0.84l1.63,-0.45l0.68,0.47l1.63,0.03l2.01,0.94l1.73,1.66l1.64,2.07l-1.52,2.04l0.16,1.72l0.39,0.38l2.05,0.0l-0.36,1.03l2.86,3.58l8.32,3.08l1.31,0.02l-6.32,6.75l-3.1,0.11l-2.36,1.77l-1.47,0.04l-0.86,0.79l-1.38,-0.0l-1.32,-0.81l-2.29,1.05l-0.76,0.98l-3.29,-0.41l-3.07,-2.07l-1.8,-0.07l-0.62,-0.6l0.0,-1.24l-0.28,-0.38l-1.15,-0.37l-1.4,-2.59l-1.19,-0.68l-0.47,-1.0l-1.27,-1.23l-1.16,-0.22l0.43,-0.72l1.45,-0.28l0.41,-0.95l-0.03,-2.21l0.68,-2.44l1.05,-0.63l1.43,-3.06l1.57,-1.37l1.02,-2.51l0.35,-1.88l2.52,0.46l0.44,-0.24l0.58,-1.43Z",name:"Ethiopia"},ZW:{path:"M498.91,341.09l-1.11,-0.22l-0.92,0.28l-2.09,-0.44l-1.5,-1.11l-1.89,-0.43l-0.62,-1.4l-0.01,-0.84l-0.3,-0.38l-0.97,-0.25l-2.71,-2.74l-1.92,-3.32l3.83,0.45l3.73,-3.82l1.08,-0.44l0.26,-0.77l1.25,-0.9l1.41,-0.26l0.5,0.89l1.99,-0.05l1.72,1.17l1.11,0.17l1.05,0.66l0.01,2.99l-0.59,3.76l0.38,0.86l-0.23,1.23l-0.39,0.35l-0.63,1.81l-2.43,2.75Z",name:"Zimbabwe"},ES:{path:"M416.0,169.21l1.07,1.17l4.61,1.38l1.06,-0.57l2.6,1.26l2.71,-0.3l0.09,1.12l-2.14,1.8l-3.11,0.61l-0.31,0.31l-0.2,0.89l-1.54,1.69l-0.97,2.4l0.84,1.74l-1.32,1.27l-0.48,1.68l-1.88,0.65l-1.66,2.07l-5.36,-0.01l-1.79,1.08l-0.89,0.98l-0.88,-0.17l-0.79,-0.82l-0.68,-1.59l-2.37,-0.63l-0.11,-0.5l1.21,-1.82l-0.77,-1.13l0.61,-1.68l-0.76,-1.62l0.87,-0.49l0.09,-1.25l0.42,-0.6l0.03,-2.11l0.99,-0.69l0.13,-0.5l-1.03,-1.73l-1.46,-0.11l-0.61,0.38l-1.06,0.0l-0.52,-1.23l-0.53,-0.21l-1.32,0.67l-0.01,-1.49l-0.75,-0.96l3.03,-1.88l2.99,0.53l3.32,-0.02l2.63,0.51l6.01,-0.06Z",name:"Spain"},ER:{path:"M520.38,246.23l3.42,2.43l3.5,3.77l0.84,0.54l-0.95,-0.01l-3.51,-3.89l-2.33,-1.15l-1.73,-0.07l-0.91,-0.51l-1.26,0.51l-1.34,-1.02l-0.61,0.17l-0.66,1.61l-2.35,-0.43l-0.17,-0.67l1.29,-5.29l0.61,-0.61l1.95,-0.53l0.87,-1.01l1.17,2.41l0.68,2.33l1.49,1.43Z",name:"Eritrea"},ME:{path:"M468.91,172.53l-1.22,-1.02l0.47,-1.81l0.89,-0.72l2.26,1.51l-0.5,0.57l-0.75,-0.27l-1.14,1.73Z",name:"Montenegro"},MD:{path:"M488.41,153.73l1.4,-0.27l1.72,0.93l1.07,0.15l0.85,0.65l-0.14,0.84l0.96,0.85l1.12,2.47l-1.15,-0.07l-0.66,-0.41l-0.52,0.25l-0.09,0.86l-1.08,1.89l-0.27,-0.86l0.25,-1.34l-0.16,-1.6l-3.29,-4.34Z",name:"Moldova"},MG:{path:"M545.91,319.14l0.4,3.03l0.62,1.21l-0.21,1.02l-0.57,-0.8l-0.69,-0.01l-0.47,0.76l0.41,2.12l-0.18,0.87l-0.73,0.78l-0.15,2.14l-4.71,15.2l-1.06,2.88l-3.92,1.64l-3.12,-1.49l-0.6,-1.21l-0.19,-2.4l-0.86,-2.05l-0.21,-1.77l0.38,-1.62l1.21,-0.75l0.01,-0.76l1.19,-2.04l0.23,-1.66l-1.06,-2.99l-0.19,-2.21l0.81,-1.33l0.32,-1.46l4.63,-1.22l3.44,-3.0l0.85,-1.4l-0.08,-0.7l0.78,-0.04l1.38,-1.77l0.13,-1.64l0.45,-0.61l1.16,1.69l0.59,1.6Z",name:"Madagascar"},MA:{path:"M378.78,230.02l0.06,-0.59l0.92,-0.73l0.82,-1.37l-0.09,-1.04l0.79,-1.7l1.31,-1.58l0.96,-0.59l0.66,-1.55l0.09,-1.47l0.81,-1.48l1.72,-1.07l1.55,-2.69l1.16,-0.96l2.44,-0.39l1.94,-1.82l1.31,-0.78l2.09,-2.28l-0.51,-3.65l1.24,-3.7l1.5,-1.75l4.46,-2.57l2.37,-4.47l1.44,0.01l1.68,1.21l2.32,-0.19l3.47,0.65l0.8,1.54l0.16,1.71l0.86,2.96l0.56,0.59l-0.26,0.61l-3.05,0.44l-1.26,1.05l-1.33,0.22l-0.33,0.37l-0.09,1.78l-2.68,1.0l-1.07,1.42l-4.47,1.13l-4.04,2.01l-0.54,4.64l-1.15,0.06l-0.92,0.61l-1.96,-0.35l-2.42,0.54l-0.74,1.9l-0.86,0.4l-1.14,3.26l-3.53,3.01l-0.8,3.55l-0.96,1.1l-0.29,0.82l-4.95,0.18Z",name:"Morocco"},UZ:{path:"M598.64,172.75l-1.63,1.52l0.06,0.64l1.85,1.12l1.97,-0.64l2.21,1.17l-2.52,1.68l-2.59,-0.22l-0.18,-0.41l0.46,-1.23l-0.45,-0.53l-3.35,0.69l-2.1,3.51l-1.87,-0.12l-1.03,1.51l0.22,0.55l1.64,0.62l0.46,1.83l-1.19,2.49l-2.66,-0.53l0.05,-1.36l-0.26,-0.39l-3.3,-1.23l-2.56,-1.4l-4.4,-3.34l-1.34,-3.14l-1.08,-0.6l-2.58,0.13l-0.69,-0.44l-0.47,-2.52l-3.37,-1.6l-0.43,0.05l-2.07,1.72l-2.1,1.01l-0.21,0.47l0.28,1.01l-1.91,0.03l-0.09,-10.5l5.99,-1.7l6.19,3.54l2.71,2.84l7.05,-0.67l2.71,2.01l-0.17,2.81l0.39,0.42l0.9,0.02l0.44,2.14l0.38,0.32l2.94,0.09l0.95,1.42l1.28,-0.24l1.05,-2.04l4.43,-2.5Z",name:"Uzbekistan"},MM:{path:"M673.9,230.21l-1.97,1.57l-0.57,0.96l-1.4,0.6l-1.36,1.05l-1.99,0.36l-1.08,2.66l-0.91,0.4l-0.19,0.55l1.21,2.27l2.52,3.43l-0.79,1.91l-0.74,0.41l-0.17,0.52l0.65,1.37l1.61,1.95l0.25,2.58l0.9,2.13l-1.92,3.57l0.68,-2.25l-0.81,-1.74l0.19,-2.65l-1.05,-1.53l-1.24,-6.17l-1.12,-2.26l-0.6,-0.13l-4.34,3.02l-2.39,-0.65l0.77,-2.84l-0.52,-2.61l-1.91,-2.96l0.25,-0.75l-0.29,-0.51l-1.33,-0.3l-1.61,-1.93l-0.1,-1.3l0.82,-0.24l0.04,-1.64l1.02,-0.52l0.21,-0.45l-0.23,-0.95l0.54,-0.96l0.08,-2.22l1.46,0.45l0.47,-0.2l1.12,-2.19l0.16,-1.35l1.33,-2.16l-0.0,-1.52l2.89,-1.66l1.63,0.44l0.5,-0.44l-0.17,-1.4l0.64,-0.36l0.08,-1.04l0.77,-0.11l0.71,1.35l1.06,0.69l-0.03,3.86l-2.38,2.37l-0.3,3.15l0.46,0.43l2.28,-0.38l0.51,2.08l1.47,0.67l-0.6,1.8l0.19,0.48l2.97,1.48l1.64,-0.55l0.02,0.32Z",name:"Myanmar"},ML:{path:"M392.61,254.08l-0.19,-2.37l-0.99,-0.87l-0.44,-1.3l-0.09,-1.28l0.81,-0.58l0.35,-1.24l2.37,0.65l1.31,-0.47l0.86,0.15l0.66,-0.56l9.83,-0.04l0.38,-0.28l0.56,-1.8l-0.44,-0.65l-2.35,-21.95l3.27,-0.04l16.7,11.38l0.74,1.31l2.5,1.09l0.02,1.38l0.44,0.39l2.34,-0.21l0.01,5.38l-1.28,1.61l-0.26,1.49l-5.31,0.57l-1.07,0.92l-2.9,0.1l-0.86,-0.48l-1.38,0.36l-2.4,1.08l-0.6,0.87l-1.85,1.09l-0.43,0.7l-0.79,0.39l-1.44,-0.21l-0.81,0.84l-0.34,1.64l-1.91,2.02l-0.06,1.03l-0.67,1.22l0.13,1.16l-0.97,0.39l-0.23,-0.64l-0.52,-0.24l-1.35,0.4l-0.34,0.55l-2.69,-0.28l-0.37,-0.35l-0.02,-0.9l-0.65,-0.35l0.45,-0.64l-0.03,-0.53l-2.12,-2.44l-0.76,-0.01l-2.0,1.16l-0.78,-0.15l-0.8,-0.67l-1.21,0.23Z",name:"Mali"},MN:{path:"M676.61,146.48l3.81,1.68l5.67,-1.0l2.37,0.41l2.34,1.5l1.79,1.75l2.29,-0.03l3.12,0.52l2.47,-0.81l3.41,-0.59l3.53,-2.21l1.25,0.29l1.53,1.13l2.27,-0.21l-2.66,5.01l0.64,1.68l0.47,0.21l1.32,-0.38l2.38,0.48l2.02,-1.11l1.76,0.89l2.06,2.02l-0.13,0.53l-1.72,-0.29l-3.77,0.46l-1.88,0.99l-1.76,1.99l-3.71,1.17l-2.45,1.6l-3.83,-0.87l-0.41,0.17l-1.31,1.99l1.04,2.24l-1.52,0.9l-1.74,1.57l-2.79,1.02l-3.78,0.13l-4.05,1.05l-2.77,1.52l-1.16,-0.85l-2.94,0.0l-3.62,-1.79l-2.58,-0.49l-3.4,0.41l-5.12,-0.67l-2.63,0.06l-1.31,-1.6l-1.4,-3.0l-1.48,-0.33l-3.13,-1.94l-6.16,-0.93l-0.71,-1.06l0.86,-3.82l-1.93,-2.71l-3.5,-1.18l-1.95,-1.58l-0.5,-1.72l2.34,-0.52l4.75,-2.8l3.62,-1.47l2.18,0.97l2.46,0.05l1.81,1.53l2.46,0.12l3.95,0.71l2.43,-2.28l0.08,-0.48l-0.9,-1.72l2.24,-2.98l2.62,1.27l4.94,1.17l0.43,2.24Z",name:"Mongolia"},MK:{path:"M472.8,173.98l0.49,-0.71l3.57,-0.71l1.0,0.77l0.13,1.45l-0.65,0.53l-1.15,-0.05l-1.12,0.67l-1.39,0.22l-0.79,-0.55l-0.29,-1.03l0.19,-0.6Z",name:"Macedonia"},MW:{path:"M505.5,309.31l0.85,1.95l0.15,2.86l-0.69,1.65l0.71,1.8l0.06,1.28l0.49,0.64l0.07,1.06l0.4,0.55l0.8,-0.23l0.55,0.61l0.69,-0.21l0.34,0.6l0.19,2.94l-1.04,0.62l-0.54,1.25l-1.11,-1.08l-0.16,-1.56l0.51,-1.31l-0.32,-1.3l-0.99,-0.65l-0.82,0.12l-2.36,-1.64l0.63,-1.96l0.82,-1.18l-0.46,-2.01l0.9,-2.86l-0.94,-2.51l0.96,0.18l0.29,0.4Z",name:"Malawi"},MR:{path:"M407.36,220.66l-2.58,0.03l-0.39,0.44l2.42,22.56l0.36,0.43l-0.39,1.24l-9.75,0.04l-0.56,0.53l-0.91,-0.11l-1.27,0.45l-1.61,-0.66l-0.97,0.03l-0.36,0.29l-0.38,1.35l-0.42,0.23l-2.93,-3.4l-2.96,-1.52l-1.62,-0.03l-1.27,0.54l-1.12,-0.2l-0.65,0.4l-0.08,-0.49l0.68,-1.29l0.31,-2.43l-0.57,-3.91l0.23,-1.21l-0.69,-1.5l-1.15,-1.02l0.25,-0.39l9.58,0.02l0.4,-0.45l-0.46,-3.68l0.47,-1.04l2.12,-0.21l0.36,-0.4l-0.08,-6.4l7.81,0.13l0.41,-0.4l0.01,-3.31l7.76,5.35Z",name:"Mauritania"},UG:{path:"M498.55,276.32l0.7,-0.46l1.65,0.5l1.96,-0.57l1.7,0.01l1.45,-0.98l0.91,1.33l1.33,3.95l-2.57,4.03l-1.46,-0.4l-2.54,0.91l-1.37,1.61l-0.01,0.81l-2.42,-0.01l-2.26,1.01l-0.17,-1.59l0.58,-1.04l0.14,-1.94l1.37,-2.28l1.78,-1.58l-0.17,-0.65l-0.72,-0.24l0.13,-2.43Z",name:"Uganda"},MY:{path:"M717.47,273.46l-1.39,0.65l-2.12,-0.41l-2.88,-0.0l-0.38,0.28l-0.84,2.75l-0.99,0.96l-1.21,3.29l-1.73,0.45l-2.45,-0.68l-1.39,0.31l-1.33,1.15l-1.59,-0.14l-1.41,0.44l-1.44,-1.19l-0.18,-0.73l1.34,0.53l1.93,-0.47l0.75,-2.22l4.02,-1.03l2.75,-3.21l0.82,0.94l0.64,-0.05l0.4,-0.65l0.96,0.06l0.42,-0.36l0.24,-2.68l1.81,-1.64l1.21,-1.86l0.63,-0.01l1.07,1.05l0.34,1.28l3.44,1.35l-0.06,0.35l-1.37,0.1l-0.35,0.54l0.32,0.88ZM673.68,269.59l0.17,1.09l0.47,0.33l1.65,-0.3l0.87,-0.94l1.61,1.52l0.98,1.56l-0.12,2.81l0.41,2.29l0.95,0.9l0.88,2.44l-1.27,0.12l-5.1,-3.67l-0.34,-1.29l-1.37,-1.59l-0.33,-1.97l-0.88,-1.4l0.25,-1.68l-0.46,-1.05l1.63,0.84Z",name:"Malaysia"},MX:{path:"M133.12,200.41l0.2,0.47l9.63,3.33l6.96,-0.02l0.4,-0.4l0.0,-0.74l3.77,0.0l3.55,2.93l1.39,2.83l1.52,1.04l2.08,0.82l0.47,-0.14l1.46,-2.0l1.73,-0.04l1.59,0.98l2.05,3.35l1.47,1.56l1.26,3.14l2.18,1.02l2.26,0.58l-1.18,3.72l-0.42,5.04l1.79,4.89l1.62,1.89l0.61,1.52l1.2,1.42l2.55,0.66l1.37,1.1l7.54,-1.89l1.86,-1.3l1.14,-4.3l4.1,-1.21l3.57,-0.11l0.32,0.3l-0.06,0.94l-1.26,1.45l-0.67,1.71l0.38,0.7l-0.72,2.27l-0.49,-0.3l-1.0,0.08l-1.0,1.39l-0.47,-0.11l-0.53,0.47l-4.26,-0.02l-0.4,0.4l-0.0,1.06l-1.1,0.26l0.1,0.44l1.82,1.44l0.56,0.91l-3.19,0.21l-1.21,2.09l0.24,0.72l-0.2,0.44l-2.24,-2.18l-1.45,-0.93l-2.22,-0.69l-1.52,0.22l-3.07,1.16l-10.55,-3.85l-2.86,-1.96l-3.78,-0.92l-1.08,-1.19l-2.62,-1.43l-1.18,-1.54l-0.38,-0.81l0.66,-0.63l-0.18,-0.53l0.52,-0.76l0.01,-0.91l-2.0,-3.82l-2.21,-2.63l-2.53,-2.09l-1.19,-1.62l-2.2,-1.17l-0.3,-0.43l0.34,-1.48l-0.21,-0.45l-1.23,-0.6l-1.36,-1.2l-0.59,-1.78l-1.54,-0.47l-2.44,-2.55l-0.16,-0.9l-1.33,-2.03l-0.84,-1.99l-0.16,-1.33l-1.81,-1.1l-0.97,0.05l-1.31,-0.7l-0.57,0.22l-0.4,1.12l0.72,3.77l3.51,3.89l0.28,0.78l0.53,0.26l0.41,1.43l1.33,1.73l1.58,1.41l0.8,2.39l1.43,2.41l0.13,1.32l0.37,0.36l1.04,0.08l1.67,2.28l-0.85,0.76l-0.66,-1.51l-1.68,-1.54l-2.91,-1.87l0.06,-1.82l-0.54,-1.68l-2.91,-2.03l-0.55,0.09l-1.95,-1.1l-0.88,-0.94l0.68,-0.08l0.93,-1.01l0.08,-1.78l-1.93,-1.94l-1.46,-0.77l-3.75,-7.56l4.88,-0.42Z",name:"Mexico"},VU:{path:"M839.04,322.8l0.22,1.14l-0.44,0.03l-0.2,-1.45l0.42,0.27Z",name:"Vanuatu"},FR:{path:"M444.48,172.62l-0.64,1.78l-0.58,-0.31l-0.49,-1.72l0.4,-0.89l1.0,-0.72l0.3,1.85ZM429.64,147.1l1.78,1.58l1.46,-0.13l2.1,1.42l1.35,0.27l1.23,0.83l3.04,0.5l-1.03,1.85l-0.3,2.12l-0.41,0.32l-0.95,-0.24l-0.5,0.43l0.06,0.61l-1.81,1.92l-0.04,1.42l0.55,0.38l0.88,-0.36l0.61,0.97l-0.03,1.0l0.57,0.91l-0.75,1.09l0.65,2.39l1.27,0.57l-0.18,0.82l-2.01,1.53l-4.77,-0.8l-3.82,1.0l-0.53,1.85l-2.49,0.34l-2.71,-1.31l-1.16,0.57l-4.31,-1.29l-0.72,-0.86l1.19,-1.78l0.39,-6.45l-2.58,-3.3l-1.9,-1.66l-3.72,-1.23l-0.19,-1.72l2.81,-0.61l4.12,0.81l0.47,-0.48l-0.6,-2.77l1.94,0.95l5.83,-2.54l0.92,-2.74l1.6,-0.49l0.24,0.78l1.36,0.33l1.05,1.19ZM289.01,278.39l-0.81,0.8l-0.78,0.12l-0.5,-0.66l-0.56,-0.1l-0.91,0.6l-0.46,-0.22l1.09,-2.96l-0.96,-1.77l-0.17,-1.49l1.07,-1.77l2.32,0.75l2.51,2.01l0.3,0.74l-2.14,3.96Z",name:"France"},FI:{path:"M492.17,76.39l-0.23,3.5l3.52,2.63l-2.08,2.88l-0.02,0.44l2.8,4.56l-1.59,3.31l2.16,3.24l-0.94,2.39l0.14,0.47l3.44,2.51l-0.77,1.62l-7.52,6.95l-4.5,0.31l-4.38,1.37l-3.8,0.74l-1.44,-1.96l-2.17,-1.11l0.5,-3.66l-1.16,-3.33l1.09,-2.08l2.21,-2.42l5.67,-4.32l1.64,-0.83l0.21,-0.42l-0.46,-2.02l-3.38,-1.89l-0.75,-1.43l-0.22,-6.74l-6.79,-4.8l0.8,-0.62l2.54,2.12l3.46,-0.12l3.0,0.96l2.51,-2.11l1.17,-3.08l3.55,-1.38l2.76,1.53l-0.95,2.79Z",name:"Finland"},FJ:{path:"M871.53,326.34l-2.8,1.05l-0.08,-0.23l2.97,-1.21l-0.1,0.39ZM867.58,329.25l0.43,0.37l-0.27,0.88l-1.24,0.28l-1.04,-0.24l-0.14,-0.66l0.63,-0.58l0.92,0.26l0.7,-0.31Z",name:"Fiji"},FK:{path:"M274.36,425.85l1.44,1.08l-0.47,0.73l-3.0,0.89l-0.96,-1.0l-0.52,-0.05l-1.83,1.29l-0.73,-0.88l2.46,-1.64l1.93,0.76l1.67,-1.19Z",name:"Falkland Is."},NI:{path:"M202.33,252.67l0.81,-0.18l1.03,-1.02l-0.04,-0.88l0.68,-0.0l0.63,-0.54l0.97,0.22l1.53,-1.26l0.58,-0.99l1.17,0.34l2.41,-0.94l0.13,1.32l-0.81,1.94l0.1,2.74l-0.36,0.37l-0.11,1.75l-0.47,0.81l0.18,1.14l-1.73,-0.85l-0.71,0.27l-1.47,-0.6l-0.52,0.16l-4.01,-3.81Z",name:"Nicaragua"},NL:{path:"M430.31,143.39l0.6,-0.5l2.13,-4.8l3.2,-1.33l1.74,0.08l0.33,0.8l-0.59,2.92l-0.5,0.99l-1.26,0.0l-0.4,0.45l0.33,2.7l-2.2,-1.78l-2.62,0.58l-0.75,-0.11Z",name:"Netherlands"},NO:{path:"M491.44,67.41l6.8,2.89l-2.29,0.86l-0.15,0.65l2.33,2.38l-4.98,1.79l0.84,-2.45l-0.18,-0.48l-3.55,-1.8l-3.89,1.52l-1.42,3.38l-2.12,1.72l-2.64,-1.0l-3.11,0.21l-2.66,-2.22l-0.5,-0.01l-1.41,1.1l-1.44,0.17l-0.35,0.35l-0.32,2.47l-4.32,-0.64l-0.44,0.29l-0.58,2.11l-2.45,0.2l-4.15,7.68l-3.88,5.76l0.78,1.62l-0.64,1.16l-2.24,-0.06l-0.38,0.24l-1.66,3.89l0.15,5.17l1.57,2.04l-0.78,4.16l-2.02,2.48l-0.85,1.63l-1.3,-1.75l-0.58,-0.07l-4.87,4.19l-3.1,0.79l-3.16,-1.7l-0.85,-3.77l-0.77,-8.55l2.14,-2.31l6.55,-3.27l5.02,-4.17l10.63,-13.84l10.98,-8.7l5.35,-1.91l4.34,0.12l3.69,-3.64l4.49,0.19l4.37,-0.89ZM484.55,20.04l4.26,1.75l-3.1,2.55l-7.1,0.65l-7.08,-0.9l-0.37,-1.31l-0.37,-0.29l-3.44,-0.1l-2.08,-2.0l6.87,-1.44l3.9,1.31l2.39,-1.64l6.13,1.4ZM481.69,33.93l-4.45,1.74l-3.54,-0.99l1.12,-0.9l0.05,-0.58l-1.06,-1.22l4.22,-0.89l1.09,1.97l2.57,0.87ZM466.44,24.04l7.43,3.77l-5.41,1.86l-1.58,4.08l-2.26,1.2l-1.12,4.11l-2.61,0.18l-4.79,-2.86l1.84,-1.54l-0.1,-0.68l-3.69,-1.53l-4.77,-4.51l-1.73,-3.89l6.11,-1.82l1.54,1.92l3.57,-0.08l1.2,-1.96l3.32,-0.18l3.05,1.92Z",name:"Norway"},NA:{path:"M474.26,330.66l-0.97,0.04l-0.38,0.4l-0.07,8.9l-2.09,0.08l-0.39,0.4l-0.0,17.42l-1.98,1.23l-1.17,0.17l-2.44,-0.66l-0.48,-1.13l-0.99,-0.74l-0.54,0.05l-0.9,1.01l-1.53,-1.68l-0.93,-1.88l-1.99,-8.56l-0.06,-3.12l-0.33,-1.52l-2.3,-3.34l-1.91,-4.83l-1.96,-2.43l-0.12,-1.57l2.33,-0.79l1.43,0.07l1.81,1.13l10.23,-0.25l1.84,1.23l5.87,0.35ZM474.66,330.64l6.51,-1.6l1.9,0.39l-1.69,0.4l-1.31,0.83l-1.12,-0.94l-4.29,0.92Z",name:"Namibia"},NC:{path:"M838.78,341.24l-0.33,0.22l-2.9,-1.75l-3.26,-3.37l1.65,0.83l4.85,4.07Z",name:"New Caledonia"},NE:{path:"M454.75,226.53l1.33,1.37l0.48,0.07l1.27,-0.7l0.53,3.52l0.94,0.83l0.17,0.92l0.81,0.69l-0.44,0.95l-0.96,5.26l-0.13,3.22l-3.04,2.31l-1.22,3.57l1.02,1.24l-0.0,1.46l0.39,0.4l1.13,0.04l-0.9,1.25l-1.47,-2.42l-0.86,-0.29l-2.09,1.37l-1.74,-0.67l-1.45,-0.17l-0.85,0.35l-1.36,-0.07l-1.64,1.09l-1.06,0.05l-2.94,-1.28l-1.44,0.59l-1.01,-0.03l-0.97,-0.94l-2.7,-0.98l-2.69,0.3l-0.87,0.64l-0.47,1.6l-0.75,1.16l-0.12,1.53l-1.57,-1.1l-1.31,0.24l0.03,-0.81l-0.32,-0.41l-2.59,-0.52l-0.15,-1.16l-1.35,-1.6l-0.29,-1.0l0.13,-0.84l1.29,-0.08l1.08,-0.92l3.31,-0.22l2.22,-0.41l0.32,-0.34l0.2,-1.47l1.39,-1.88l-0.01,-5.66l3.36,-1.12l7.24,-5.12l8.42,-4.92l3.69,1.06Z",name:"Niger"},NG:{path:"M456.32,253.89l0.64,0.65l-0.28,1.04l-2.11,2.01l-2.03,5.18l-1.37,1.16l-1.15,3.18l-1.33,0.66l-1.46,-0.97l-1.21,0.16l-1.38,1.36l-0.91,0.24l-1.79,4.06l-2.33,0.81l-1.11,-0.07l-0.86,0.5l-1.71,-0.05l-1.19,-1.39l-0.89,-1.89l-1.77,-1.66l-3.95,-0.08l0.07,-5.21l0.42,-1.43l1.95,-2.3l-0.14,-0.91l0.43,-1.18l-0.53,-1.41l0.25,-2.92l0.72,-1.07l0.32,-1.34l0.46,-0.39l2.47,-0.28l2.34,0.89l1.15,1.02l1.28,0.04l1.22,-0.58l3.03,1.27l1.49,-0.14l1.36,-1.0l1.33,0.07l0.82,-0.35l3.45,0.8l1.82,-1.32l1.84,2.67l0.66,0.16Z",name:"Nigeria"},NZ:{path:"M857.8,379.65l1.86,3.12l0.44,0.18l0.3,-0.38l0.03,-1.23l0.38,0.27l0.57,2.31l2.02,0.94l1.81,0.27l1.57,-1.06l0.7,0.18l-1.15,3.59l-1.98,0.11l-0.74,1.2l0.2,1.11l-2.42,3.98l-1.49,0.92l-1.04,-0.85l1.21,-2.05l-0.81,-2.01l-2.63,-1.25l0.04,-0.57l1.82,-1.19l0.43,-2.34l-0.16,-2.03l-0.95,-1.82l-0.06,-0.72l-3.11,-3.64l-0.79,-1.52l1.56,1.45l1.76,0.66l0.65,2.34ZM853.83,393.59l0.57,1.24l0.59,0.16l1.42,-0.97l0.46,0.79l0.0,1.03l-2.47,3.48l-1.26,1.2l-0.06,0.5l0.55,0.87l-1.41,0.07l-2.33,1.38l-2.03,5.02l-3.02,2.16l-2.06,-0.06l-1.71,-1.04l-2.47,-0.2l-0.27,-0.73l1.22,-2.1l3.05,-2.94l1.62,-0.59l4.02,-2.82l1.57,-1.67l1.07,-2.16l0.88,-0.7l0.48,-1.75l1.24,-0.97l0.35,0.79Z",name:"New Zealand"},NP:{path:"M641.14,213.62l0.01,3.19l-1.74,0.04l-4.8,-0.86l-1.58,-1.39l-3.37,-0.34l-7.65,-3.7l0.8,-2.09l2.33,-1.7l1.77,0.75l2.49,1.76l1.38,0.41l0.99,1.35l1.9,0.52l1.99,1.17l5.49,0.9Z",name:"Nepal"},CI:{path:"M407.4,259.27l0.86,0.42l0.56,0.9l1.13,0.53l1.19,-0.61l0.97,-0.08l1.42,0.54l0.6,3.24l-1.03,2.08l-0.65,2.84l1.06,2.33l-0.06,0.53l-2.54,-0.47l-1.66,0.03l-3.06,0.46l-4.11,1.6l0.32,-3.06l-1.18,-1.31l-1.32,-0.66l0.42,-0.85l-0.2,-1.4l0.5,-0.67l0.01,-1.59l0.84,-0.32l0.26,-0.5l-1.15,-3.01l0.12,-0.5l0.51,-0.25l0.66,0.31l1.93,0.02l0.67,-0.71l0.71,-0.14l0.25,0.69l0.57,0.22l1.4,-0.61Z",name:"Côte d'Ivoire"},CH:{path:"M444.62,156.35l-0.29,0.87l0.18,0.53l1.13,0.58l1.0,0.1l-0.1,0.65l-0.79,0.38l-1.72,-0.37l-0.45,0.23l-0.45,1.04l-0.75,0.06l-0.84,-0.4l-1.32,1.0l-0.96,0.12l-0.88,-0.55l-0.81,-1.3l-0.49,-0.16l-0.63,0.26l0.02,-0.65l1.71,-1.66l0.1,-0.56l0.93,0.08l0.58,-0.46l1.99,0.02l0.66,-0.61l2.19,0.79Z",name:"Switzerland"},CO:{path:"M242.07,254.93l-1.7,0.59l-0.59,1.18l-1.7,1.69l-0.38,1.93l-0.67,1.43l0.31,0.57l1.03,0.13l0.25,0.9l0.57,0.64l-0.04,2.34l1.64,1.42l3.16,-0.24l1.26,0.28l1.67,2.06l0.41,0.13l4.09,-0.39l0.45,0.22l-0.92,1.95l-0.2,1.8l0.52,1.83l0.75,1.05l-1.12,1.1l0.07,0.63l0.84,0.51l0.74,1.29l-0.39,-0.45l-0.59,-0.01l-0.71,0.74l-4.71,-0.05l-0.4,0.41l0.03,1.57l0.33,0.39l1.11,0.2l-1.68,0.4l-0.29,0.38l-0.01,1.82l1.16,1.14l0.34,1.25l-1.05,7.05l-1.04,-0.87l1.26,-1.99l-0.13,-0.56l-2.18,-1.23l-1.38,0.2l-1.14,-0.38l-1.27,0.61l-1.55,-0.26l-1.38,-2.46l-1.23,-0.75l-0.85,-1.2l-1.67,-1.19l-0.86,0.13l-2.11,-1.32l-1.01,0.31l-1.8,-0.29l-0.52,-0.91l-3.09,-1.68l0.77,-0.52l-0.1,-1.12l0.41,-0.64l1.34,-0.32l2.0,-2.88l-0.11,-0.57l-0.66,-0.43l0.39,-1.38l-0.52,-2.1l0.49,-0.83l-0.4,-2.13l-0.97,-1.35l0.17,-0.66l0.86,-0.08l0.47,-0.75l-0.46,-1.63l1.41,-0.07l1.8,-1.69l0.93,-0.24l0.3,-0.38l0.45,-2.76l1.22,-1.0l1.44,-0.04l0.45,-0.5l1.91,0.12l2.93,-1.84l1.15,-1.14l0.91,0.46l-0.25,0.45Z",name:"Colombia"},CN:{path:"M740.23,148.97l4.57,1.3l2.8,2.17l0.98,2.9l0.38,0.27l3.8,0.0l2.32,-1.28l3.29,-0.75l-0.96,2.09l-1.02,1.28l-0.85,3.4l-1.52,2.73l-2.76,-0.5l-2.4,1.13l-0.21,0.45l0.64,2.57l-0.32,3.2l-0.94,0.06l-0.37,0.89l-0.91,-1.01l-0.64,0.07l-0.92,1.57l-3.73,1.25l-0.26,0.48l0.26,1.06l-1.5,-0.08l-1.09,-0.86l-0.56,0.06l-1.67,2.06l-2.7,1.56l-2.03,1.88l-3.4,0.83l-1.93,1.4l-1.15,0.34l0.33,-0.7l-0.41,-0.89l1.79,-1.79l0.02,-0.54l-1.32,-1.56l-0.48,-0.1l-2.24,1.09l-2.83,2.06l-1.51,1.83l-2.28,0.13l-1.55,1.49l-0.04,0.5l1.32,1.97l2.0,0.58l0.31,1.35l1.98,0.84l3.0,-1.96l2.0,1.02l1.49,0.11l0.22,0.83l-3.37,0.86l-1.12,1.48l-2.5,1.52l-1.29,1.99l0.14,0.56l2.57,1.48l0.97,2.7l3.17,4.63l-0.03,1.66l-1.35,0.65l-0.2,0.51l0.6,1.47l1.4,0.91l-0.89,3.82l-1.43,0.38l-3.85,6.44l-2.27,3.11l-6.78,4.57l-2.73,0.29l-1.45,1.04l-0.62,-0.61l-0.55,-0.01l-1.36,1.25l-3.39,1.27l-2.61,0.4l-1.1,2.79l-0.81,0.09l-0.49,-1.42l0.5,-0.85l-0.25,-0.59l-3.36,-0.84l-1.3,0.4l-2.31,-0.62l-0.94,-0.84l0.33,-1.28l-0.3,-0.49l-2.19,-0.46l-1.13,-0.93l-0.47,-0.02l-2.06,1.36l-4.29,0.28l-2.76,1.05l-0.28,0.43l0.32,2.53l-0.59,-0.03l-0.19,-1.34l-0.55,-0.34l-1.68,0.7l-2.46,-1.23l0.62,-1.87l-0.26,-0.51l-1.37,-0.44l-0.54,-2.22l-0.45,-0.3l-2.13,0.35l0.24,-2.48l2.39,-2.4l0.03,-4.31l-1.19,-0.92l-0.78,-1.49l-0.41,-0.21l-1.41,0.19l-1.98,-0.3l0.46,-1.07l-1.17,-1.7l-0.55,-0.11l-1.63,1.05l-2.25,-0.57l-2.89,1.73l-2.25,1.98l-1.75,0.29l-1.17,-0.71l-3.31,-0.65l-1.48,0.79l-1.04,1.27l-0.12,-1.17l-0.54,-0.34l-1.44,0.54l-5.55,-0.86l-1.98,-1.16l-1.89,-0.54l-0.99,-1.35l-1.34,-0.37l-2.55,-1.79l-2.01,-0.84l-1.21,0.56l-5.57,-3.45l-0.53,-2.31l1.19,0.25l0.48,-0.37l0.08,-1.42l-0.98,-1.56l0.15,-2.44l-2.69,-3.32l-4.12,-1.23l-0.67,-2.0l-1.92,-1.48l-0.38,-0.7l-0.51,-3.01l-1.52,-0.66l-0.7,0.13l-0.48,-2.05l0.55,-0.51l-0.09,-0.82l2.03,-1.19l1.6,-0.54l2.56,0.38l0.42,-0.22l0.85,-1.7l3.0,-0.33l1.1,-1.26l4.05,-1.77l0.39,-0.91l-0.17,-1.44l1.45,-0.67l0.2,-0.52l-2.07,-4.9l4.51,-1.12l1.37,-0.73l1.89,-5.51l4.98,0.86l1.51,-1.7l0.11,-2.87l1.99,-0.38l1.83,-2.06l0.49,-0.13l0.68,2.08l2.23,1.77l3.44,1.16l1.55,2.29l-0.92,3.49l0.96,1.67l6.54,1.13l2.95,1.87l1.47,0.35l1.06,2.62l1.53,1.91l3.05,0.08l5.14,0.67l3.37,-0.41l2.36,0.43l3.65,1.8l3.06,0.04l1.45,0.88l2.87,-1.59l3.95,-1.02l3.83,-0.14l3.06,-1.14l1.77,-1.6l1.72,-1.01l0.17,-0.49l-1.1,-2.05l1.02,-1.54l4.02,0.8l2.45,-1.61l3.76,-1.19l1.96,-2.13l1.63,-0.83l3.51,-0.4l1.92,0.34l0.46,-0.3l0.17,-1.5l-2.27,-2.22l-2.11,-1.09l-2.18,1.11l-2.32,-0.47l-1.29,0.32l-0.4,-0.82l2.73,-5.16l3.02,1.06l3.53,-2.06l0.18,-1.68l2.16,-3.35l1.49,-1.35l-0.03,-1.85l-1.07,-0.85l1.54,-1.26l2.98,-0.59l3.23,-0.09l3.64,0.99l2.04,1.16l3.29,6.71l0.92,3.19ZM696.92,237.31l-1.87,1.08l-1.63,-0.64l-0.06,-1.79l1.03,-0.98l2.58,-0.69l1.16,0.05l0.3,0.54l-0.98,1.06l-0.53,1.37Z",name:"China"},CM:{path:"M457.92,257.49l1.05,1.91l-1.4,0.16l-1.05,-0.23l-0.45,0.22l-0.54,1.19l0.08,0.45l1.48,1.47l1.05,0.45l1.01,2.46l-1.52,2.99l-0.68,0.68l-0.13,3.69l2.38,3.84l1.09,0.8l0.24,2.48l-3.67,-1.14l-11.27,-0.13l0.23,-1.79l-0.98,-1.66l-1.19,-0.54l-0.44,-0.97l-0.6,-0.42l1.71,-4.27l0.75,-0.13l1.38,-1.36l0.65,-0.03l1.71,0.99l1.93,-1.12l1.14,-3.18l1.38,-1.17l2.0,-5.14l2.17,-2.13l0.3,-1.64l-0.86,-0.88l0.03,-0.33l0.94,1.28l0.07,3.22Z",name:"Cameroon"},CL:{path:"M246.5,429.18l-3.14,1.83l-0.57,3.16l-0.64,0.05l-2.68,-1.06l-2.82,-2.33l-3.04,-1.89l-0.69,-1.85l0.63,-2.14l-1.21,-2.11l-0.31,-5.37l1.01,-2.91l2.57,-2.38l-0.18,-0.68l-3.16,-0.77l2.05,-2.47l0.77,-4.65l2.32,0.9l0.54,-0.29l1.31,-6.31l-0.22,-0.44l-1.68,-0.8l-0.56,0.28l-0.7,3.36l-0.81,-0.22l1.56,-9.41l1.15,-2.24l-0.71,-2.82l-0.18,-2.84l1.01,-0.33l3.26,-9.14l1.07,-4.22l-0.56,-4.21l0.74,-2.34l-0.29,-3.27l1.46,-3.34l2.04,-16.59l-0.66,-7.76l1.03,-0.53l0.54,-0.9l0.79,1.14l0.32,1.78l1.25,1.16l-0.69,2.55l1.33,2.9l0.97,3.59l0.46,0.29l1.5,-0.3l0.11,0.23l-0.76,2.44l-2.57,1.23l-0.23,0.37l0.08,4.33l-0.46,0.77l0.56,1.21l-1.58,1.51l-1.68,2.62l-0.89,2.47l0.2,2.7l-1.48,2.73l1.12,5.09l0.64,0.61l-0.01,2.29l-1.38,2.68l0.01,2.4l-1.89,2.04l0.02,2.75l0.69,2.57l-1.43,1.13l-1.26,5.68l0.39,3.51l-0.97,0.89l0.58,3.5l1.02,1.14l-0.65,1.02l0.15,0.57l1.0,0.53l0.16,0.69l-1.03,0.85l0.26,1.75l-0.89,4.03l-1.31,2.66l0.24,1.75l-0.71,1.83l-1.99,1.7l0.3,3.67l0.88,1.19l1.58,0.01l0.01,2.21l1.04,1.95l5.98,0.63ZM248.69,430.79l0.0,7.33l0.4,0.4l3.52,0.05l-0.44,0.75l-1.94,0.98l-2.49,-0.37l-1.88,-1.06l-2.55,-0.49l-5.59,-3.71l-2.38,-2.63l4.1,2.48l3.32,1.23l0.45,-0.12l1.29,-1.57l0.83,-2.32l2.05,-1.24l1.31,0.29Z",name:"Chile"},CA:{path:"M280.06,145.6l-1.67,2.88l0.07,0.49l0.5,0.04l1.46,-0.98l1.0,0.42l-0.56,0.72l0.17,0.62l2.22,0.89l1.35,-0.71l1.95,0.78l-0.66,2.01l0.5,0.51l1.32,-0.42l0.98,3.17l-0.91,2.41l-0.8,0.08l-1.23,-0.45l0.47,-2.25l-0.89,-0.83l-0.48,0.06l-2.78,2.63l-0.34,-0.02l1.02,-0.85l-0.14,-0.69l-2.4,-0.77l-7.4,0.08l-0.17,-0.41l1.3,-0.94l0.02,-0.64l-0.73,-0.58l1.85,-1.74l2.57,-5.16l1.47,-1.79l1.99,-1.05l0.46,0.06l-1.53,2.45ZM68.32,74.16l4.13,0.95l4.02,2.14l2.61,0.4l2.47,-1.89l2.88,-1.31l3.85,0.48l3.71,-1.94l3.82,-1.04l1.56,1.68l0.49,0.08l1.87,-1.04l0.65,-1.98l1.24,0.35l4.16,3.94l0.54,0.01l2.75,-2.49l0.26,2.59l0.49,0.35l3.08,-0.73l1.04,-1.27l2.73,0.23l3.83,1.86l5.86,1.61l3.47,0.75l2.44,-0.26l2.73,1.78l-2.98,1.81l-0.19,0.41l0.31,0.32l4.53,0.92l6.87,-0.5l2.0,-0.69l2.49,2.39l0.53,0.02l2.72,-2.16l-0.02,-0.64l-2.16,-1.54l1.15,-1.06l4.83,-0.61l1.84,0.95l2.48,2.31l3.01,-0.23l4.55,1.92l3.85,-0.67l3.61,0.1l0.41,-0.44l-0.25,-2.36l1.79,-0.61l3.49,1.32l-0.01,3.77l0.31,0.39l0.45,-0.22l1.48,-3.16l1.74,0.1l0.41,-0.3l1.13,-4.37l-2.78,-3.11l-2.8,-1.74l0.19,-4.64l2.71,-3.07l2.98,0.67l2.41,1.95l3.19,4.8l-1.99,1.97l0.21,0.68l4.33,0.84l-0.01,4.15l0.25,0.37l0.44,-0.09l3.07,-3.15l2.54,2.39l-0.61,3.33l2.42,2.88l0.61,0.0l2.61,-3.08l1.88,-3.82l0.17,-4.58l6.72,0.94l3.13,2.04l0.13,1.82l-1.76,2.19l-0.01,0.49l1.66,2.16l-0.26,1.71l-4.68,2.8l-3.28,0.61l-2.47,-1.2l-0.55,0.23l-0.73,2.04l-2.38,3.43l-0.74,1.77l-2.74,2.57l-3.44,0.25l-2.21,1.78l-0.28,2.53l-2.82,0.55l-3.12,3.22l-2.72,4.31l-1.03,3.17l-0.14,4.31l0.33,0.41l3.44,0.57l2.24,5.95l0.45,0.23l3.4,-0.69l4.52,1.51l2.43,1.31l1.91,1.73l3.1,0.96l2.62,1.46l6.6,0.54l-0.35,2.74l0.81,3.53l1.81,3.78l3.83,3.3l0.45,0.04l2.1,-1.28l1.37,-3.69l-1.31,-5.38l-1.45,-1.58l3.57,-1.47l2.84,-2.46l1.52,-2.8l-0.25,-2.55l-1.7,-3.07l-2.85,-2.61l2.8,-3.95l-1.08,-3.37l-0.79,-5.67l1.36,-0.7l6.76,1.41l2.12,-0.96l5.12,3.36l1.05,1.61l4.08,0.26l-0.06,2.87l0.83,4.7l0.3,0.32l2.16,0.54l1.73,2.06l0.5,0.09l3.63,-2.03l2.52,-4.19l1.26,-1.32l7.6,11.72l-0.92,2.04l0.16,0.51l3.3,1.97l2.22,1.98l4.1,0.98l1.43,0.99l0.95,2.79l2.1,0.68l0.84,1.08l0.17,3.45l-3.37,2.26l-4.22,1.24l-3.06,2.63l-4.06,0.51l-5.35,-0.69l-6.39,0.2l-2.3,2.41l-3.26,1.51l-6.47,7.15l-0.06,0.48l0.44,0.19l2.13,-0.52l4.17,-4.24l5.12,-2.62l3.52,-0.3l1.69,1.21l-2.12,2.21l0.81,3.47l1.02,2.61l3.47,1.6l4.14,-0.45l2.15,-2.8l0.26,1.48l1.14,0.8l-2.56,1.69l-5.5,1.82l-2.54,1.27l-2.74,2.15l-1.4,-0.16l-0.07,-2.01l4.14,-2.44l0.18,-0.45l-0.39,-0.29l-6.63,0.45l-1.39,-1.49l-0.14,-4.43l-1.11,-0.91l-1.82,0.39l-0.66,-0.66l-0.6,0.03l-1.91,2.39l-0.82,2.52l-0.8,1.27l-1.67,0.56l-0.46,0.76l-8.31,0.07l-1.21,0.62l-2.35,1.97l-0.71,-0.14l-1.37,0.96l-1.12,-0.48l-4.74,1.26l-0.9,1.17l0.21,0.62l1.73,0.3l-1.81,0.31l-1.85,0.81l-2.11,-0.13l-2.95,1.78l-0.69,-0.09l1.39,-2.1l1.73,-1.21l0.1,-2.29l1.16,-1.99l0.49,0.53l2.03,0.42l1.2,-1.16l0.02,-0.47l-2.66,-3.51l-2.28,-0.61l-5.64,-0.71l-0.4,-0.57l-0.79,0.13l0.2,-0.41l-0.22,-0.55l-0.68,-0.26l0.19,-1.26l-0.78,-0.73l0.31,-0.64l-0.29,-0.57l-2.6,-0.44l-0.75,-1.63l-0.94,-0.66l-4.31,-0.65l-1.13,1.19l-1.48,0.59l-0.85,1.06l-2.83,-0.76l-2.09,0.39l-2.39,-0.97l-4.24,-0.7l-0.57,-0.4l-0.41,-1.63l-0.4,-0.3l-0.85,0.02l-0.39,0.4l-0.01,0.85l-69.13,-0.01l-6.51,-4.52l-4.5,-1.38l-1.26,-2.66l0.33,-1.93l-0.23,-0.43l-3.01,-1.35l-0.55,-2.77l-2.89,-2.38l-0.04,-1.45l1.39,-1.83l-0.28,-2.55l-4.16,-2.2l-4.07,-6.6l-4.02,-3.22l-1.3,-1.88l-0.5,-0.13l-2.51,1.21l-2.23,1.87l-3.85,-3.88l-2.44,-1.04l-2.22,-0.13l0.03,-37.49ZM260.37,148.65l3.04,0.76l2.26,1.2l-3.78,-0.95l-1.53,-1.01ZM249.4,3.81l6.68,0.49l5.32,0.79l4.26,1.57l-0.07,1.1l-5.85,2.53l-6.02,1.21l-2.39,1.39l-0.18,0.45l0.39,0.29l4.01,-0.02l-4.65,2.82l-4.2,1.74l-4.19,4.59l-5.03,0.92l-1.67,1.15l-7.47,0.59l-0.37,0.37l0.32,0.42l2.41,0.49l-0.81,0.47l-0.12,0.59l1.83,2.41l-2.02,1.59l-3.81,1.51l-1.32,2.16l-3.38,1.53l-0.22,0.48l0.35,1.19l0.4,0.29l3.88,-0.18l0.03,0.61l-6.33,2.95l-6.41,-1.4l-7.43,0.79l-3.72,-0.62l-4.4,-0.25l-0.23,-1.83l4.29,-1.11l0.28,-0.51l-1.1,-3.45l1.0,-0.25l6.58,2.28l0.47,-0.16l-0.05,-0.49l-3.41,-3.45l-3.58,-0.98l1.48,-1.55l4.34,-1.29l0.97,-2.19l-0.16,-0.48l-3.42,-2.13l-0.81,-2.26l6.2,0.22l2.24,0.58l3.91,-2.1l0.2,-0.43l-0.35,-0.32l-5.64,-0.67l-8.73,0.36l-4.26,-1.9l-2.12,-2.4l-2.78,-1.66l-0.41,-1.52l3.31,-1.03l2.93,-0.2l4.91,-0.99l3.7,-2.27l2.87,0.3l2.62,1.67l0.56,-0.14l1.82,-3.2l3.13,-0.94l4.44,-0.69l7.53,-0.26l1.48,0.67l7.19,-1.06l10.8,0.79ZM203.85,57.54l0.01,0.42l1.97,2.97l0.68,-0.02l2.24,-3.72l5.95,-1.86l4.01,4.64l-0.35,2.91l0.5,0.43l4.95,-1.36l2.32,-1.8l5.31,2.28l3.27,2.11l0.3,1.84l0.48,0.33l4.42,-0.99l2.64,2.87l5.97,1.77l2.06,1.72l2.11,3.71l-4.19,1.86l-0.01,0.73l5.9,2.83l3.94,0.94l3.78,3.95l3.46,0.25l-0.63,2.37l-4.11,4.47l-2.76,-1.56l-3.9,-3.94l-3.59,0.41l-0.33,0.34l-0.19,2.72l2.63,2.38l3.42,1.89l0.94,0.97l1.55,3.75l-0.7,2.29l-2.74,-0.92l-6.25,-3.15l-0.51,0.13l0.05,0.52l6.07,5.69l0.18,0.59l-6.09,-1.39l-5.31,-2.24l-2.63,-1.66l0.6,-0.77l-0.12,-0.6l-7.39,-4.01l-0.59,0.37l0.03,0.79l-6.73,0.6l-1.69,-1.1l1.36,-2.46l4.51,-0.07l5.15,-0.52l0.31,-0.6l-0.74,-1.3l0.78,-1.84l3.21,-4.05l-0.67,-2.35l-1.11,-1.6l-3.84,-2.1l-4.35,-1.28l0.91,-0.63l0.06,-0.61l-2.65,-2.75l-2.34,-0.36l-1.89,-1.46l-0.53,0.03l-1.24,1.23l-4.36,0.55l-9.04,-0.99l-9.26,-1.98l-1.6,-1.22l2.22,-1.77l0.13,-0.44l-0.38,-0.27l-3.22,-0.02l-0.72,-4.25l1.83,-4.04l2.42,-1.85l5.5,-1.1l-1.39,2.35ZM261.19,159.33l2.07,0.61l1.44,-0.04l-1.15,0.63l-2.94,-1.23l-0.4,-0.68l0.36,-0.37l0.61,1.07ZM230.83,84.39l-2.37,0.18l-0.49,-1.63l0.93,-2.09l1.94,-0.51l1.62,0.99l0.02,1.52l-1.66,1.54ZM229.43,58.25l0.11,0.65l-4.87,-0.21l-2.72,0.62l-3.1,-2.57l0.08,-1.26l0.86,-0.23l5.57,0.51l4.08,2.5ZM222.0,105.02l-0.72,1.49l-0.63,-0.19l-0.48,-0.84l0.81,-0.99l0.65,0.05l0.37,0.46ZM183.74,38.32l2.9,1.7l4.79,-0.01l1.84,1.46l-0.49,1.68l0.23,0.48l2.82,1.14l1.76,1.26l7.01,0.65l4.1,-1.1l5.03,-0.43l3.93,0.35l2.48,1.77l0.46,1.7l-1.3,1.1l-3.56,1.01l-3.23,-0.59l-7.17,0.76l-5.09,0.09l-3.99,-0.6l-6.42,-1.54l-0.79,-2.51l-0.3,-2.49l-2.64,-2.5l-5.32,-0.72l-2.52,-1.4l0.68,-1.57l4.78,0.31ZM207.38,91.35l0.4,1.56l0.56,0.26l1.06,-0.52l1.32,0.96l5.42,2.57l0.2,1.68l0.46,0.35l1.68,-0.28l1.15,0.85l-1.55,0.87l-3.61,-0.88l-1.32,-1.69l-0.57,-0.06l-2.45,2.1l-3.12,1.79l-0.7,-1.87l-0.42,-0.26l-2.16,0.24l1.39,-1.39l0.32,-3.14l0.76,-3.35l1.18,0.22ZM215.49,102.6l-2.67,1.95l-1.4,-0.07l-0.3,-0.58l1.53,-1.48l2.84,0.18ZM202.7,24.12l2.53,1.59l-2.87,1.4l-4.53,4.05l-4.25,0.38l-5.03,-0.68l-2.45,-2.04l0.03,-1.62l1.82,-1.37l0.14,-0.45l-0.38,-0.27l-4.45,0.04l-2.59,-1.76l-1.41,-2.29l1.57,-2.32l1.62,-1.66l2.44,-0.39l0.25,-0.65l-0.6,-0.74l4.86,-0.25l3.24,3.11l8.16,2.3l1.9,3.61ZM187.47,59.2l-2.76,3.49l-2.38,-0.15l-1.44,-3.84l0.04,-2.2l1.19,-1.88l2.3,-1.23l5.07,0.17l4.11,1.02l-3.24,3.72l-2.88,0.89ZM186.07,48.79l-1.08,1.53l-3.34,-0.34l-2.56,-1.1l1.03,-1.75l3.25,-1.23l1.95,1.58l0.75,1.3ZM185.71,35.32l-5.3,-0.2l-0.32,-0.71l4.31,0.07l1.3,0.84ZM180.68,32.48l-3.34,1.0l-1.79,-1.1l-0.98,-1.87l-0.15,-1.73l4.1,0.53l2.67,1.7l-0.51,1.47ZM180.9,76.31l-1.1,1.08l-3.13,-1.23l-2.12,0.43l-2.71,-1.57l1.72,-1.09l1.55,-1.72l3.81,1.9l1.98,2.2ZM169.74,54.87l2.96,0.97l4.17,-0.57l0.41,0.88l-2.14,2.11l0.09,0.64l3.55,1.92l-0.4,3.72l-3.79,1.65l-2.17,-0.35l-1.72,-1.74l-6.02,-3.5l0.03,-0.85l4.68,0.54l0.4,-0.21l-0.05,-0.45l-2.48,-2.81l2.46,-1.95ZM174.45,40.74l1.37,1.73l0.07,2.44l-1.05,3.45l-3.79,0.47l-2.32,-0.69l0.05,-2.64l-0.44,-0.41l-3.68,0.35l-0.12,-3.1l2.45,0.1l3.67,-1.73l3.41,0.29l0.37,-0.26ZM170.05,31.55l0.67,1.56l-3.33,-0.49l-4.22,-1.77l-4.35,-0.16l1.4,-0.94l-0.06,-0.7l-2.81,-1.23l-0.12,-1.39l4.39,0.68l6.62,1.98l1.81,2.47ZM134.5,58.13l-1.02,1.82l0.45,0.58l5.4,-1.39l3.33,2.29l0.49,-0.03l2.6,-2.23l1.94,1.32l2.0,4.5l0.7,0.06l1.3,-2.29l-1.63,-4.46l1.69,-0.54l2.31,0.71l2.65,1.81l2.49,7.92l8.48,4.27l-0.19,1.35l-3.79,0.33l-0.26,0.67l1.4,1.49l-0.58,1.1l-4.23,-0.64l-4.43,-1.19l-3.0,0.28l-4.66,1.47l-10.52,1.04l-1.43,-2.02l-3.42,-1.2l-2.21,0.43l-2.51,-2.86l4.84,-1.05l3.6,0.19l3.27,-0.78l0.31,-0.39l-0.31,-0.39l-4.84,-1.06l-8.79,0.27l-0.85,-1.07l5.26,-1.66l0.27,-0.45l-0.4,-0.34l-3.8,0.06l-3.81,-1.06l1.81,-3.01l1.66,-1.79l6.48,-2.81l1.97,0.71ZM158.7,56.61l-1.7,2.44l-3.2,-2.75l0.37,-0.3l3.11,-0.18l1.42,0.79ZM149.61,42.73l1.01,1.89l0.5,0.18l2.14,-0.82l2.23,0.19l0.36,2.04l-1.33,2.09l-8.28,0.76l-6.35,2.15l-3.41,0.1l-0.19,-0.96l4.9,-2.08l0.23,-0.46l-0.41,-0.31l-11.25,0.59l-2.89,-0.74l3.04,-4.44l2.14,-1.32l6.81,1.69l4.58,3.06l4.37,0.39l0.36,-0.63l-3.36,-4.6l1.85,-1.53l2.18,0.51l0.77,2.26ZM144.76,34.41l-4.36,1.44l-3.0,-1.4l1.46,-1.24l3.47,-0.52l2.96,0.71l-0.52,1.01ZM145.13,29.83l-1.9,0.66l-3.67,-0.0l2.27,-1.61l3.3,0.95ZM118.92,65.79l-6.03,2.02l-1.33,-1.9l-5.38,-2.28l2.59,-5.05l2.16,-3.14l-0.02,-0.48l-1.97,-2.41l7.64,-0.7l3.6,1.02l6.3,0.27l4.42,2.95l-2.53,0.98l-6.24,3.43l-3.1,3.28l-0.11,2.01ZM129.54,35.53l-0.28,3.37l-1.72,1.62l-2.33,0.28l-4.61,2.19l-3.86,0.76l-2.64,-0.87l3.72,-3.4l5.01,-3.34l3.72,0.07l3.0,-0.67ZM111.09,152.69l-0.67,0.24l-3.85,-1.37l-0.83,-1.17l-2.12,-1.07l-0.66,-1.02l-2.4,-0.55l-0.74,-1.71l6.02,1.45l2.0,2.55l2.52,1.39l0.73,1.27ZM87.8,134.64l0.89,0.29l1.86,-0.21l-0.65,3.34l1.69,2.33l-1.31,-1.33l-0.99,-1.62l-1.17,-0.98l-0.33,-1.82Z",name:"Canada"},CG:{path:"M466.72,276.48l-0.1,1.03l-1.25,2.97l-0.19,3.62l-0.46,1.78l-0.23,0.63l-1.61,1.19l-1.21,1.39l-1.09,2.43l0.04,2.09l-3.25,3.24l-0.5,-0.24l-0.5,-0.83l-1.36,-0.02l-0.98,0.89l-1.68,-0.99l-1.54,1.24l-1.52,-1.96l1.57,-1.14l0.11,-0.52l-0.77,-1.35l2.1,-0.66l0.39,-0.73l1.05,0.82l2.21,0.11l1.12,-1.37l0.37,-1.81l-0.27,-2.09l-1.13,-1.5l1.0,-2.69l-0.13,-0.45l-0.92,-0.58l-1.6,0.17l-0.51,-0.94l0.1,-0.61l2.75,0.09l3.97,1.24l0.51,-0.33l0.17,-1.28l1.24,-2.21l1.28,-1.14l2.76,0.49Z",name:"Congo"},CF:{path:"M461.16,278.2l-0.26,-1.19l-1.09,-0.77l-0.84,-1.17l-0.29,-1.0l-1.04,-1.15l0.08,-3.43l0.58,-0.49l1.16,-2.35l1.85,-0.17l0.61,-0.62l0.97,0.58l3.15,-0.96l2.48,-1.92l0.02,-0.96l2.81,0.02l2.36,-1.17l1.93,-2.85l1.16,-0.93l1.11,-0.3l0.27,0.86l1.34,1.47l-0.39,2.01l0.3,1.01l4.01,2.75l0.17,0.93l2.63,2.31l0.6,1.44l2.08,1.4l-3.84,-0.21l-1.94,0.88l-1.23,-0.49l-2.67,1.2l-1.29,-0.18l-0.51,0.36l-0.6,1.22l-3.35,-0.65l-1.57,-0.91l-2.42,-0.83l-1.45,0.91l-0.97,1.27l-0.26,1.56l-3.22,-0.43l-1.49,1.33l-0.94,1.62Z",name:"Central African Rep."},CD:{path:"M487.01,272.38l2.34,-0.14l1.35,1.84l1.34,0.45l0.86,-0.39l1.21,0.12l1.07,-0.41l0.54,0.89l2.04,1.54l-0.14,2.72l0.7,0.54l-1.38,1.13l-1.53,2.54l-0.17,2.05l-0.59,1.08l-0.02,1.72l-0.72,0.84l-0.66,3.01l0.63,1.32l-0.44,4.26l0.64,1.47l-0.37,1.22l0.86,1.8l1.53,1.41l0.3,1.26l0.44,0.5l-4.08,0.75l-0.92,1.81l0.51,1.34l-0.74,5.43l0.17,0.38l2.45,1.46l0.54,-0.1l0.12,1.62l-1.28,-0.01l-1.85,-2.35l-1.94,-0.45l-0.48,-1.13l-0.55,-0.2l-1.41,0.74l-1.71,-0.3l-1.01,-1.18l-2.49,-0.19l-0.44,-0.77l-1.98,-0.21l-2.88,0.36l0.11,-2.41l-0.85,-1.13l-0.16,-1.36l0.32,-1.73l-0.46,-0.89l-0.04,-1.49l-0.4,-0.39l-2.53,0.02l0.1,-0.41l-0.39,-0.49l-1.28,0.01l-0.43,0.45l-1.62,0.32l-0.83,1.79l-1.09,-0.28l-2.4,0.52l-1.37,-1.91l-1.3,-3.3l-0.38,-0.27l-7.39,-0.03l-2.46,0.42l0.5,-0.45l0.37,-1.47l0.66,-0.38l0.92,0.08l0.73,-0.82l0.87,0.02l0.31,0.68l1.4,0.36l3.59,-3.63l0.01,-2.23l1.02,-2.29l2.69,-2.39l0.43,-0.99l0.49,-1.96l0.17,-3.51l1.25,-2.95l0.36,-3.14l0.86,-1.13l1.1,-0.66l3.57,1.73l3.65,0.73l0.46,-0.21l0.8,-1.46l1.24,0.19l2.61,-1.17l0.81,0.44l1.04,-0.03l0.59,-0.66l0.7,-0.16l1.81,0.25Z",name:"Dem. Rep. Congo"},CZ:{path:"M458.46,144.88l1.22,1.01l1.47,0.23l0.13,0.93l1.36,0.68l0.54,-0.2l0.24,-0.55l1.15,0.25l0.53,1.09l1.68,0.18l0.6,0.84l-1.04,0.73l-0.96,1.28l-1.6,0.17l-0.55,0.56l-1.04,-0.46l-1.05,0.15l-2.12,-0.96l-1.05,0.34l-1.2,1.12l-1.56,-0.87l-2.57,-2.1l-0.53,-1.88l4.7,-2.52l0.71,0.26l0.9,-0.28Z",name:"Czech Rep."},CY:{path:"M504.36,193.47l0.43,0.28l-1.28,0.57l-0.92,-0.28l-0.24,-0.46l2.01,-0.13Z",name:"Cyprus"},CR:{path:"M211.34,258.05l0.48,0.99l1.6,1.6l-0.54,0.45l0.29,1.42l-0.25,1.19l-1.09,-0.59l-0.05,-1.25l-2.46,-1.42l-0.28,-0.77l-0.66,-0.45l-0.45,-0.0l-0.11,1.04l-1.32,-0.95l0.31,-1.3l-0.36,-0.6l0.31,-0.27l1.42,0.58l1.29,-0.14l0.56,0.56l0.74,0.17l0.55,-0.27Z",name:"Costa Rica"},CU:{path:"M221.21,227.25l1.27,1.02l2.19,-0.28l4.43,3.33l2.08,0.43l-0.1,0.38l0.36,0.5l1.75,0.1l1.48,0.84l-3.11,0.51l-4.15,-0.03l0.77,-0.67l-0.04,-0.64l-1.2,-0.74l-1.49,-0.16l-0.7,-0.61l-0.56,-1.4l-0.4,-0.25l-1.34,0.1l-2.2,-0.66l-0.88,-0.58l-3.18,-0.4l-0.27,-0.16l0.58,-0.74l-0.36,-0.29l-2.72,-0.05l-1.7,1.29l-0.91,0.03l-0.61,0.69l-1.01,0.22l1.11,-1.29l1.01,-0.52l3.69,-1.01l3.98,0.21l2.21,0.84Z",name:"Cuba"},SZ:{path:"M500.35,351.36l0.5,2.04l-0.38,0.89l-1.05,0.21l-1.23,-1.2l-0.02,-0.64l0.83,-1.57l1.34,0.27Z",name:"Swaziland"},SY:{path:"M511.0,199.79l0.05,-1.33l0.54,-1.36l1.28,-0.99l0.13,-0.45l-0.41,-1.11l-1.14,-0.36l-0.19,-1.74l0.52,-1.0l1.29,-1.21l0.2,-1.18l0.59,0.23l2.62,-0.76l1.36,0.52l2.06,-0.01l2.95,-1.08l3.25,-0.26l-0.67,0.94l-1.28,0.66l-0.21,0.4l0.23,2.01l-0.88,3.19l-10.15,5.73l-2.15,-0.85Z",name:"Syria"},KG:{path:"M621.35,172.32l-3.87,1.69l-0.96,1.18l-3.04,0.34l-1.13,1.86l-2.36,-0.35l-1.99,0.63l-2.39,1.4l0.06,0.95l-0.4,0.37l-4.52,0.43l-3.02,-0.93l-2.37,0.17l0.11,-0.79l2.32,0.42l1.13,-0.88l1.99,0.2l3.21,-2.14l-0.03,-0.69l-2.97,-1.57l-1.94,0.65l-1.22,-0.74l1.71,-1.58l-0.12,-0.67l-0.36,-0.15l0.32,-0.77l1.36,-0.35l4.02,1.02l0.49,-0.3l0.35,-1.59l1.09,-0.48l3.42,1.22l1.11,-0.31l7.64,0.39l1.16,1.0l1.23,0.39Z",name:"Kyrgyzstan"},KE:{path:"M506.26,284.69l1.87,-2.56l0.93,-2.15l-1.38,-4.08l-1.06,-1.6l2.82,-2.75l0.79,0.26l0.12,1.41l0.86,0.83l1.9,0.11l3.28,2.13l3.57,0.44l1.05,-1.12l1.96,-0.9l0.82,0.68l1.16,0.09l-1.78,2.45l0.03,9.12l1.3,1.94l-1.37,0.78l-0.67,1.03l-1.08,0.46l-0.34,1.67l-0.81,1.07l-0.45,1.55l-0.68,0.56l-3.2,-2.23l-0.35,-1.58l-8.86,-4.98l0.14,-1.6l-0.57,-1.04Z",name:"Kenya"},SS:{path:"M481.71,263.34l1.07,-0.72l1.2,-3.18l1.36,-0.26l1.61,1.99l0.87,0.34l1.1,-0.41l1.5,0.07l0.57,0.53l2.49,0.0l0.44,-0.63l1.07,-0.4l0.45,-0.84l0.59,-0.33l1.9,1.33l1.6,-0.2l2.83,-3.33l-0.32,-2.21l1.59,-0.52l-0.24,1.6l0.3,1.83l1.35,1.18l0.2,1.87l0.35,0.41l0.02,1.53l-0.23,0.47l-1.42,0.25l-0.85,1.44l0.3,0.6l1.4,0.16l1.11,1.08l0.59,1.13l1.03,0.53l1.28,2.36l-4.41,3.98l-1.74,0.01l-1.89,0.55l-1.47,-0.52l-1.15,0.57l-2.96,-2.62l-1.3,0.49l-1.06,-0.15l-0.79,0.39l-0.82,-0.22l-1.8,-2.7l-1.91,-1.1l-0.66,-1.5l-2.62,-2.32l-0.18,-0.94l-2.37,-1.6Z",name:"S. Sudan"},SR:{path:"M283.12,270.19l2.1,0.53l-1.08,1.95l0.2,1.72l0.93,1.49l-0.59,2.03l-0.43,0.71l-1.12,-0.42l-1.32,0.22l-0.93,-0.2l-0.46,0.26l-0.25,0.73l0.33,0.7l-0.89,-0.13l-1.39,-1.97l-0.31,-1.34l-0.97,-0.31l-0.89,-1.47l0.35,-1.61l1.45,-0.82l0.33,-1.87l2.61,0.44l0.57,-0.47l1.75,-0.16Z",name:"Suriname"},KH:{path:"M689.52,249.39l0.49,1.45l-0.28,2.74l-4.0,1.86l-0.16,0.6l0.68,0.95l-2.06,0.17l-2.05,0.97l-1.82,-0.32l-2.12,-3.7l-0.55,-2.85l1.4,-1.85l3.02,-0.45l2.23,0.35l2.01,0.98l0.51,-0.14l0.95,-1.48l1.74,0.74Z",name:"Cambodia"},SV:{path:"M195.8,250.13l1.4,-1.19l2.24,1.45l0.98,-0.27l0.44,0.2l-0.27,1.05l-1.14,-0.03l-3.64,-1.21Z",name:"El Salvador"},SK:{path:"M476.82,151.17l-1.14,1.9l-2.73,-0.92l-0.82,0.2l-0.74,0.8l-3.46,0.73l-0.47,0.69l-1.76,0.33l-1.88,-1.0l-0.18,-0.81l0.38,-0.75l1.87,-0.32l1.74,-1.89l0.83,0.16l0.79,-0.34l1.51,1.04l1.34,-0.63l1.25,0.3l1.65,-0.42l1.81,0.95Z",name:"Slovakia"},KR:{path:"M737.51,185.84l0.98,-0.1l0.87,-1.17l2.69,-0.32l0.33,-0.29l1.76,2.79l0.58,1.76l0.02,3.12l-0.8,1.32l-2.21,0.55l-1.93,1.13l-1.8,0.19l-0.2,-1.1l0.43,-2.28l-0.95,-2.56l1.43,-0.37l0.23,-0.62l-1.43,-2.06Z",name:"Korea"},SI:{path:"M456.18,162.07l-0.51,-1.32l0.18,-1.05l1.69,0.2l1.42,-0.71l2.09,-0.07l0.62,-0.51l0.21,0.47l-1.61,0.67l-0.44,1.34l-0.66,0.24l-0.26,0.82l-1.22,-0.49l-0.84,0.46l-0.69,-0.04Z",name:"Slovenia"},KP:{path:"M736.77,185.16l-0.92,-0.42l-0.88,0.62l-1.21,-0.88l0.96,-1.15l0.59,-2.59l-0.46,-0.74l-2.09,-0.77l1.64,-1.52l2.72,-1.58l1.58,-1.91l1.11,0.78l2.17,0.11l0.41,-0.5l-0.3,-1.22l3.52,-1.18l0.94,-1.4l0.98,1.08l-2.19,2.18l0.01,2.14l-1.06,0.54l-1.41,1.4l-1.7,0.52l-1.25,1.09l-0.14,1.98l0.94,0.45l1.15,1.04l-0.13,0.26l-2.6,0.29l-1.13,1.29l-1.22,0.08Z",name:"Dem. Rep. Korea"},SO:{path:"M525.13,288.48l-1.13,-1.57l-0.03,-8.86l2.66,-3.38l1.67,-0.13l2.13,-1.69l3.41,-0.23l7.08,-7.55l2.91,-3.69l0.08,-4.82l2.98,-0.67l1.24,-0.86l0.45,-0.0l-0.2,3.0l-1.21,3.62l-2.73,5.97l-2.13,3.65l-5.03,6.16l-8.56,6.4l-2.78,3.08l-0.8,1.56Z",name:"Somalia"},SN:{path:"M390.09,248.21l0.12,1.55l0.49,1.46l0.96,0.82l0.05,1.28l-1.26,-0.19l-0.75,0.33l-1.84,-0.61l-5.84,-0.13l-2.54,0.51l-0.22,-1.03l1.77,0.04l2.01,-0.91l1.03,0.48l1.09,0.04l1.29,-0.62l0.14,-0.58l-0.51,-0.74l-1.81,0.25l-1.13,-0.63l-0.79,0.04l-0.72,0.61l-2.31,0.06l-0.92,-1.77l-0.81,-0.64l0.64,-0.35l2.46,-3.74l1.04,0.19l1.38,-0.56l1.19,-0.02l2.72,1.37l3.03,3.48Z",name:"Senegal"},SL:{path:"M394.46,264.11l-1.73,1.98l-0.58,1.33l-2.07,-1.06l-1.22,-1.26l-0.65,-2.39l1.16,-0.96l0.67,-1.17l1.21,-0.52l1.66,0.0l1.03,1.64l0.52,2.41Z",name:"Sierra Leone"},SB:{path:"M826.69,311.6l-0.61,0.09l-0.2,-0.33l0.37,0.15l0.44,0.09ZM824.18,307.38l-0.26,-0.3l-0.31,-0.91l0.03,0.0l0.54,1.21ZM823.04,309.33l-1.66,-0.22l-0.2,-0.52l1.16,0.28l0.69,0.46ZM819.28,304.68l1.14,0.65l0.02,0.03l-0.81,-0.44l-0.35,-0.23Z",name:"Solomon Is."},SA:{path:"M537.53,210.34l2.0,0.24l0.9,1.32l1.49,-0.06l0.87,2.08l1.29,0.76l0.51,0.99l1.56,1.03l-0.1,1.9l0.32,0.9l1.58,2.47l0.76,0.53l0.7,-0.04l1.68,4.23l7.53,1.33l0.51,-0.29l0.77,1.25l-1.55,4.87l-7.29,2.52l-7.3,1.03l-2.34,1.17l-1.88,2.74l-0.76,0.28l-0.82,-0.78l-0.91,0.12l-2.88,-0.51l-3.51,0.25l-0.86,-0.56l-0.57,0.15l-0.66,1.27l0.16,1.11l-0.43,0.32l-0.93,-1.4l-0.33,-1.16l-1.23,-0.88l-1.27,-2.06l-0.78,-2.22l-1.73,-1.79l-1.14,-0.48l-1.54,-2.31l-0.21,-3.41l-1.44,-2.93l-1.27,-1.16l-1.33,-0.57l-1.31,-3.37l-0.77,-0.67l-0.97,-1.97l-2.8,-4.03l-1.06,-0.17l0.37,-1.96l0.2,-0.72l2.74,0.3l1.08,-0.84l0.6,-0.94l1.74,-0.35l0.65,-1.03l0.71,-0.4l0.1,-0.62l-2.06,-2.28l4.39,-1.22l0.48,-0.37l2.77,0.69l3.66,1.9l7.03,5.5l4.87,0.3Z",name:"Saudi Arabia"},SE:{path:"M480.22,89.3l-4.03,1.17l-2.43,2.86l0.26,2.57l-8.77,6.64l-1.78,5.79l1.78,2.68l2.22,1.96l-2.07,3.77l-2.72,1.13l-0.95,6.04l-1.29,3.01l-2.74,-0.31l-0.4,0.22l-1.31,2.59l-2.34,0.13l-0.75,-3.09l-2.08,-4.03l-1.83,-4.96l1.0,-1.93l2.14,-2.7l0.83,-4.45l-1.6,-2.17l-0.15,-4.94l1.48,-3.39l2.58,-0.15l0.87,-1.59l-0.78,-1.57l3.76,-5.59l4.04,-7.48l2.17,0.01l0.39,-0.29l0.57,-2.07l4.37,0.64l0.46,-0.34l0.33,-2.56l1.1,-0.13l6.94,4.87l0.06,6.32l0.66,1.36Z",name:"Sweden"},SD:{path:"M505.98,259.4l-0.34,-0.77l-1.17,-0.9l-0.26,-1.61l0.29,-1.81l-0.34,-0.46l-1.16,-0.17l-0.54,0.59l-1.23,0.11l-0.28,0.65l0.53,0.65l0.17,1.22l-2.44,3.0l-0.96,0.19l-2.39,-1.4l-0.95,0.52l-0.38,0.78l-1.11,0.41l-0.29,0.5l-1.94,0.0l-0.54,-0.52l-1.81,-0.09l-0.95,0.4l-2.45,-2.35l-2.07,0.54l-0.73,1.26l-0.6,2.1l-1.25,0.58l-0.75,-0.62l0.27,-2.65l-1.48,-1.78l-0.22,-1.48l-0.92,-0.96l-0.02,-1.29l-0.57,-1.16l-0.68,-0.16l0.69,-1.29l-0.18,-1.14l0.65,-0.62l0.03,-0.55l-0.36,-0.41l1.55,-2.97l1.91,0.16l0.43,-0.4l-0.1,-10.94l2.49,-0.01l0.4,-0.4l-0.0,-4.82l29.02,0.0l0.64,2.04l-0.49,0.66l0.36,2.69l0.93,3.16l2.12,1.55l-0.89,1.04l-1.72,0.39l-0.98,0.9l-1.43,5.65l0.24,1.15l-0.38,2.06l-0.96,2.38l-1.53,1.31l-1.32,2.91l-1.22,0.86l-0.37,1.34Z",name:"Sudan"},DO:{path:"M241.8,239.2l0.05,-0.65l-0.46,-0.73l0.42,-0.44l0.19,-1.0l-0.09,-1.53l1.66,0.01l1.99,0.63l0.33,0.67l1.28,0.19l0.33,0.76l1.0,0.08l0.8,0.62l-0.45,0.51l-1.13,-0.47l-1.88,-0.01l-1.27,0.59l-0.75,-0.55l-1.01,0.54l-0.79,1.4l-0.23,-0.61Z",name:"Dominican Rep."},DJ:{path:"M528.43,256.18l-0.45,0.66l-0.58,-0.25l-1.51,0.13l-0.18,-1.01l1.45,-1.95l0.83,0.17l0.77,-0.44l0.2,1.0l-1.2,0.51l-0.06,0.7l0.73,0.47Z",name:"Djibouti"},DK:{path:"M452.28,129.07l-1.19,2.24l-2.13,-1.6l-0.23,-0.95l2.98,-0.95l0.57,1.26ZM447.74,126.31l-0.26,0.57l-0.88,-0.07l-1.8,2.53l0.48,1.69l-1.09,0.36l-1.61,-0.39l-0.89,-1.69l-0.07,-3.43l0.96,-1.73l2.02,-0.2l1.09,-1.07l1.33,-0.67l-0.05,1.06l-0.73,1.41l0.3,1.0l1.2,0.64Z",name:"Denmark"},DE:{path:"M453.14,155.55l-0.55,-0.36l-1.2,-0.1l-1.87,0.57l-2.13,-0.13l-0.56,0.63l-0.86,-0.6l-0.96,0.09l-2.57,-0.93l-0.85,0.67l-1.47,-0.02l0.24,-1.75l1.23,-2.14l-0.28,-0.59l-3.52,-0.58l-0.92,-0.66l0.12,-1.2l-0.48,-0.88l0.27,-2.17l-0.37,-3.03l1.41,-0.22l0.63,-1.26l0.66,-3.19l-0.41,-1.18l0.26,-0.39l1.66,-0.15l0.33,0.54l0.62,0.07l1.7,-1.69l-0.54,-3.02l1.37,0.33l1.31,-0.37l0.31,1.18l2.25,0.71l-0.02,0.92l0.5,0.4l2.55,-0.65l1.34,-0.87l2.57,1.24l1.06,0.98l0.48,1.44l-0.57,0.74l-0.0,0.48l0.87,1.15l0.57,1.64l-0.14,1.29l0.82,1.7l-1.5,-0.07l-0.56,0.57l-4.47,2.15l-0.22,0.54l0.68,2.26l2.58,2.16l-0.66,1.11l-0.79,0.36l-0.23,0.43l0.32,1.87Z",name:"Germany"},YE:{path:"M528.27,246.72l0.26,-0.42l-0.22,-1.01l0.19,-1.5l0.92,-0.69l-0.07,-1.35l0.39,-0.75l1.01,0.47l3.34,-0.27l3.76,0.41l0.95,0.81l1.36,-0.58l1.74,-2.62l2.18,-1.09l6.86,-0.94l2.48,5.41l-1.64,0.76l-0.56,1.9l-6.23,2.16l-2.29,1.8l-1.93,0.05l-1.41,1.02l-4.24,0.74l-1.72,1.49l-3.28,0.19l-0.52,-1.18l0.02,-1.51l-1.34,-3.29Z",name:"Yemen"},AT:{path:"M462.89,152.8l0.04,2.25l-1.07,0.0l-0.33,0.63l0.36,0.51l-1.04,2.13l-2.02,0.07l-1.33,0.7l-5.29,-0.99l-0.47,-0.93l-0.44,-0.21l-2.47,0.55l-0.42,0.51l-3.18,-0.81l0.43,-0.91l1.12,0.78l0.6,-0.17l0.25,-0.58l1.93,0.12l1.86,-0.56l1.0,0.08l0.68,0.57l0.62,-0.15l0.26,-0.77l-0.3,-1.78l0.8,-0.44l0.68,-1.15l1.52,0.85l0.47,-0.06l1.34,-1.25l0.64,-0.17l1.81,0.92l1.28,-0.11l0.7,0.37Z",name:"Austria"},DZ:{path:"M441.46,188.44l-0.32,1.07l0.39,2.64l-0.54,2.16l-1.58,1.82l0.37,2.39l1.91,1.55l0.18,0.8l1.42,1.03l1.84,7.23l0.12,1.16l-0.57,5.0l0.2,1.51l-0.87,0.99l-0.02,0.51l1.41,1.86l0.14,1.2l0.89,1.48l0.5,0.16l0.98,-0.41l1.73,1.08l0.82,1.23l-8.22,4.81l-7.23,5.11l-3.43,1.13l-2.3,0.21l-0.28,-1.59l-2.56,-1.09l-0.67,-1.25l-26.12,-17.86l0.01,-3.47l3.77,-1.88l2.44,-0.41l2.12,-0.75l1.08,-1.42l2.81,-1.05l0.35,-2.08l1.33,-0.29l1.04,-0.94l3.47,-0.69l0.46,-1.08l-0.1,-0.45l-0.58,-0.52l-0.82,-2.81l-0.19,-1.83l-0.78,-1.49l2.03,-1.31l2.63,-0.48l1.7,-1.22l2.31,-0.84l8.24,-0.73l1.49,0.38l2.28,-1.1l2.46,-0.02l0.92,0.6l1.35,-0.05Z",name:"Algeria"},US:{path:"M892.72,99.2l1.31,0.53l1.41,-0.37l1.89,0.98l1.89,0.42l-1.32,0.58l-2.9,-1.53l-2.08,0.22l-0.26,-0.15l0.07,-0.67ZM183.22,150.47l0.37,1.47l1.12,0.85l4.23,0.7l2.39,0.98l2.17,-0.38l1.85,0.5l-1.55,0.65l-3.49,2.61l-0.16,0.77l0.5,0.39l2.33,-0.61l1.77,1.02l5.15,-2.4l-0.31,0.65l0.25,0.56l1.36,0.38l1.71,1.16l4.7,-0.88l0.67,0.85l1.31,0.21l0.58,0.58l-1.34,0.17l-2.18,-0.32l-3.6,0.89l-2.71,3.25l0.35,0.9l0.59,-0.0l0.55,-0.6l-1.36,4.65l0.29,3.09l0.67,1.58l0.61,0.45l1.77,-0.44l1.6,-1.96l0.14,-2.21l-0.82,-1.96l0.11,-1.13l1.19,-2.37l0.44,-0.33l0.48,0.75l0.4,-0.29l0.4,-1.37l0.6,-0.47l0.24,-0.8l1.69,0.49l1.65,1.08l-0.03,2.37l-1.27,1.13l-0.0,1.13l0.87,0.36l1.66,-1.29l0.5,0.17l0.5,2.6l-2.49,3.75l0.17,0.61l1.54,0.62l1.48,0.17l1.92,-0.44l4.72,-2.15l2.16,-1.8l-0.05,-1.24l0.75,-0.22l3.92,0.36l2.12,-1.05l0.21,-0.4l-0.28,-1.48l3.27,-2.4l8.32,-0.02l0.56,-0.82l1.9,-0.77l0.93,-1.51l0.74,-2.37l1.58,-1.98l0.92,0.62l1.47,-0.47l0.8,0.66l-0.0,4.09l1.96,2.6l-2.34,1.31l-5.37,2.09l-1.83,2.72l0.02,1.79l0.83,1.59l0.54,0.23l-6.19,0.94l-2.2,0.89l-0.23,0.48l0.45,0.29l2.99,-0.46l-2.19,0.56l-1.13,0.0l-0.15,-0.32l-0.48,0.08l-0.76,0.82l0.22,0.67l0.32,0.06l-0.41,1.62l-1.27,1.58l-1.48,-1.07l-0.49,-0.04l-0.16,0.46l0.52,1.58l0.61,0.59l0.03,0.79l-0.95,1.38l-1.21,-1.22l-0.27,-2.27l-0.35,-0.35l-0.42,0.25l-0.48,1.27l0.33,1.41l-0.97,-0.27l-0.48,0.24l0.18,0.5l1.52,0.83l0.1,2.52l0.79,0.51l0.52,3.42l-1.42,1.88l-2.47,0.8l-1.71,1.66l-1.31,0.25l-1.27,1.03l-0.43,0.99l-2.69,1.78l-2.64,3.03l-0.45,2.12l0.45,2.08l0.85,2.38l1.09,1.9l0.04,1.2l1.16,3.06l-0.18,2.69l-0.55,1.43l-0.47,0.21l-0.89,-0.23l-0.49,-1.18l-0.87,-0.56l-2.75,-5.16l0.48,-1.68l-0.72,-1.78l-2.01,-2.38l-1.12,-0.53l-2.72,1.18l-1.47,-1.35l-1.57,-0.68l-2.99,0.31l-2.17,-0.3l-2.0,0.19l-1.15,0.46l-0.19,0.58l0.39,0.63l0.14,1.34l-0.84,-0.2l-0.84,0.46l-1.58,-0.07l-2.08,-1.44l-2.09,0.33l-1.91,-0.62l-3.73,0.84l-2.39,2.07l-2.54,1.22l-1.45,1.41l-0.61,1.38l0.34,3.71l-0.29,0.02l-3.5,-1.33l-1.25,-3.11l-1.44,-1.5l-2.24,-3.56l-1.76,-1.09l-2.27,-0.01l-1.71,2.07l-1.76,-0.69l-1.16,-0.74l-1.52,-2.98l-3.93,-3.16l-4.34,-0.0l-0.4,0.4l-0.0,0.74l-6.5,0.02l-9.02,-3.14l-0.34,-0.71l-5.7,0.49l-0.43,-1.29l-1.62,-1.61l-1.14,-0.38l-0.55,-0.88l-1.28,-0.13l-1.01,-0.77l-2.22,-0.27l-0.43,-0.3l-0.36,-1.58l-2.4,-2.83l-2.01,-3.85l-0.06,-0.9l-2.92,-3.26l-0.33,-2.29l-1.3,-1.66l0.52,-2.37l-0.09,-2.57l-0.78,-2.3l0.95,-2.82l0.61,-5.68l-0.47,-4.27l-1.46,-4.08l3.19,0.79l1.26,2.83l0.69,0.08l0.69,-1.14l-1.1,-4.79l68.76,-0.0l0.4,-0.4l0.14,-0.86ZM32.44,67.52l1.73,1.97l0.55,0.05l0.99,-0.79l3.65,0.24l-0.09,0.62l0.32,0.45l3.83,0.77l2.61,-0.43l5.19,1.4l4.84,0.43l1.89,0.57l3.42,-0.7l6.14,1.87l-0.03,38.06l0.38,0.4l2.39,0.11l2.31,0.98l3.9,3.99l0.55,0.04l2.4,-2.03l2.16,-1.04l1.2,1.71l3.95,3.14l4.09,6.63l4.2,2.29l0.06,1.83l-1.02,1.23l-1.16,-1.08l-2.04,-1.03l-0.67,-2.89l-3.28,-3.03l-1.65,-3.57l-6.35,-0.32l-2.82,-1.01l-5.26,-3.85l-6.77,-2.04l-3.53,0.3l-4.81,-1.69l-3.25,-1.63l-2.78,0.8l-0.28,0.46l0.44,2.21l-3.91,0.96l-2.26,1.27l-2.3,0.65l-0.27,-1.65l1.05,-3.42l2.49,-1.09l0.16,-0.6l-0.69,-0.96l-0.55,-0.1l-3.19,2.12l-1.78,2.56l-3.55,2.61l-0.04,0.61l1.56,1.52l-2.07,2.29l-5.11,2.57l-0.77,1.66l-3.76,1.77l-0.92,1.73l-2.69,1.38l-1.81,-0.22l-6.95,3.32l-3.97,0.91l4.85,-2.5l2.59,-1.86l3.26,-0.52l1.19,-1.4l3.42,-2.1l2.59,-2.27l0.42,-2.68l1.23,-2.1l-0.04,-0.46l-0.45,-0.11l-2.68,1.03l-0.63,-0.49l-0.53,0.03l-1.05,1.04l-1.36,-1.54l-0.66,0.08l-0.32,0.62l-0.58,-1.14l-0.56,-0.16l-2.41,1.42l-1.07,-0.0l-0.17,-1.75l0.3,-1.71l-1.61,-1.33l-3.41,0.59l-1.96,-1.63l-1.57,-0.84l-0.15,-2.21l-1.7,-1.43l0.82,-1.88l1.99,-2.12l0.88,-1.92l1.71,-0.24l2.04,0.51l1.87,-1.77l1.91,0.25l1.91,-1.23l0.17,-0.43l-0.47,-1.82l-1.07,-0.7l1.39,-1.17l0.12,-0.45l-0.39,-0.26l-1.65,0.07l-2.66,0.88l-0.75,0.78l-1.92,-0.8l-3.46,0.44l-3.44,-0.91l-1.06,-1.61l-2.65,-1.99l2.91,-1.43l5.5,-2.0l1.52,0.0l-0.26,1.62l0.41,0.46l5.29,-0.16l0.3,-0.65l-2.03,-2.59l-3.14,-1.68l-1.79,-2.12l-2.4,-1.83l-3.09,-1.24l1.04,-1.69l4.23,-0.14l3.36,-2.07l0.73,-2.27l2.39,-1.99l2.42,-0.52l4.65,-1.97l2.46,0.23l3.71,-2.35l3.5,0.89ZM37.6,123.41l-2.25,1.23l-0.95,-0.69l-0.29,-1.24l3.21,-1.63l1.42,0.21l0.67,0.7l-1.8,1.42ZM31.06,234.03l0.98,0.47l0.74,0.87l-1.77,1.07l-0.44,-1.53l0.49,-0.89ZM29.34,232.07l0.18,0.05l0.08,0.05l-0.16,0.03l-0.11,-0.14ZM25.16,230.17l0.05,-0.03l0.18,0.22l-0.13,-0.01l-0.1,-0.18ZM5.89,113.26l-1.08,0.41l-2.21,-1.12l1.53,-0.4l1.62,0.28l0.14,0.83Z",name:"United States"},LV:{path:"M489.16,122.85l0.96,0.66l0.22,1.65l0.68,1.76l-3.65,1.7l-2.23,-1.58l-1.29,-0.26l-0.68,-0.77l-2.42,0.34l-4.16,-0.23l-2.47,0.9l0.06,-1.98l1.13,-2.06l1.95,-1.02l2.12,2.58l2.01,-0.07l0.38,-0.33l0.44,-2.52l1.76,-0.53l3.06,1.7l2.15,0.07Z",name:"Latvia"},UY:{path:"M286.85,372.74l-0.92,1.5l-2.59,1.44l-1.69,-0.52l-1.42,0.26l-2.39,-1.19l-1.52,0.08l-1.27,-1.3l0.16,-1.5l0.56,-0.79l-0.02,-2.73l1.21,-4.74l1.19,-0.21l2.37,2.0l1.08,0.03l4.36,3.17l1.22,1.6l-0.96,1.5l0.61,1.4Z",name:"Uruguay"},LB:{path:"M510.37,198.01l-0.88,0.51l1.82,-3.54l0.62,0.08l0.22,0.61l-1.13,0.88l-0.65,1.47Z",name:"Lebanon"},LA:{path:"M689.54,248.53l-1.76,-0.74l-0.49,0.15l-0.94,1.46l-1.32,-0.64l0.62,-0.98l0.11,-2.17l-2.04,-2.42l-0.25,-2.65l-1.9,-2.1l-2.15,-0.31l-0.78,0.91l-1.12,0.06l-1.05,-0.4l-2.06,1.2l-0.04,-1.59l0.61,-2.68l-0.36,-0.49l-1.35,-0.1l-0.11,-1.23l-0.96,-0.88l1.96,-1.89l0.39,0.36l1.33,0.07l0.42,-0.45l-0.34,-2.66l0.7,-0.21l1.28,1.81l1.11,2.35l0.36,0.23l2.82,0.02l0.71,1.67l-1.39,0.65l-0.72,0.93l0.13,0.6l2.91,1.51l3.6,5.25l1.88,1.78l0.56,1.62l-0.35,1.96Z",name:"Lao PDR"},TW:{path:"M724.01,226.68l-0.74,1.48l-0.9,-1.52l-0.25,-1.74l1.38,-2.44l1.73,-1.74l0.64,0.44l-1.85,5.52Z",name:"Taiwan"},TT:{path:"M266.64,259.32l0.28,-1.16l1.13,-0.22l-0.06,1.2l-1.35,0.18Z",name:"Trinidad and Tobago"},TR:{path:"M513.21,175.47l3.64,1.17l3.05,-0.44l2.1,0.26l3.11,-1.56l2.46,-0.13l2.19,1.33l0.33,0.82l-0.22,1.33l0.25,0.44l2.28,1.13l-1.17,0.57l-0.21,0.45l0.75,3.2l-0.41,1.16l1.13,1.92l-0.55,0.22l-0.9,-0.67l-2.91,-0.37l-1.24,0.46l-4.23,0.41l-2.81,1.05l-1.91,0.01l-1.52,-0.53l-2.58,0.75l-0.66,-0.45l-0.62,0.3l-0.12,1.45l-0.89,0.84l-0.47,-0.67l0.79,-1.3l-0.41,-0.2l-1.43,0.23l-2.0,-0.63l-2.02,1.65l-3.51,0.3l-2.13,-1.53l-2.7,-0.1l-0.86,1.24l-1.38,0.27l-2.29,-1.44l-2.71,-0.01l-1.37,-2.65l-1.68,-1.52l1.07,-1.99l-0.09,-0.49l-1.27,-1.12l2.37,-2.41l3.7,-0.11l1.28,-2.24l4.49,0.37l3.21,-1.97l2.81,-0.82l3.99,-0.06l4.29,2.07ZM488.79,176.72l-1.72,1.31l-0.5,-0.88l1.37,-2.57l-0.7,-0.85l1.7,-0.63l1.8,0.34l0.46,1.17l1.76,0.78l-2.87,0.32l-1.3,1.01Z",name:"Turkey"},LK:{path:"M624.16,268.99l-1.82,0.48l-0.99,-1.67l-0.42,-3.46l0.95,-3.43l1.21,0.98l2.26,4.19l-0.34,2.33l-0.85,0.58Z",name:"Sri Lanka"},TN:{path:"M448.1,188.24l-1.0,1.27l-0.02,1.32l0.84,0.88l-0.28,2.09l-1.53,1.32l-0.12,0.42l0.48,1.54l1.42,0.32l0.53,1.11l0.9,0.52l-0.11,1.67l-3.54,2.64l-0.1,2.38l-0.58,0.3l-0.96,-4.45l-1.54,-1.25l-0.16,-0.78l-1.92,-1.56l-0.18,-1.76l1.51,-1.62l0.59,-2.34l-0.38,-2.78l0.42,-1.21l2.45,-1.05l1.29,0.26l-0.06,1.11l0.58,0.38l1.47,-0.73Z",name:"Tunisia"},TL:{path:"M734.55,307.93l-0.1,-0.97l4.5,-0.86l-2.82,1.28l-1.59,0.55Z",name:"Timor-Leste"},TM:{path:"M553.03,173.76l-0.04,0.34l-0.09,-0.22l0.13,-0.12ZM555.87,172.66l0.45,-0.1l1.48,0.74l2.06,2.43l4.07,-0.18l0.38,-0.51l-0.32,-1.19l1.92,-0.94l1.91,-1.59l2.94,1.39l0.43,2.47l1.19,0.67l2.58,-0.13l0.62,0.4l1.32,3.12l4.54,3.44l2.67,1.45l3.06,1.14l-0.04,1.05l-1.33,-0.75l-0.59,0.19l-0.32,0.84l-2.2,0.81l-0.46,2.13l-1.21,0.74l-1.91,0.42l-0.73,1.33l-1.56,0.31l-2.22,-0.94l-0.2,-2.17l-0.38,-0.36l-1.73,-0.09l-2.76,-2.46l-2.14,-0.4l-2.84,-1.48l-1.78,-0.27l-1.24,0.53l-1.57,-0.08l-2.0,1.69l-1.7,0.43l-0.36,-1.58l0.36,-2.98l-0.22,-0.4l-1.65,-0.84l0.54,-1.69l-0.34,-0.52l-1.22,-0.13l0.36,-1.64l2.22,0.59l2.2,-0.95l0.12,-0.65l-1.77,-1.74l-0.66,-1.57Z",name:"Turkmenistan"},TJ:{path:"M597.75,178.82l-2.54,-0.44l-0.47,0.34l-0.24,1.7l0.43,0.45l2.64,-0.22l3.18,0.95l4.39,-0.41l0.56,2.37l0.52,0.29l0.67,-0.24l1.11,0.49l0.21,2.13l-3.76,-0.21l-1.8,1.32l-1.76,0.74l-0.61,-0.58l0.21,-2.23l-0.64,-0.49l-0.07,-0.93l-1.36,-0.66l-0.45,0.07l-1.08,1.01l-0.55,1.48l-1.31,-0.05l-0.95,1.16l-0.9,-0.35l-1.86,0.74l1.26,-2.83l-0.54,-2.17l-1.67,-0.82l0.33,-0.66l2.18,-0.04l1.19,-1.63l0.76,-1.79l2.43,-0.5l-0.26,1.0l0.73,1.05Z",name:"Tajikistan"},LS:{path:"M491.06,363.48l-0.49,0.15l-1.49,-1.67l1.1,-1.43l2.19,-1.44l1.51,1.27l-0.98,1.82l-1.23,0.38l-0.62,0.93Z",name:"Lesotho"},TH:{path:"M670.27,255.86l-1.41,3.87l0.15,2.0l0.38,0.36l1.38,0.07l0.9,2.04l0.55,2.34l1.4,1.44l1.61,0.38l0.96,0.97l-0.5,0.64l-1.1,0.2l-0.34,-1.18l-2.04,-1.1l-0.63,0.23l-0.63,-0.62l-0.48,-1.3l-2.56,-2.63l-0.73,0.41l0.95,-3.89l2.16,-4.22ZM670.67,254.77l-0.92,-2.18l-0.26,-2.61l-2.14,-3.06l0.71,-0.49l0.89,-2.59l-3.61,-5.45l0.87,-0.51l1.05,-2.58l1.74,-0.18l2.6,-1.59l0.76,0.56l0.13,1.39l0.37,0.36l1.23,0.09l-0.51,2.28l0.05,2.42l0.6,0.34l2.43,-1.42l0.77,0.39l1.47,-0.07l0.71,-0.88l1.48,0.14l1.71,1.88l0.25,2.65l1.92,2.11l-0.1,1.89l-0.61,0.86l-2.22,-0.33l-3.5,0.64l-1.6,2.12l0.36,2.58l-1.51,-0.79l-1.84,-0.01l0.28,-1.52l-0.4,-0.47l-2.21,0.01l-0.4,0.37l-0.19,2.74l-0.34,0.93Z",name:"Thailand"},TF:{path:"M596.68,420.38l-3.2,0.18l-0.05,-1.26l0.39,-1.41l1.3,0.78l2.08,0.35l-0.52,1.36Z",name:"Fr. S. Antarctic Lands"},TG:{path:"M422.7,257.63l-0.09,1.23l1.53,1.52l0.08,1.09l0.5,0.65l-0.11,5.62l0.49,1.47l-1.31,0.35l-1.02,-2.13l-0.18,-1.12l0.53,-2.19l-0.63,-1.16l-0.22,-3.68l-1.01,-1.4l0.07,-0.28l1.37,0.03Z",name:"Togo"},TD:{path:"M480.25,235.49l0.12,9.57l-2.1,0.05l-1.14,1.89l-0.69,1.63l0.34,0.73l-0.66,0.91l0.24,0.89l-0.86,1.95l0.45,0.5l0.6,-0.1l0.34,0.64l0.03,1.38l0.9,1.04l-1.45,0.43l-1.27,1.03l-1.83,2.76l-2.16,1.07l-2.31,-0.15l-0.86,0.25l-0.26,0.49l0.17,0.61l-2.11,1.68l-2.85,0.87l-1.09,-0.57l-0.73,0.66l-1.12,0.1l-1.1,-3.12l-1.25,-0.64l-1.22,-1.22l0.29,-0.64l3.01,0.04l0.35,-0.6l-1.3,-2.2l-0.08,-3.31l-0.97,-1.66l0.22,-1.04l-0.38,-0.48l-1.22,-0.04l0.0,-1.25l-0.98,-1.07l0.96,-3.01l3.25,-2.65l0.13,-3.33l0.95,-5.18l0.52,-1.07l-0.1,-0.48l-0.91,-0.78l-0.2,-0.96l-0.8,-0.58l-0.55,-3.65l2.1,-1.2l19.57,9.83Z",name:"Chad"},LY:{path:"M483.48,203.15l-0.75,1.1l0.29,1.39l-0.6,1.83l0.73,2.14l0.0,24.12l-2.48,0.01l-0.41,0.85l-19.41,-9.76l-4.41,2.28l-1.37,-1.33l-3.82,-1.1l-1.14,-1.65l-1.98,-1.23l-1.22,0.32l-0.66,-1.11l-0.17,-1.26l-1.28,-1.69l0.87,-1.19l-0.07,-4.34l0.43,-2.27l-0.86,-3.45l1.13,-0.76l0.22,-1.16l-0.2,-1.03l3.48,-2.61l0.29,-1.94l2.45,0.8l1.18,-0.21l1.98,0.44l3.15,1.18l1.37,2.54l5.72,1.67l2.64,1.35l1.61,-0.72l1.29,-1.34l-0.44,-2.34l0.66,-1.13l1.67,-1.21l1.57,-0.35l3.14,0.53l1.08,1.28l3.99,0.78l0.36,0.54Z",name:"Libya"},AE:{path:"M550.76,223.97l1.88,-0.4l3.84,0.02l4.78,-4.75l0.19,0.36l0.26,1.58l-0.81,0.01l-0.39,0.35l-0.08,2.04l-0.81,0.63l-0.01,0.96l-0.66,0.99l-0.39,1.41l-7.08,-1.25l-0.7,-1.96Z",name:"United Arab Emirates"},VE:{path:"M240.68,256.69l0.53,0.75l-0.02,1.06l-1.07,1.78l0.95,2.0l0.42,0.22l1.4,-0.44l0.56,-1.83l-0.77,-1.17l-0.1,-1.47l2.82,-0.93l0.26,-0.49l-0.28,-0.96l0.3,-0.28l0.66,1.31l1.96,0.26l1.4,1.22l0.08,0.68l0.39,0.35l4.81,-0.22l1.49,1.11l1.92,0.31l1.67,-0.84l0.22,-0.6l3.44,-0.14l-0.17,0.55l0.86,1.19l2.19,0.35l1.67,1.1l0.37,1.86l0.41,0.32l1.55,0.17l-1.66,1.35l-0.22,0.92l0.65,0.97l-1.67,0.54l-0.3,0.4l0.04,0.99l-0.56,0.57l-0.01,0.55l1.85,2.27l-0.66,0.69l-4.47,1.29l-0.72,0.54l-3.69,-0.9l-0.71,0.27l-0.02,0.7l0.91,0.53l-0.08,1.54l0.35,1.58l0.35,0.31l1.66,0.17l-1.3,0.52l-0.48,1.13l-2.68,0.91l-0.6,0.77l-1.57,0.13l-1.17,-1.13l-0.8,-2.52l-1.25,-1.26l1.02,-1.23l-1.29,-2.95l0.18,-1.62l1.0,-2.21l-0.2,-0.49l-1.14,-0.46l-4.02,0.36l-1.82,-2.1l-1.57,-0.33l-2.99,0.22l-1.06,-0.97l0.25,-1.23l-0.2,-1.01l-0.59,-0.69l-0.29,-1.06l-1.08,-0.39l0.78,-2.79l1.9,-2.11Z",name:"Venezuela"},AF:{path:"M600.7,188.88l-1.57,1.3l-0.1,0.48l0.8,2.31l-1.09,1.04l-0.03,1.27l-0.48,0.71l-2.16,-0.08l-0.37,0.59l0.78,1.48l-1.38,0.69l-1.06,1.69l0.06,1.7l-0.65,0.52l-0.91,-0.21l-1.91,0.36l-0.48,0.77l-1.88,0.13l-1.4,1.56l-0.18,2.32l-2.91,1.02l-1.65,-0.23l-0.71,0.55l-1.41,-0.3l-2.41,0.39l-3.52,-1.17l1.96,-2.35l-0.21,-1.78l-0.3,-0.34l-1.63,-0.4l-0.19,-1.58l-0.75,-2.03l0.95,-1.36l-0.19,-0.6l-0.73,-0.28l1.47,-4.8l2.14,0.9l2.12,-0.36l0.74,-1.34l1.77,-0.39l1.54,-0.92l0.63,-2.31l1.87,-0.5l0.49,-0.81l0.94,0.56l2.13,0.11l2.55,0.92l1.95,-0.83l0.65,0.43l0.56,-0.13l0.69,-1.12l1.57,-0.08l0.72,-1.66l0.79,-0.74l0.8,0.39l-0.17,0.56l0.71,0.58l-0.08,2.39l1.11,0.95ZM601.37,188.71l1.73,-0.71l1.43,-1.18l4.03,0.35l-2.23,0.74l-4.95,0.8Z",name:"Afghanistan"},IQ:{path:"M530.82,187.47l0.79,0.66l1.26,-0.28l1.46,3.08l1.63,0.94l0.14,1.23l-1.22,1.05l-0.53,2.52l1.73,2.67l3.12,1.62l1.15,1.88l-0.38,1.85l0.39,0.48l0.41,-0.0l0.02,1.07l0.76,0.94l-2.47,-0.1l-1.71,2.44l-4.31,-0.2l-7.02,-5.48l-3.73,-1.94l-2.88,-0.73l-0.85,-2.87l5.45,-3.02l0.95,-3.43l-0.19,-1.96l1.27,-0.7l1.22,-1.7l0.87,-0.36l2.69,0.34Z",name:"Iraq"},IS:{path:"M384.14,88.06l-0.37,2.61l2.54,2.51l-2.9,2.75l-9.19,3.4l-9.25,-1.66l1.7,-1.22l-0.1,-0.7l-4.05,-1.47l2.96,-0.53l0.33,-0.43l-0.11,-1.2l-0.33,-0.36l-4.67,-0.85l1.28,-2.04l3.45,-0.56l3.77,2.72l0.44,0.02l3.64,-2.16l3.3,1.08l3.98,-2.16l3.58,0.26Z",name:"Iceland"},IR:{path:"M533.43,187.16l-1.27,-2.15l0.42,-0.98l-0.71,-3.04l1.03,-0.5l0.33,0.83l1.26,1.35l2.05,0.51l1.11,-0.16l2.89,-2.11l0.62,-0.14l0.39,0.46l-0.72,1.2l0.06,0.49l1.56,1.53l0.65,0.04l0.67,1.81l2.56,0.83l1.87,1.48l3.69,0.49l3.91,-0.76l0.47,-0.73l2.17,-0.6l1.66,-1.54l1.51,0.08l1.18,-0.53l1.59,0.24l2.83,1.48l1.88,0.3l2.77,2.47l1.77,0.18l0.18,1.99l-1.68,5.49l0.24,0.5l0.61,0.23l-0.82,1.48l0.8,2.18l0.19,1.71l0.3,0.34l1.63,0.4l0.15,1.32l-2.15,2.35l-0.01,0.53l2.21,3.03l2.34,1.24l0.06,2.14l1.24,0.72l0.11,0.69l-3.31,1.27l-1.08,3.03l-9.68,-1.68l-0.99,-3.05l-1.43,-0.73l-2.17,0.46l-2.47,1.26l-2.83,-0.82l-2.46,-2.02l-2.41,-0.8l-3.42,-6.06l-0.48,-0.2l-1.18,0.39l-1.44,-0.82l-0.5,0.08l-0.65,0.74l-0.97,-1.01l-0.02,-1.31l-0.71,-0.39l0.26,-1.81l-1.29,-2.11l-3.13,-1.63l-1.58,-2.43l0.5,-1.9l1.31,-1.26l-0.19,-1.66l-1.74,-1.1l-1.57,-3.3Z",name:"Iran"},AM:{path:"M536.99,182.33l-0.28,0.03l-1.23,-2.13l-0.93,0.01l-0.62,-0.66l-0.69,-0.07l-0.96,-0.81l-1.56,-0.62l0.19,-1.12l-0.26,-0.79l2.72,-0.36l1.09,1.01l-0.17,0.92l1.02,0.78l-0.47,0.62l0.08,0.56l2.04,1.23l0.04,1.4Z",name:"Armenia"},IT:{path:"M451.59,158.63l3.48,0.94l-0.21,1.17l0.3,0.83l-1.49,-0.24l-2.04,1.1l-0.21,0.39l0.13,1.45l-0.25,1.12l0.82,1.57l2.39,1.63l1.31,2.54l2.79,2.43l2.05,0.08l0.21,0.23l-0.39,0.33l0.09,0.67l4.05,1.97l2.17,1.76l-0.16,0.36l-1.17,-1.08l-2.18,-0.49l-0.44,0.2l-1.05,1.91l0.14,0.54l1.57,0.95l-0.19,0.98l-1.06,0.33l-1.25,2.34l-0.37,0.08l0.0,-0.33l1.0,-2.45l-1.73,-3.17l-1.12,-0.51l-0.88,-1.33l-1.51,-0.51l-1.27,-1.25l-1.75,-0.18l-4.12,-3.21l-1.62,-1.65l-1.03,-3.19l-3.53,-1.36l-1.3,0.51l-1.69,1.41l0.16,-0.72l-0.28,-0.47l-1.14,-0.33l-0.53,-1.96l0.72,-0.78l0.04,-0.48l-0.65,-1.17l0.8,0.39l1.4,-0.23l1.11,-0.84l0.52,0.35l1.19,-0.1l0.75,-1.2l1.53,0.33l1.36,-0.56l0.35,-1.14l1.08,0.32l0.68,-0.64l1.98,-0.44l0.42,0.82ZM459.19,184.75l-0.65,1.65l0.32,1.05l-0.31,0.89l-1.5,-0.85l-4.5,-1.67l0.19,-0.82l2.67,0.23l3.78,-0.48ZM443.93,176.05l1.18,1.66l-0.3,3.32l-1.06,-0.01l-0.77,0.73l-0.53,-0.44l-0.1,-3.37l-0.39,-1.22l1.04,0.01l0.92,-0.68Z",name:"Italy"},VN:{path:"M690.56,230.25l-2.7,1.82l-2.09,2.46l-0.63,1.95l4.31,6.45l2.32,1.65l1.43,1.94l1.11,4.59l-0.32,4.24l-1.93,1.54l-2.84,1.61l-2.11,2.15l-2.73,2.06l-0.59,-1.05l0.63,-1.53l-0.13,-0.47l-1.34,-1.04l1.51,-0.71l2.55,-0.18l0.3,-0.63l-0.82,-1.14l4.0,-2.07l0.31,-3.05l-0.57,-1.77l0.42,-2.66l-0.73,-1.97l-1.86,-1.76l-3.63,-5.29l-2.72,-1.46l0.36,-0.47l1.5,-0.64l0.21,-0.52l-0.97,-2.27l-0.37,-0.24l-2.83,-0.02l-2.24,-3.9l0.83,-0.4l4.39,-0.29l2.06,-1.31l1.15,0.89l1.88,0.4l-0.17,1.51l1.35,1.16l1.67,0.45Z",name:"Vietnam"},AR:{path:"M249.29,428.93l-2.33,-0.52l-5.83,-0.43l-0.89,-1.66l0.05,-2.37l-0.45,-0.4l-1.43,0.18l-0.67,-0.91l-0.2,-3.13l1.88,-1.47l0.79,-2.04l-0.25,-1.7l1.3,-2.68l0.91,-4.15l-0.22,-1.69l0.85,-0.45l0.2,-0.44l-0.27,-1.16l-0.98,-0.68l0.59,-0.92l-0.05,-0.5l-1.04,-1.07l-0.52,-3.1l0.97,-0.86l-0.42,-3.58l1.2,-5.43l1.38,-0.98l0.16,-0.43l-0.75,-2.79l-0.01,-2.43l1.78,-1.75l0.06,-2.57l1.43,-2.85l0.01,-2.58l-0.69,-0.74l-1.09,-4.52l1.47,-2.7l-0.18,-2.79l0.85,-2.35l1.59,-2.46l1.73,-1.64l0.05,-0.52l-0.6,-0.84l0.44,-0.85l-0.07,-4.19l2.7,-1.44l0.86,-2.75l-0.21,-0.71l1.76,-2.01l2.9,0.57l1.38,1.78l0.68,-0.08l0.87,-1.87l2.39,0.09l4.95,4.77l2.17,0.49l3.0,1.92l2.47,1.0l0.25,0.82l-2.37,3.93l0.23,0.59l5.39,1.16l2.12,-0.44l2.45,-2.16l0.5,-2.38l0.76,-0.31l0.98,1.2l-0.04,1.8l-3.67,2.51l-2.85,2.66l-3.43,3.88l-1.3,5.07l0.01,2.72l-0.54,0.73l-0.36,3.28l3.14,2.64l-0.16,2.11l1.4,1.11l-0.1,1.09l-2.29,3.52l-3.55,1.49l-4.92,0.6l-2.71,-0.29l-0.43,0.51l0.5,1.65l-0.49,2.1l0.38,1.42l-1.19,0.83l-2.36,0.38l-2.3,-1.04l-1.38,0.83l0.41,3.64l1.69,0.91l1.4,-0.71l0.36,0.76l-2.04,0.86l-2.01,1.89l-0.97,4.63l-2.34,0.1l-2.09,1.78l-0.61,2.75l2.46,2.31l2.17,0.63l-0.7,2.32l-2.83,1.73l-1.73,3.86l-2.17,1.22l-1.16,1.67l0.75,3.76l1.04,1.28ZM256.71,438.88l-2.0,0.15l-1.4,-1.22l-3.82,-0.1l-0.0,-5.83l1.6,3.05l3.26,2.07l3.08,0.78l-0.71,1.1Z",name:"Argentina"},AU:{path:"M705.8,353.26l0.26,0.04l0.17,-0.47l-0.48,-1.42l0.92,1.11l0.45,0.15l0.27,-0.39l-0.1,-1.56l-1.98,-3.63l1.09,-3.31l-0.24,-1.57l0.34,-0.62l0.38,1.06l0.43,-0.19l0.99,-1.7l1.91,-0.83l1.29,-1.15l1.81,-0.91l0.96,-0.17l0.92,0.26l1.92,-0.95l1.47,-0.28l1.03,-0.8l1.43,0.04l2.78,-0.84l1.36,-1.15l0.71,-1.45l1.41,-1.26l0.3,-2.58l1.27,-1.59l0.78,1.65l0.54,0.19l1.07,-0.51l0.15,-0.6l-0.73,-1.0l0.45,-0.71l0.78,0.39l0.58,-0.3l0.28,-1.82l1.87,-2.14l1.12,-0.39l0.28,-0.58l0.62,0.17l0.53,-0.73l1.87,-0.57l1.65,1.05l1.35,1.48l3.39,0.38l0.43,-0.54l-0.46,-1.23l1.05,-1.79l1.04,-0.61l0.14,-0.55l-0.25,-0.41l0.88,-1.17l1.31,-0.77l1.3,0.27l2.1,-0.48l0.31,-0.4l-0.05,-1.3l-0.92,-0.77l1.48,0.56l1.41,1.07l2.11,0.65l0.81,-0.2l1.4,0.7l1.69,-0.66l0.8,0.19l0.64,-0.33l0.71,0.77l-1.33,1.94l-0.71,0.07l-0.35,0.51l0.24,0.86l-1.52,2.35l0.12,1.05l2.15,1.65l1.97,0.85l3.04,2.36l1.97,0.65l0.55,0.88l2.72,0.85l1.84,-1.1l2.07,-5.97l-0.42,-3.59l0.3,-1.73l0.47,-0.87l-0.31,-0.68l1.09,-3.28l0.46,-0.47l0.4,0.71l0.16,1.51l0.65,0.52l0.16,1.04l0.85,1.21l0.12,2.38l0.9,2.0l0.57,0.18l1.3,-0.78l1.69,1.7l-0.2,1.08l0.53,2.2l0.39,1.3l0.68,0.48l0.6,1.95l-0.19,1.48l0.81,1.76l6.01,3.69l-0.11,0.76l1.38,1.58l0.95,2.77l0.58,0.22l0.72,-0.41l0.8,0.9l0.61,0.01l0.46,2.41l4.81,4.71l0.66,2.02l-0.07,3.31l1.14,2.2l-0.13,2.24l-1.1,3.68l0.03,1.64l-0.47,1.89l-1.05,2.4l-1.9,1.47l-1.72,3.51l-2.38,6.09l-0.24,2.82l-1.14,0.8l-2.85,0.15l-2.31,1.19l-2.51,2.25l-3.09,-1.57l0.3,-1.15l-0.54,-0.47l-1.5,0.63l-2.01,1.94l-7.12,-2.18l-1.48,-1.63l-1.14,-3.74l-1.45,-1.26l-1.81,-0.26l0.56,-1.18l-0.61,-2.1l-0.72,-0.1l-1.14,1.82l-0.9,0.21l0.63,-0.82l0.36,-1.55l0.92,-1.31l-0.13,-2.34l-0.7,-0.22l-2.0,2.34l-1.51,0.93l-0.94,2.01l-1.35,-0.81l-0.02,-1.52l-1.57,-2.04l-1.09,-0.88l0.24,-0.33l-0.14,-0.59l-3.21,-1.69l-1.83,-0.12l-2.54,-1.35l-4.58,0.28l-6.02,1.9l-2.53,-0.13l-2.62,1.41l-2.13,0.63l-1.49,2.6l-3.49,0.31l-2.29,-0.5l-3.48,0.43l-1.6,1.47l-0.81,-0.04l-2.37,1.63l-3.26,-0.1l-3.72,-2.21l0.04,-1.05l1.19,-0.46l0.49,-0.89l0.21,-2.97l-0.28,-1.64l-1.34,-2.86l-0.38,-1.47l0.05,-1.72l-0.95,-1.7l-0.18,-0.97l-1.01,-0.99l-0.29,-1.98l-1.13,-1.75ZM784.92,393.44l2.65,1.02l3.23,-0.96l1.09,0.14l0.15,3.06l-0.85,1.13l-0.17,1.63l-0.87,-0.24l-1.57,1.91l-1.68,-0.18l-1.4,-2.36l-0.37,-2.04l-1.39,-2.51l0.04,-0.8l1.15,0.18Z",name:"Australia"},IL:{path:"M507.76,203.05l0.4,-0.78l0.18,0.4l-0.33,1.03l0.52,0.44l0.68,-0.22l-0.86,3.6l-1.16,-3.32l0.59,-0.74l-0.03,-0.41ZM508.73,200.34l0.37,-1.02l0.64,0.0l0.52,-0.51l-0.49,1.53l-0.56,-0.24l-0.48,0.23Z",name:"Israel"},IN:{path:"M623.34,207.03l-1.24,1.04l-0.97,2.55l0.22,0.51l8.04,3.87l3.42,0.37l1.57,1.38l4.92,0.88l2.18,-0.04l0.38,-0.3l0.29,-1.24l-0.32,-1.64l0.14,-0.87l0.82,-0.31l0.45,2.48l2.28,1.02l1.77,-0.38l4.14,0.1l0.38,-0.36l0.18,-1.66l-0.5,-0.65l1.37,-0.29l2.25,-1.99l2.7,-1.62l1.93,0.62l1.8,-0.98l0.79,1.14l-0.68,0.91l0.26,0.63l2.42,0.36l0.09,0.47l-0.83,0.75l0.13,1.07l-1.52,-0.29l-3.24,1.86l-0.13,1.78l-1.32,2.14l-0.18,1.39l-0.93,1.82l-1.64,-0.5l-0.52,0.37l-0.09,2.63l-0.56,1.11l0.19,0.81l-0.53,0.27l-1.18,-3.73l-1.08,-0.27l-0.38,0.31l-0.24,1.0l-0.66,-0.66l0.54,-1.06l1.22,-0.34l1.15,-2.25l-0.24,-0.56l-1.57,-0.47l-4.34,-0.28l-0.18,-1.56l-0.35,-0.35l-1.11,-0.12l-1.91,-1.12l-0.56,0.17l-0.88,1.82l0.11,0.49l1.36,1.07l-1.09,0.69l-0.69,1.11l0.18,0.56l1.24,0.57l-0.32,1.54l0.85,1.94l0.36,2.01l-0.22,0.59l-4.58,0.52l-0.33,0.42l0.13,1.8l-1.17,1.36l-3.65,1.81l-2.79,3.03l-4.32,3.28l-0.18,1.27l-4.65,1.79l-0.77,2.16l0.64,5.3l-1.06,2.49l-0.01,3.94l-1.24,0.28l-1.14,1.93l0.39,0.84l-1.68,0.53l-1.04,1.83l-0.65,0.47l-2.06,-2.05l-2.1,-6.02l-2.2,-3.64l-1.05,-4.75l-2.29,-3.57l-1.76,-8.2l0.01,-3.11l-0.49,-2.53l-0.55,-0.29l-3.53,1.52l-1.53,-0.27l-2.86,-2.77l0.85,-0.67l0.08,-0.55l-0.74,-1.03l-2.67,-2.06l1.24,-1.32l5.34,0.01l0.39,-0.49l-0.5,-2.29l-1.42,-1.46l-0.27,-1.93l-1.43,-1.2l2.31,-2.37l3.05,0.06l2.62,-2.85l1.6,-2.81l2.4,-2.73l0.07,-2.04l1.97,-1.48l-0.02,-0.65l-1.93,-1.31l-0.82,-1.78l-0.8,-2.21l0.9,-0.89l3.59,0.65l2.92,-0.42l2.33,-2.19l2.31,2.85l-0.24,2.13l0.99,1.59l-0.05,0.82l-1.34,-0.28l-0.47,0.48l0.7,3.06l2.62,1.99l2.99,1.65Z",name:"India"},TZ:{path:"M495.56,296.42l2.8,-3.12l-0.02,-0.81l-0.64,-1.3l0.68,-0.52l0.14,-1.47l-0.76,-1.25l0.31,-0.11l2.26,0.03l-0.51,2.76l0.76,1.3l0.5,0.12l1.05,-0.53l1.19,-0.12l0.61,0.24l1.43,-0.62l0.1,-0.67l-0.71,-0.62l1.57,-1.7l8.65,4.86l0.32,1.53l3.34,2.33l-1.05,2.8l0.13,1.61l1.63,1.12l-0.6,1.76l-0.01,2.33l1.89,4.03l0.57,0.43l-1.46,1.08l-2.61,0.94l-1.43,-0.04l-1.06,0.77l-2.29,0.36l-2.87,-0.68l-0.83,0.07l-0.63,-0.75l-0.31,-2.78l-1.32,-1.35l-3.25,-0.77l-3.96,-1.58l-1.18,-2.41l-0.32,-1.75l-1.76,-1.49l0.42,-1.05l-0.44,-0.89l0.08,-0.96l-0.46,-0.58l0.06,-0.56Z",name:"Tanzania"},AZ:{path:"M539.29,175.73l1.33,0.32l1.94,-1.8l2.3,3.34l1.43,0.43l-1.26,0.15l-0.35,0.32l-0.8,3.14l-0.99,0.96l0.05,1.11l-1.26,-1.13l0.7,-1.18l-0.04,-0.47l-0.74,-0.86l-1.48,0.15l-2.34,1.71l-0.03,-1.27l-2.03,-1.35l0.47,-0.62l-0.08,-0.56l-1.03,-0.79l0.29,-0.43l-0.14,-0.58l-1.13,-0.86l1.89,0.68l1.69,0.06l0.37,-0.87l-0.81,-1.37l0.42,0.06l1.63,1.72ZM533.78,180.57l0.61,0.46l0.69,-0.0l0.59,1.15l-0.68,-0.15l-1.21,-1.45Z",name:"Azerbaijan"},IE:{path:"M405.08,135.42l0.35,2.06l-1.75,2.78l-4.22,1.88l-2.84,-0.4l1.73,-3.0l-1.18,-3.53l4.6,-3.74l0.32,1.15l-0.49,1.74l0.4,0.51l1.47,-0.04l1.6,0.6Z",name:"Ireland"},ID:{path:"M756.47,287.89l0.69,4.01l2.79,1.78l0.51,-0.1l2.04,-2.59l2.71,-1.43l2.05,-0.0l3.9,1.73l2.46,0.45l0.08,15.12l-1.75,-1.54l-2.54,-0.51l-0.88,0.71l-2.32,0.06l0.69,-1.33l1.45,-0.64l0.23,-0.46l-0.65,-2.74l-1.24,-2.21l-5.04,-2.29l-2.09,-0.23l-3.68,-2.27l-0.55,0.13l-0.65,1.07l-0.52,0.12l-0.55,-1.89l-1.21,-0.78l1.84,-0.62l1.72,0.05l0.39,-0.52l-0.21,-0.66l-0.38,-0.28l-3.45,-0.0l-1.13,-1.48l-2.1,-0.43l-0.52,-0.6l2.69,-0.48l1.28,-0.78l3.66,0.94l0.3,0.71ZM757.91,300.34l-0.62,0.82l-0.1,-0.8l0.59,-1.12l0.13,1.1ZM747.38,292.98l0.34,0.72l-1.22,-0.57l-4.68,-0.1l0.27,-0.62l2.78,-0.09l2.52,0.67ZM741.05,285.25l-0.67,-2.88l0.64,-2.01l0.41,0.86l1.21,0.18l0.16,0.7l-0.1,1.68l-0.84,-0.16l-0.46,0.3l-0.34,1.34ZM739.05,293.5l-0.5,0.44l-1.34,-0.36l-0.17,-0.37l1.73,-0.08l0.27,0.36ZM721.45,284.51l-0.19,1.97l2.24,2.23l0.54,0.02l1.27,-1.07l2.75,-0.5l-0.9,1.21l-2.11,0.93l-0.16,0.6l2.22,3.01l-0.3,1.07l1.36,1.74l-2.26,0.85l-0.28,-0.31l0.12,-1.19l-1.64,-1.34l0.17,-2.23l-0.56,-0.39l-1.67,0.76l-0.23,0.39l0.3,6.17l-1.1,0.25l-0.69,-0.47l0.64,-2.21l-0.39,-2.42l-0.39,-0.34l-0.8,-0.01l-0.58,-1.29l0.98,-1.6l0.35,-1.96l1.32,-3.87ZM728.59,296.27l0.38,0.49l-0.02,1.28l-0.88,0.49l-0.53,-0.47l1.04,-1.79ZM729.04,286.98l0.27,-0.05l-0.02,0.13l-0.24,-0.08ZM721.68,284.05l0.16,-0.32l1.89,-1.65l1.83,0.68l3.16,0.35l2.94,-0.1l2.39,-1.66l-1.73,2.13l-1.66,0.43l-2.41,-0.48l-4.17,0.13l-2.39,0.51ZM730.55,310.47l1.11,-1.93l2.03,-0.82l0.08,0.62l-1.45,1.67l-1.77,0.46ZM728.12,305.88l-0.1,0.38l-3.46,0.66l-2.91,-0.27l-0.0,-0.25l1.54,-0.41l1.66,0.73l1.67,-0.19l1.61,-0.65ZM722.9,310.24l-0.64,0.03l-2.26,-1.2l1.11,-0.24l1.78,1.41ZM716.26,305.77l0.88,0.51l1.28,-0.17l0.2,0.35l-4.65,0.73l0.39,-0.67l1.15,-0.02l0.75,-0.73ZM711.66,293.84l-0.38,-0.16l-2.54,1.01l-1.12,-1.44l-1.69,-0.13l-1.16,-0.75l-3.04,0.77l-1.1,-1.15l-3.31,-0.11l-0.35,-3.05l-1.35,-0.95l-1.11,-1.98l-0.33,-2.06l0.27,-2.14l0.9,-1.01l0.37,1.15l2.09,1.49l1.53,-0.48l1.82,0.08l1.38,-1.19l1.0,-0.18l2.28,0.67l2.26,-0.53l1.52,-3.64l1.01,-0.99l0.78,-2.57l4.1,0.3l-1.11,1.77l0.02,0.46l1.7,2.2l-0.23,1.39l2.07,1.71l-2.33,0.42l-0.88,1.9l0.1,2.05l-2.4,1.9l-0.06,2.45l-0.7,2.79ZM692.58,302.03l0.35,0.26l4.8,0.25l0.78,-0.97l4.17,1.09l1.13,1.68l3.69,0.45l2.13,1.04l-1.8,0.6l-2.77,-0.99l-4.8,-0.12l-5.24,-1.41l-1.84,-0.25l-1.11,0.3l-4.26,-0.97l-0.7,-1.14l-1.59,-0.13l1.18,-1.65l2.74,0.13l2.87,1.13l0.26,0.68ZM685.53,299.17l-2.22,0.04l-2.06,-2.03l-3.15,-2.01l-2.93,-3.51l-3.11,-5.33l-2.2,-2.12l-1.64,-4.06l-2.32,-1.69l-1.27,-2.07l-1.96,-1.5l-2.51,-2.65l-0.11,-0.66l4.81,0.53l2.15,2.38l3.31,2.74l2.35,2.66l2.7,0.17l1.95,1.59l1.54,2.17l1.59,0.95l-0.84,1.71l0.15,0.52l1.44,0.87l0.79,0.1l0.4,1.58l0.87,1.4l1.96,0.39l1.0,1.31l-0.6,3.01l-0.09,3.5Z",name:"Indonesia"},UA:{path:"M492.5,162.44l1.28,-2.49l1.82,0.19l0.66,-0.23l0.09,-0.71l-0.25,-0.75l-0.79,-0.72l-0.33,-1.21l-0.86,-0.62l-0.02,-1.19l-1.13,-0.86l-1.15,-0.19l-2.04,-1.0l-1.66,0.32l-0.66,0.47l-0.92,-0.0l-0.84,0.78l-2.48,0.7l-1.18,-0.71l-3.07,-0.36l-0.89,0.43l-0.24,-0.55l-1.11,-0.7l0.35,-0.93l1.26,-1.02l-0.54,-1.23l2.04,-2.43l1.4,-0.62l0.25,-1.19l-1.04,-2.39l0.83,-0.13l1.28,-0.84l1.8,-0.07l2.47,0.26l2.86,0.81l1.88,0.06l0.86,0.44l1.04,-0.41l0.77,0.66l2.18,-0.15l0.92,0.3l0.52,-0.34l0.15,-1.53l0.56,-0.54l2.85,-0.05l0.84,-0.72l3.04,-0.18l1.23,1.46l-0.48,0.77l0.21,1.03l0.36,0.32l1.8,0.14l0.93,2.08l3.18,1.15l1.94,-0.45l1.67,1.49l1.4,-0.03l3.35,0.96l0.02,0.54l-0.96,1.59l0.47,1.97l-0.26,0.7l-2.36,0.28l-1.29,0.89l-0.23,1.38l-1.83,0.27l-1.58,0.97l-2.41,0.21l-2.16,1.17l-0.21,0.38l0.34,2.26l1.23,0.75l2.13,-0.08l-0.14,0.31l-2.65,0.53l-3.23,1.69l-0.87,-0.39l0.42,-1.1l-0.25,-0.52l-2.21,-0.73l2.35,-1.06l0.12,-0.65l-0.93,-0.82l-3.62,-0.74l-0.13,-0.89l-0.46,-0.34l-2.61,0.59l-0.91,1.69l-1.71,2.04l-0.86,-0.4l-1.62,0.27Z",name:"Ukraine"},QA:{path:"M549.33,221.64l-0.76,-0.23l-0.14,-1.64l0.84,-1.29l0.47,0.52l0.04,1.34l-0.45,1.3Z",name:"Qatar"},MZ:{path:"M508.58,318.75l-0.34,-2.57l0.51,-2.05l3.55,0.63l2.5,-0.38l1.02,-0.76l1.49,0.01l2.74,-0.98l1.66,-1.2l0.5,9.24l0.41,1.23l-0.68,1.67l-0.93,1.71l-1.5,1.5l-5.16,2.28l-2.78,2.73l-1.02,0.53l-1.71,1.8l-0.98,0.57l-0.35,2.41l1.16,1.94l0.49,2.17l0.43,0.31l-0.06,2.06l-0.39,1.17l0.5,0.72l-0.25,0.73l-0.92,0.83l-5.12,2.39l-1.22,1.36l0.21,1.13l0.58,0.39l-0.11,0.72l-1.22,-0.01l-0.73,-2.97l0.42,-3.09l-1.78,-5.37l2.49,-2.81l0.69,-1.89l0.44,-0.43l0.28,-1.53l-0.39,-0.93l0.59,-3.65l-0.01,-3.26l-1.49,-1.16l-1.2,-0.22l-1.74,-1.17l-1.92,0.01l-0.29,-2.08l7.06,-1.96l1.28,1.09l0.89,-0.1l0.67,0.44l0.1,0.73l-0.51,1.29l0.19,1.81l1.75,1.83l0.65,-0.13l0.71,-1.65l1.17,-0.86l-0.26,-3.47l-1.05,-1.85l-1.04,-0.94Z",name:"Mozambique"}},height:440.70631074413296,width:900,projection:{type:"mill",centralMeridian:11.5}});
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 95
|
|
(module) {
|
|
|
|
const API_URL = 'https://maps.googleapis.com/maps/api/js'
|
|
const CALLBACK_NAME = '__googleMapsApiOnLoadCallback'
|
|
|
|
const optionsKeys = ['channel', 'client', 'key', 'language', 'region', 'v']
|
|
|
|
let promise = null
|
|
|
|
module.exports = function (options = {}) {
|
|
promise =
|
|
promise ||
|
|
new Promise(function (resolve, reject) {
|
|
// Reject the promise after a timeout
|
|
const timeoutId = setTimeout(function () {
|
|
window[CALLBACK_NAME] = function () {} // Set the on load callback to a no-op
|
|
reject(new Error('Could not load the Google Maps API'))
|
|
}, options.timeout || 10000)
|
|
|
|
// Hook up the on load callback
|
|
window[CALLBACK_NAME] = function () {
|
|
if (timeoutId !== null) {
|
|
clearTimeout(timeoutId)
|
|
}
|
|
resolve(window.google.maps)
|
|
delete window[CALLBACK_NAME]
|
|
}
|
|
|
|
// Prepare the `script` tag to be inserted into the page
|
|
const scriptElement = document.createElement('script')
|
|
const params = [`callback=${CALLBACK_NAME}`]
|
|
optionsKeys.forEach(function (key) {
|
|
if (options[key]) {
|
|
params.push(`${key}=${options[key]}`)
|
|
}
|
|
})
|
|
if (options.libraries && options.libraries.length) {
|
|
params.push(`libraries=${options.libraries.join(',')}`)
|
|
}
|
|
scriptElement.src = `${options.apiUrl || API_URL}?${params.join('&')}`
|
|
|
|
// Insert the `script` tag
|
|
document.body.appendChild(scriptElement)
|
|
})
|
|
return promise
|
|
}
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 201
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
|
|
* Masonry v4.2.2
|
|
* Cascading grid layout library
|
|
* https://masonry.desandro.com
|
|
* MIT License
|
|
* by David DeSandro
|
|
*/
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* jshint strict: false */ /*globals define, module, require */
|
|
if ( true ) {
|
|
// AMD
|
|
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
|
|
__webpack_require__(442),
|
|
__webpack_require__(928)
|
|
], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
|
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( window, function factory( Outlayer, getSize ) {
|
|
|
|
'use strict';
|
|
|
|
// -------------------------- masonryDefinition -------------------------- //
|
|
|
|
// create an Outlayer layout class
|
|
var Masonry = Outlayer.create('masonry');
|
|
// isFitWidth -> fitWidth
|
|
Masonry.compatOptions.fitWidth = 'isFitWidth';
|
|
|
|
var proto = Masonry.prototype;
|
|
|
|
proto._resetLayout = function() {
|
|
this.getSize();
|
|
this._getMeasurement( 'columnWidth', 'outerWidth' );
|
|
this._getMeasurement( 'gutter', 'outerWidth' );
|
|
this.measureColumns();
|
|
|
|
// reset column Y
|
|
this.colYs = [];
|
|
for ( var i=0; i < this.cols; i++ ) {
|
|
this.colYs.push( 0 );
|
|
}
|
|
|
|
this.maxY = 0;
|
|
this.horizontalColIndex = 0;
|
|
};
|
|
|
|
proto.measureColumns = function() {
|
|
this.getContainerWidth();
|
|
// if columnWidth is 0, default to outerWidth of first item
|
|
if ( !this.columnWidth ) {
|
|
var firstItem = this.items[0];
|
|
var firstItemElem = firstItem && firstItem.element;
|
|
// columnWidth fall back to item of first element
|
|
this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth ||
|
|
// if first elem has no width, default to size of container
|
|
this.containerWidth;
|
|
}
|
|
|
|
var columnWidth = this.columnWidth += this.gutter;
|
|
|
|
// calculate columns
|
|
var containerWidth = this.containerWidth + this.gutter;
|
|
var cols = containerWidth / columnWidth;
|
|
// fix rounding errors, typically with gutters
|
|
var excess = columnWidth - containerWidth % columnWidth;
|
|
// if overshoot is less than a pixel, round up, otherwise floor it
|
|
var mathMethod = excess && excess < 1 ? 'round' : 'floor';
|
|
cols = Math[ mathMethod ]( cols );
|
|
this.cols = Math.max( cols, 1 );
|
|
};
|
|
|
|
proto.getContainerWidth = function() {
|
|
// container is parent if fit width
|
|
var isFitWidth = this._getOption('fitWidth');
|
|
var container = isFitWidth ? this.element.parentNode : this.element;
|
|
// check that this.size and size are there
|
|
// IE8 triggers resize on body size change, so they might not be
|
|
var size = getSize( container );
|
|
this.containerWidth = size && size.innerWidth;
|
|
};
|
|
|
|
proto._getItemLayoutPosition = function( item ) {
|
|
item.getSize();
|
|
// how many columns does this brick span
|
|
var remainder = item.size.outerWidth % this.columnWidth;
|
|
var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil';
|
|
// round if off by 1 pixel, otherwise use ceil
|
|
var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth );
|
|
colSpan = Math.min( colSpan, this.cols );
|
|
// use horizontal or top column position
|
|
var colPosMethod = this.options.horizontalOrder ?
|
|
'_getHorizontalColPosition' : '_getTopColPosition';
|
|
var colPosition = this[ colPosMethod ]( colSpan, item );
|
|
// position the brick
|
|
var position = {
|
|
x: this.columnWidth * colPosition.col,
|
|
y: colPosition.y
|
|
};
|
|
// apply setHeight to necessary columns
|
|
var setHeight = colPosition.y + item.size.outerHeight;
|
|
var setMax = colSpan + colPosition.col;
|
|
for ( var i = colPosition.col; i < setMax; i++ ) {
|
|
this.colYs[i] = setHeight;
|
|
}
|
|
|
|
return position;
|
|
};
|
|
|
|
proto._getTopColPosition = function( colSpan ) {
|
|
var colGroup = this._getTopColGroup( colSpan );
|
|
// get the minimum Y value from the columns
|
|
var minimumY = Math.min.apply( Math, colGroup );
|
|
|
|
return {
|
|
col: colGroup.indexOf( minimumY ),
|
|
y: minimumY,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {Number} colSpan - number of columns the element spans
|
|
* @returns {Array} colGroup
|
|
*/
|
|
proto._getTopColGroup = function( colSpan ) {
|
|
if ( colSpan < 2 ) {
|
|
// if brick spans only one column, use all the column Ys
|
|
return this.colYs;
|
|
}
|
|
|
|
var colGroup = [];
|
|
// how many different places could this brick fit horizontally
|
|
var groupCount = this.cols + 1 - colSpan;
|
|
// for each group potential horizontal position
|
|
for ( var i = 0; i < groupCount; i++ ) {
|
|
colGroup[i] = this._getColGroupY( i, colSpan );
|
|
}
|
|
return colGroup;
|
|
};
|
|
|
|
proto._getColGroupY = function( col, colSpan ) {
|
|
if ( colSpan < 2 ) {
|
|
return this.colYs[ col ];
|
|
}
|
|
// make an array of colY values for that one group
|
|
var groupColYs = this.colYs.slice( col, col + colSpan );
|
|
// and get the max value of the array
|
|
return Math.max.apply( Math, groupColYs );
|
|
};
|
|
|
|
// get column position based on horizontal index. #873
|
|
proto._getHorizontalColPosition = function( colSpan, item ) {
|
|
var col = this.horizontalColIndex % this.cols;
|
|
var isOver = colSpan > 1 && col + colSpan > this.cols;
|
|
// shift to next row if item can't fit on current row
|
|
col = isOver ? 0 : col;
|
|
// don't let zero-size items take up space
|
|
var hasSize = item.size.outerWidth && item.size.outerHeight;
|
|
this.horizontalColIndex = hasSize ? col + colSpan : this.horizontalColIndex;
|
|
|
|
return {
|
|
col: col,
|
|
y: this._getColGroupY( col, colSpan ),
|
|
};
|
|
};
|
|
|
|
proto._manageStamp = function( stamp ) {
|
|
var stampSize = getSize( stamp );
|
|
var offset = this._getElementOffset( stamp );
|
|
// get the columns that this stamp affects
|
|
var isOriginLeft = this._getOption('originLeft');
|
|
var firstX = isOriginLeft ? offset.left : offset.right;
|
|
var lastX = firstX + stampSize.outerWidth;
|
|
var firstCol = Math.floor( firstX / this.columnWidth );
|
|
firstCol = Math.max( 0, firstCol );
|
|
var lastCol = Math.floor( lastX / this.columnWidth );
|
|
// lastCol should not go over if multiple of columnWidth #425
|
|
lastCol -= lastX % this.columnWidth ? 0 : 1;
|
|
lastCol = Math.min( this.cols - 1, lastCol );
|
|
// set colYs to bottom of the stamp
|
|
|
|
var isOriginTop = this._getOption('originTop');
|
|
var stampMaxY = ( isOriginTop ? offset.top : offset.bottom ) +
|
|
stampSize.outerHeight;
|
|
for ( var i = firstCol; i <= lastCol; i++ ) {
|
|
this.colYs[i] = Math.max( stampMaxY, this.colYs[i] );
|
|
}
|
|
};
|
|
|
|
proto._getContainerSize = function() {
|
|
this.maxY = Math.max.apply( Math, this.colYs );
|
|
var size = {
|
|
height: this.maxY
|
|
};
|
|
|
|
if ( this._getOption('fitWidth') ) {
|
|
size.width = this._getContainerFitWidth();
|
|
}
|
|
|
|
return size;
|
|
};
|
|
|
|
proto._getContainerFitWidth = function() {
|
|
var unusedCols = 0;
|
|
// count unused columns
|
|
var i = this.cols;
|
|
while ( --i ) {
|
|
if ( this.colYs[i] !== 0 ) {
|
|
break;
|
|
}
|
|
unusedCols++;
|
|
}
|
|
// fit container to columns that have been used
|
|
return ( this.cols - unusedCols ) * this.columnWidth - this.gutter;
|
|
};
|
|
|
|
proto.needsResizeLayout = function() {
|
|
var previousWidth = this.containerWidth;
|
|
this.getContainerWidth();
|
|
return previousWidth != this.containerWidth;
|
|
};
|
|
|
|
return Masonry;
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 396
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/**
|
|
* Outlayer Item
|
|
*/
|
|
|
|
( function( window, factory ) {
|
|
// universal module definition
|
|
/* jshint strict: false */ /* globals define, module, require */
|
|
if ( true ) {
|
|
// AMD - RequireJS
|
|
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
|
|
__webpack_require__(240),
|
|
__webpack_require__(928)
|
|
], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
|
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( window, function factory( EvEmitter, getSize ) {
|
|
'use strict';
|
|
|
|
// ----- helpers ----- //
|
|
|
|
function isEmptyObj( obj ) {
|
|
for ( var prop in obj ) {
|
|
return false;
|
|
}
|
|
prop = null;
|
|
return true;
|
|
}
|
|
|
|
// -------------------------- CSS3 support -------------------------- //
|
|
|
|
|
|
var docElemStyle = document.documentElement.style;
|
|
|
|
var transitionProperty = typeof docElemStyle.transition == 'string' ?
|
|
'transition' : 'WebkitTransition';
|
|
var transformProperty = typeof docElemStyle.transform == 'string' ?
|
|
'transform' : 'WebkitTransform';
|
|
|
|
var transitionEndEvent = {
|
|
WebkitTransition: 'webkitTransitionEnd',
|
|
transition: 'transitionend'
|
|
}[ transitionProperty ];
|
|
|
|
// cache all vendor properties that could have vendor prefix
|
|
var vendorProperties = {
|
|
transform: transformProperty,
|
|
transition: transitionProperty,
|
|
transitionDuration: transitionProperty + 'Duration',
|
|
transitionProperty: transitionProperty + 'Property',
|
|
transitionDelay: transitionProperty + 'Delay'
|
|
};
|
|
|
|
// -------------------------- Item -------------------------- //
|
|
|
|
function Item( element, layout ) {
|
|
if ( !element ) {
|
|
return;
|
|
}
|
|
|
|
this.element = element;
|
|
// parent layout class, i.e. Masonry, Isotope, or Packery
|
|
this.layout = layout;
|
|
this.position = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
|
|
this._create();
|
|
}
|
|
|
|
// inherit EvEmitter
|
|
var proto = Item.prototype = Object.create( EvEmitter.prototype );
|
|
proto.constructor = Item;
|
|
|
|
proto._create = function() {
|
|
// transition objects
|
|
this._transn = {
|
|
ingProperties: {},
|
|
clean: {},
|
|
onEnd: {}
|
|
};
|
|
|
|
this.css({
|
|
position: 'absolute'
|
|
});
|
|
};
|
|
|
|
// trigger specified handler for event type
|
|
proto.handleEvent = function( event ) {
|
|
var method = 'on' + event.type;
|
|
if ( this[ method ] ) {
|
|
this[ method ]( event );
|
|
}
|
|
};
|
|
|
|
proto.getSize = function() {
|
|
this.size = getSize( this.element );
|
|
};
|
|
|
|
/**
|
|
* apply CSS styles to element
|
|
* @param {Object} style
|
|
*/
|
|
proto.css = function( style ) {
|
|
var elemStyle = this.element.style;
|
|
|
|
for ( var prop in style ) {
|
|
// use vendor property if available
|
|
var supportedProp = vendorProperties[ prop ] || prop;
|
|
elemStyle[ supportedProp ] = style[ prop ];
|
|
}
|
|
};
|
|
|
|
// measure position, and sets it
|
|
proto.getPosition = function() {
|
|
var style = getComputedStyle( this.element );
|
|
var isOriginLeft = this.layout._getOption('originLeft');
|
|
var isOriginTop = this.layout._getOption('originTop');
|
|
var xValue = style[ isOriginLeft ? 'left' : 'right' ];
|
|
var yValue = style[ isOriginTop ? 'top' : 'bottom' ];
|
|
var x = parseFloat( xValue );
|
|
var y = parseFloat( yValue );
|
|
// convert percent to pixels
|
|
var layoutSize = this.layout.size;
|
|
if ( xValue.indexOf('%') != -1 ) {
|
|
x = ( x / 100 ) * layoutSize.width;
|
|
}
|
|
if ( yValue.indexOf('%') != -1 ) {
|
|
y = ( y / 100 ) * layoutSize.height;
|
|
}
|
|
// clean up 'auto' or other non-integer values
|
|
x = isNaN( x ) ? 0 : x;
|
|
y = isNaN( y ) ? 0 : y;
|
|
// remove padding from measurement
|
|
x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight;
|
|
y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom;
|
|
|
|
this.position.x = x;
|
|
this.position.y = y;
|
|
};
|
|
|
|
// set settled position, apply padding
|
|
proto.layoutPosition = function() {
|
|
var layoutSize = this.layout.size;
|
|
var style = {};
|
|
var isOriginLeft = this.layout._getOption('originLeft');
|
|
var isOriginTop = this.layout._getOption('originTop');
|
|
|
|
// x
|
|
var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight';
|
|
var xProperty = isOriginLeft ? 'left' : 'right';
|
|
var xResetProperty = isOriginLeft ? 'right' : 'left';
|
|
|
|
var x = this.position.x + layoutSize[ xPadding ];
|
|
// set in percentage or pixels
|
|
style[ xProperty ] = this.getXValue( x );
|
|
// reset other property
|
|
style[ xResetProperty ] = '';
|
|
|
|
// y
|
|
var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom';
|
|
var yProperty = isOriginTop ? 'top' : 'bottom';
|
|
var yResetProperty = isOriginTop ? 'bottom' : 'top';
|
|
|
|
var y = this.position.y + layoutSize[ yPadding ];
|
|
// set in percentage or pixels
|
|
style[ yProperty ] = this.getYValue( y );
|
|
// reset other property
|
|
style[ yResetProperty ] = '';
|
|
|
|
this.css( style );
|
|
this.emitEvent( 'layout', [ this ] );
|
|
};
|
|
|
|
proto.getXValue = function( x ) {
|
|
var isHorizontal = this.layout._getOption('horizontal');
|
|
return this.layout.options.percentPosition && !isHorizontal ?
|
|
( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px';
|
|
};
|
|
|
|
proto.getYValue = function( y ) {
|
|
var isHorizontal = this.layout._getOption('horizontal');
|
|
return this.layout.options.percentPosition && isHorizontal ?
|
|
( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px';
|
|
};
|
|
|
|
proto._transitionTo = function( x, y ) {
|
|
this.getPosition();
|
|
// get current x & y from top/left
|
|
var curX = this.position.x;
|
|
var curY = this.position.y;
|
|
|
|
var didNotMove = x == this.position.x && y == this.position.y;
|
|
|
|
// save end position
|
|
this.setPosition( x, y );
|
|
|
|
// if did not move and not transitioning, just go to layout
|
|
if ( didNotMove && !this.isTransitioning ) {
|
|
this.layoutPosition();
|
|
return;
|
|
}
|
|
|
|
var transX = x - curX;
|
|
var transY = y - curY;
|
|
var transitionStyle = {};
|
|
transitionStyle.transform = this.getTranslate( transX, transY );
|
|
|
|
this.transition({
|
|
to: transitionStyle,
|
|
onTransitionEnd: {
|
|
transform: this.layoutPosition
|
|
},
|
|
isCleaning: true
|
|
});
|
|
};
|
|
|
|
proto.getTranslate = function( x, y ) {
|
|
// flip cooridinates if origin on right or bottom
|
|
var isOriginLeft = this.layout._getOption('originLeft');
|
|
var isOriginTop = this.layout._getOption('originTop');
|
|
x = isOriginLeft ? x : -x;
|
|
y = isOriginTop ? y : -y;
|
|
return 'translate3d(' + x + 'px, ' + y + 'px, 0)';
|
|
};
|
|
|
|
// non transition + transform support
|
|
proto.goTo = function( x, y ) {
|
|
this.setPosition( x, y );
|
|
this.layoutPosition();
|
|
};
|
|
|
|
proto.moveTo = proto._transitionTo;
|
|
|
|
proto.setPosition = function( x, y ) {
|
|
this.position.x = parseFloat( x );
|
|
this.position.y = parseFloat( y );
|
|
};
|
|
|
|
// ----- transition ----- //
|
|
|
|
/**
|
|
* @param {Object} style - CSS
|
|
* @param {Function} onTransitionEnd
|
|
*/
|
|
|
|
// non transition, just trigger callback
|
|
proto._nonTransition = function( args ) {
|
|
this.css( args.to );
|
|
if ( args.isCleaning ) {
|
|
this._removeStyles( args.to );
|
|
}
|
|
for ( var prop in args.onTransitionEnd ) {
|
|
args.onTransitionEnd[ prop ].call( this );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* proper transition
|
|
* @param {Object} args - arguments
|
|
* @param {Object} to - style to transition to
|
|
* @param {Object} from - style to start transition from
|
|
* @param {Boolean} isCleaning - removes transition styles after transition
|
|
* @param {Function} onTransitionEnd - callback
|
|
*/
|
|
proto.transition = function( args ) {
|
|
// redirect to nonTransition if no transition duration
|
|
if ( !parseFloat( this.layout.options.transitionDuration ) ) {
|
|
this._nonTransition( args );
|
|
return;
|
|
}
|
|
|
|
var _transition = this._transn;
|
|
// keep track of onTransitionEnd callback by css property
|
|
for ( var prop in args.onTransitionEnd ) {
|
|
_transition.onEnd[ prop ] = args.onTransitionEnd[ prop ];
|
|
}
|
|
// keep track of properties that are transitioning
|
|
for ( prop in args.to ) {
|
|
_transition.ingProperties[ prop ] = true;
|
|
// keep track of properties to clean up when transition is done
|
|
if ( args.isCleaning ) {
|
|
_transition.clean[ prop ] = true;
|
|
}
|
|
}
|
|
|
|
// set from styles
|
|
if ( args.from ) {
|
|
this.css( args.from );
|
|
// force redraw. http://blog.alexmaccaw.com/css-transitions
|
|
var h = this.element.offsetHeight;
|
|
// hack for JSHint to hush about unused var
|
|
h = null;
|
|
}
|
|
// enable transition
|
|
this.enableTransition( args.to );
|
|
// set styles that are transitioning
|
|
this.css( args.to );
|
|
|
|
this.isTransitioning = true;
|
|
|
|
};
|
|
|
|
// dash before all cap letters, including first for
|
|
// WebkitTransform => -webkit-transform
|
|
function toDashedAll( str ) {
|
|
return str.replace( /([A-Z])/g, function( $1 ) {
|
|
return '-' + $1.toLowerCase();
|
|
});
|
|
}
|
|
|
|
var transitionProps = 'opacity,' + toDashedAll( transformProperty );
|
|
|
|
proto.enableTransition = function(/* style */) {
|
|
// HACK changing transitionProperty during a transition
|
|
// will cause transition to jump
|
|
if ( this.isTransitioning ) {
|
|
return;
|
|
}
|
|
|
|
// make `transition: foo, bar, baz` from style object
|
|
// HACK un-comment this when enableTransition can work
|
|
// while a transition is happening
|
|
// var transitionValues = [];
|
|
// for ( var prop in style ) {
|
|
// // dash-ify camelCased properties like WebkitTransition
|
|
// prop = vendorProperties[ prop ] || prop;
|
|
// transitionValues.push( toDashedAll( prop ) );
|
|
// }
|
|
// munge number to millisecond, to match stagger
|
|
var duration = this.layout.options.transitionDuration;
|
|
duration = typeof duration == 'number' ? duration + 'ms' : duration;
|
|
// enable transition styles
|
|
this.css({
|
|
transitionProperty: transitionProps,
|
|
transitionDuration: duration,
|
|
transitionDelay: this.staggerDelay || 0
|
|
});
|
|
// listen for transition end event
|
|
this.element.addEventListener( transitionEndEvent, this, false );
|
|
};
|
|
|
|
// ----- events ----- //
|
|
|
|
proto.onwebkitTransitionEnd = function( event ) {
|
|
this.ontransitionend( event );
|
|
};
|
|
|
|
proto.onotransitionend = function( event ) {
|
|
this.ontransitionend( event );
|
|
};
|
|
|
|
// properties that I munge to make my life easier
|
|
var dashedVendorProperties = {
|
|
'-webkit-transform': 'transform'
|
|
};
|
|
|
|
proto.ontransitionend = function( event ) {
|
|
// disregard bubbled events from children
|
|
if ( event.target !== this.element ) {
|
|
return;
|
|
}
|
|
var _transition = this._transn;
|
|
// get property name of transitioned property, convert to prefix-free
|
|
var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName;
|
|
|
|
// remove property that has completed transitioning
|
|
delete _transition.ingProperties[ propertyName ];
|
|
// check if any properties are still transitioning
|
|
if ( isEmptyObj( _transition.ingProperties ) ) {
|
|
// all properties have completed transitioning
|
|
this.disableTransition();
|
|
}
|
|
// clean style
|
|
if ( propertyName in _transition.clean ) {
|
|
// clean up style
|
|
this.element.style[ event.propertyName ] = '';
|
|
delete _transition.clean[ propertyName ];
|
|
}
|
|
// trigger onTransitionEnd callback
|
|
if ( propertyName in _transition.onEnd ) {
|
|
var onTransitionEnd = _transition.onEnd[ propertyName ];
|
|
onTransitionEnd.call( this );
|
|
delete _transition.onEnd[ propertyName ];
|
|
}
|
|
|
|
this.emitEvent( 'transitionEnd', [ this ] );
|
|
};
|
|
|
|
proto.disableTransition = function() {
|
|
this.removeTransitionStyles();
|
|
this.element.removeEventListener( transitionEndEvent, this, false );
|
|
this.isTransitioning = false;
|
|
};
|
|
|
|
/**
|
|
* removes style property from element
|
|
* @param {Object} style
|
|
**/
|
|
proto._removeStyles = function( style ) {
|
|
// clean up transition styles
|
|
var cleanStyle = {};
|
|
for ( var prop in style ) {
|
|
cleanStyle[ prop ] = '';
|
|
}
|
|
this.css( cleanStyle );
|
|
};
|
|
|
|
var cleanTransitionStyle = {
|
|
transitionProperty: '',
|
|
transitionDuration: '',
|
|
transitionDelay: ''
|
|
};
|
|
|
|
proto.removeTransitionStyles = function() {
|
|
// remove transition
|
|
this.css( cleanTransitionStyle );
|
|
};
|
|
|
|
// ----- stagger ----- //
|
|
|
|
proto.stagger = function( delay ) {
|
|
delay = isNaN( delay ) ? 0 : delay;
|
|
this.staggerDelay = delay + 'ms';
|
|
};
|
|
|
|
// ----- show/hide/remove ----- //
|
|
|
|
// remove element from DOM
|
|
proto.removeElem = function() {
|
|
this.element.parentNode.removeChild( this.element );
|
|
// remove display: none
|
|
this.css({ display: '' });
|
|
this.emitEvent( 'remove', [ this ] );
|
|
};
|
|
|
|
proto.remove = function() {
|
|
// just remove element if no transition support or no transition
|
|
if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) {
|
|
this.removeElem();
|
|
return;
|
|
}
|
|
|
|
// start transition
|
|
this.once( 'transitionEnd', function() {
|
|
this.removeElem();
|
|
});
|
|
this.hide();
|
|
};
|
|
|
|
proto.reveal = function() {
|
|
delete this.isHidden;
|
|
// remove display: none
|
|
this.css({ display: '' });
|
|
|
|
var options = this.layout.options;
|
|
|
|
var onTransitionEnd = {};
|
|
var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle');
|
|
onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd;
|
|
|
|
this.transition({
|
|
from: options.hiddenStyle,
|
|
to: options.visibleStyle,
|
|
isCleaning: true,
|
|
onTransitionEnd: onTransitionEnd
|
|
});
|
|
};
|
|
|
|
proto.onRevealTransitionEnd = function() {
|
|
// check if still visible
|
|
// during transition, item may have been hidden
|
|
if ( !this.isHidden ) {
|
|
this.emitEvent('reveal');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* get style property use for hide/reveal transition end
|
|
* @param {String} styleProperty - hiddenStyle/visibleStyle
|
|
* @returns {String}
|
|
*/
|
|
proto.getHideRevealTransitionEndProperty = function( styleProperty ) {
|
|
var optionStyle = this.layout.options[ styleProperty ];
|
|
// use opacity
|
|
if ( optionStyle.opacity ) {
|
|
return 'opacity';
|
|
}
|
|
// get first property
|
|
for ( var prop in optionStyle ) {
|
|
return prop;
|
|
}
|
|
};
|
|
|
|
proto.hide = function() {
|
|
// set flag
|
|
this.isHidden = true;
|
|
// remove display: none
|
|
this.css({ display: '' });
|
|
|
|
var options = this.layout.options;
|
|
|
|
var onTransitionEnd = {};
|
|
var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle');
|
|
onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd;
|
|
|
|
this.transition({
|
|
from: options.visibleStyle,
|
|
to: options.hiddenStyle,
|
|
// keep hidden stuff hidden
|
|
isCleaning: true,
|
|
onTransitionEnd: onTransitionEnd
|
|
});
|
|
};
|
|
|
|
proto.onHideTransitionEnd = function() {
|
|
// check if still hidden
|
|
// during transition, item may have been un-hidden
|
|
if ( this.isHidden ) {
|
|
this.css({ display: 'none' });
|
|
this.emitEvent('hide');
|
|
}
|
|
};
|
|
|
|
proto.destroy = function() {
|
|
this.css({
|
|
position: '',
|
|
left: '',
|
|
right: '',
|
|
top: '',
|
|
bottom: '',
|
|
transition: '',
|
|
transform: ''
|
|
});
|
|
};
|
|
|
|
return Item;
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 442
|
|
(module, exports, __webpack_require__) {
|
|
|
|
var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*!
|
|
* Outlayer v2.1.1
|
|
* the brains and guts of a layout library
|
|
* MIT license
|
|
*/
|
|
|
|
( function( window, factory ) {
|
|
'use strict';
|
|
// universal module definition
|
|
/* jshint strict: false */ /* globals define, module, require */
|
|
if ( true ) {
|
|
// AMD - RequireJS
|
|
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [
|
|
__webpack_require__(240),
|
|
__webpack_require__(928),
|
|
__webpack_require__(346),
|
|
__webpack_require__(396)
|
|
], __WEBPACK_AMD_DEFINE_RESULT__ = (function( EvEmitter, getSize, utils, Item ) {
|
|
return factory( window, EvEmitter, getSize, utils, Item);
|
|
}).apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
|
|
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
|
} else // removed by dead control flow
|
|
{}
|
|
|
|
}( window, function factory( window, EvEmitter, getSize, utils, Item ) {
|
|
'use strict';
|
|
|
|
// ----- vars ----- //
|
|
|
|
var console = window.console;
|
|
var jQuery = window.jQuery;
|
|
var noop = function() {};
|
|
|
|
// -------------------------- Outlayer -------------------------- //
|
|
|
|
// globally unique identifiers
|
|
var GUID = 0;
|
|
// internal store of all Outlayer intances
|
|
var instances = {};
|
|
|
|
|
|
/**
|
|
* @param {Element, String} element
|
|
* @param {Object} options
|
|
* @constructor
|
|
*/
|
|
function Outlayer( element, options ) {
|
|
var queryElement = utils.getQueryElement( element );
|
|
if ( !queryElement ) {
|
|
if ( console ) {
|
|
console.error( 'Bad element for ' + this.constructor.namespace +
|
|
': ' + ( queryElement || element ) );
|
|
}
|
|
return;
|
|
}
|
|
this.element = queryElement;
|
|
// add jQuery
|
|
if ( jQuery ) {
|
|
this.$element = jQuery( this.element );
|
|
}
|
|
|
|
// options
|
|
this.options = utils.extend( {}, this.constructor.defaults );
|
|
this.option( options );
|
|
|
|
// add id for Outlayer.getFromElement
|
|
var id = ++GUID;
|
|
this.element.outlayerGUID = id; // expando
|
|
instances[ id ] = this; // associate via id
|
|
|
|
// kick it off
|
|
this._create();
|
|
|
|
var isInitLayout = this._getOption('initLayout');
|
|
if ( isInitLayout ) {
|
|
this.layout();
|
|
}
|
|
}
|
|
|
|
// settings are for internal use only
|
|
Outlayer.namespace = 'outlayer';
|
|
Outlayer.Item = Item;
|
|
|
|
// default options
|
|
Outlayer.defaults = {
|
|
containerStyle: {
|
|
position: 'relative'
|
|
},
|
|
initLayout: true,
|
|
originLeft: true,
|
|
originTop: true,
|
|
resize: true,
|
|
resizeContainer: true,
|
|
// item options
|
|
transitionDuration: '0.4s',
|
|
hiddenStyle: {
|
|
opacity: 0,
|
|
transform: 'scale(0.001)'
|
|
},
|
|
visibleStyle: {
|
|
opacity: 1,
|
|
transform: 'scale(1)'
|
|
}
|
|
};
|
|
|
|
var proto = Outlayer.prototype;
|
|
// inherit EvEmitter
|
|
utils.extend( proto, EvEmitter.prototype );
|
|
|
|
/**
|
|
* set options
|
|
* @param {Object} opts
|
|
*/
|
|
proto.option = function( opts ) {
|
|
utils.extend( this.options, opts );
|
|
};
|
|
|
|
/**
|
|
* get backwards compatible option value, check old name
|
|
*/
|
|
proto._getOption = function( option ) {
|
|
var oldOption = this.constructor.compatOptions[ option ];
|
|
return oldOption && this.options[ oldOption ] !== undefined ?
|
|
this.options[ oldOption ] : this.options[ option ];
|
|
};
|
|
|
|
Outlayer.compatOptions = {
|
|
// currentName: oldName
|
|
initLayout: 'isInitLayout',
|
|
horizontal: 'isHorizontal',
|
|
layoutInstant: 'isLayoutInstant',
|
|
originLeft: 'isOriginLeft',
|
|
originTop: 'isOriginTop',
|
|
resize: 'isResizeBound',
|
|
resizeContainer: 'isResizingContainer'
|
|
};
|
|
|
|
proto._create = function() {
|
|
// get items from children
|
|
this.reloadItems();
|
|
// elements that affect layout, but are not laid out
|
|
this.stamps = [];
|
|
this.stamp( this.options.stamp );
|
|
// set container style
|
|
utils.extend( this.element.style, this.options.containerStyle );
|
|
|
|
// bind resize method
|
|
var canBindResize = this._getOption('resize');
|
|
if ( canBindResize ) {
|
|
this.bindResize();
|
|
}
|
|
};
|
|
|
|
// goes through all children again and gets bricks in proper order
|
|
proto.reloadItems = function() {
|
|
// collection of item elements
|
|
this.items = this._itemize( this.element.children );
|
|
};
|
|
|
|
|
|
/**
|
|
* turn elements into Outlayer.Items to be used in layout
|
|
* @param {Array or NodeList or HTMLElement} elems
|
|
* @returns {Array} items - collection of new Outlayer Items
|
|
*/
|
|
proto._itemize = function( elems ) {
|
|
|
|
var itemElems = this._filterFindItemElements( elems );
|
|
var Item = this.constructor.Item;
|
|
|
|
// create new Outlayer Items for collection
|
|
var items = [];
|
|
for ( var i=0; i < itemElems.length; i++ ) {
|
|
var elem = itemElems[i];
|
|
var item = new Item( elem, this );
|
|
items.push( item );
|
|
}
|
|
|
|
return items;
|
|
};
|
|
|
|
/**
|
|
* get item elements to be used in layout
|
|
* @param {Array or NodeList or HTMLElement} elems
|
|
* @returns {Array} items - item elements
|
|
*/
|
|
proto._filterFindItemElements = function( elems ) {
|
|
return utils.filterFindElements( elems, this.options.itemSelector );
|
|
};
|
|
|
|
/**
|
|
* getter method for getting item elements
|
|
* @returns {Array} elems - collection of item elements
|
|
*/
|
|
proto.getItemElements = function() {
|
|
return this.items.map( function( item ) {
|
|
return item.element;
|
|
});
|
|
};
|
|
|
|
// ----- init & layout ----- //
|
|
|
|
/**
|
|
* lays out all items
|
|
*/
|
|
proto.layout = function() {
|
|
this._resetLayout();
|
|
this._manageStamps();
|
|
|
|
// don't animate first layout
|
|
var layoutInstant = this._getOption('layoutInstant');
|
|
var isInstant = layoutInstant !== undefined ?
|
|
layoutInstant : !this._isLayoutInited;
|
|
this.layoutItems( this.items, isInstant );
|
|
|
|
// flag for initalized
|
|
this._isLayoutInited = true;
|
|
};
|
|
|
|
// _init is alias for layout
|
|
proto._init = proto.layout;
|
|
|
|
/**
|
|
* logic before any new layout
|
|
*/
|
|
proto._resetLayout = function() {
|
|
this.getSize();
|
|
};
|
|
|
|
|
|
proto.getSize = function() {
|
|
this.size = getSize( this.element );
|
|
};
|
|
|
|
/**
|
|
* get measurement from option, for columnWidth, rowHeight, gutter
|
|
* if option is String -> get element from selector string, & get size of element
|
|
* if option is Element -> get size of element
|
|
* else use option as a number
|
|
*
|
|
* @param {String} measurement
|
|
* @param {String} size - width or height
|
|
* @private
|
|
*/
|
|
proto._getMeasurement = function( measurement, size ) {
|
|
var option = this.options[ measurement ];
|
|
var elem;
|
|
if ( !option ) {
|
|
// default to 0
|
|
this[ measurement ] = 0;
|
|
} else {
|
|
// use option as an element
|
|
if ( typeof option == 'string' ) {
|
|
elem = this.element.querySelector( option );
|
|
} else if ( option instanceof HTMLElement ) {
|
|
elem = option;
|
|
}
|
|
// use size of element, if element
|
|
this[ measurement ] = elem ? getSize( elem )[ size ] : option;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* layout a collection of item elements
|
|
* @api public
|
|
*/
|
|
proto.layoutItems = function( items, isInstant ) {
|
|
items = this._getItemsForLayout( items );
|
|
|
|
this._layoutItems( items, isInstant );
|
|
|
|
this._postLayout();
|
|
};
|
|
|
|
/**
|
|
* get the items to be laid out
|
|
* you may want to skip over some items
|
|
* @param {Array} items
|
|
* @returns {Array} items
|
|
*/
|
|
proto._getItemsForLayout = function( items ) {
|
|
return items.filter( function( item ) {
|
|
return !item.isIgnored;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* layout items
|
|
* @param {Array} items
|
|
* @param {Boolean} isInstant
|
|
*/
|
|
proto._layoutItems = function( items, isInstant ) {
|
|
this._emitCompleteOnItems( 'layout', items );
|
|
|
|
if ( !items || !items.length ) {
|
|
// no items, emit event with empty array
|
|
return;
|
|
}
|
|
|
|
var queue = [];
|
|
|
|
items.forEach( function( item ) {
|
|
// get x/y object from method
|
|
var position = this._getItemLayoutPosition( item );
|
|
// enqueue
|
|
position.item = item;
|
|
position.isInstant = isInstant || item.isLayoutInstant;
|
|
queue.push( position );
|
|
}, this );
|
|
|
|
this._processLayoutQueue( queue );
|
|
};
|
|
|
|
/**
|
|
* get item layout position
|
|
* @param {Outlayer.Item} item
|
|
* @returns {Object} x and y position
|
|
*/
|
|
proto._getItemLayoutPosition = function( /* item */ ) {
|
|
return {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
};
|
|
|
|
/**
|
|
* iterate over array and position each item
|
|
* Reason being - separating this logic prevents 'layout invalidation'
|
|
* thx @paul_irish
|
|
* @param {Array} queue
|
|
*/
|
|
proto._processLayoutQueue = function( queue ) {
|
|
this.updateStagger();
|
|
queue.forEach( function( obj, i ) {
|
|
this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i );
|
|
}, this );
|
|
};
|
|
|
|
// set stagger from option in milliseconds number
|
|
proto.updateStagger = function() {
|
|
var stagger = this.options.stagger;
|
|
if ( stagger === null || stagger === undefined ) {
|
|
this.stagger = 0;
|
|
return;
|
|
}
|
|
this.stagger = getMilliseconds( stagger );
|
|
return this.stagger;
|
|
};
|
|
|
|
/**
|
|
* Sets position of item in DOM
|
|
* @param {Outlayer.Item} item
|
|
* @param {Number} x - horizontal position
|
|
* @param {Number} y - vertical position
|
|
* @param {Boolean} isInstant - disables transitions
|
|
*/
|
|
proto._positionItem = function( item, x, y, isInstant, i ) {
|
|
if ( isInstant ) {
|
|
// if not transition, just set CSS
|
|
item.goTo( x, y );
|
|
} else {
|
|
item.stagger( i * this.stagger );
|
|
item.moveTo( x, y );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Any logic you want to do after each layout,
|
|
* i.e. size the container
|
|
*/
|
|
proto._postLayout = function() {
|
|
this.resizeContainer();
|
|
};
|
|
|
|
proto.resizeContainer = function() {
|
|
var isResizingContainer = this._getOption('resizeContainer');
|
|
if ( !isResizingContainer ) {
|
|
return;
|
|
}
|
|
var size = this._getContainerSize();
|
|
if ( size ) {
|
|
this._setContainerMeasure( size.width, true );
|
|
this._setContainerMeasure( size.height, false );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets width or height of container if returned
|
|
* @returns {Object} size
|
|
* @param {Number} width
|
|
* @param {Number} height
|
|
*/
|
|
proto._getContainerSize = noop;
|
|
|
|
/**
|
|
* @param {Number} measure - size of width or height
|
|
* @param {Boolean} isWidth
|
|
*/
|
|
proto._setContainerMeasure = function( measure, isWidth ) {
|
|
if ( measure === undefined ) {
|
|
return;
|
|
}
|
|
|
|
var elemSize = this.size;
|
|
// add padding and border width if border box
|
|
if ( elemSize.isBorderBox ) {
|
|
measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight +
|
|
elemSize.borderLeftWidth + elemSize.borderRightWidth :
|
|
elemSize.paddingBottom + elemSize.paddingTop +
|
|
elemSize.borderTopWidth + elemSize.borderBottomWidth;
|
|
}
|
|
|
|
measure = Math.max( measure, 0 );
|
|
this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px';
|
|
};
|
|
|
|
/**
|
|
* emit eventComplete on a collection of items events
|
|
* @param {String} eventName
|
|
* @param {Array} items - Outlayer.Items
|
|
*/
|
|
proto._emitCompleteOnItems = function( eventName, items ) {
|
|
var _this = this;
|
|
function onComplete() {
|
|
_this.dispatchEvent( eventName + 'Complete', null, [ items ] );
|
|
}
|
|
|
|
var count = items.length;
|
|
if ( !items || !count ) {
|
|
onComplete();
|
|
return;
|
|
}
|
|
|
|
var doneCount = 0;
|
|
function tick() {
|
|
doneCount++;
|
|
if ( doneCount == count ) {
|
|
onComplete();
|
|
}
|
|
}
|
|
|
|
// bind callback
|
|
items.forEach( function( item ) {
|
|
item.once( eventName, tick );
|
|
});
|
|
};
|
|
|
|
/**
|
|
* emits events via EvEmitter and jQuery events
|
|
* @param {String} type - name of event
|
|
* @param {Event} event - original event
|
|
* @param {Array} args - extra arguments
|
|
*/
|
|
proto.dispatchEvent = function( type, event, args ) {
|
|
// add original event to arguments
|
|
var emitArgs = event ? [ event ].concat( args ) : args;
|
|
this.emitEvent( type, emitArgs );
|
|
|
|
if ( jQuery ) {
|
|
// set this.$element
|
|
this.$element = this.$element || jQuery( this.element );
|
|
if ( event ) {
|
|
// create jQuery event
|
|
var $event = jQuery.Event( event );
|
|
$event.type = type;
|
|
this.$element.trigger( $event, args );
|
|
} else {
|
|
// just trigger with type if no event available
|
|
this.$element.trigger( type, args );
|
|
}
|
|
}
|
|
};
|
|
|
|
// -------------------------- ignore & stamps -------------------------- //
|
|
|
|
|
|
/**
|
|
* keep item in collection, but do not lay it out
|
|
* ignored items do not get skipped in layout
|
|
* @param {Element} elem
|
|
*/
|
|
proto.ignore = function( elem ) {
|
|
var item = this.getItem( elem );
|
|
if ( item ) {
|
|
item.isIgnored = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* return item to layout collection
|
|
* @param {Element} elem
|
|
*/
|
|
proto.unignore = function( elem ) {
|
|
var item = this.getItem( elem );
|
|
if ( item ) {
|
|
delete item.isIgnored;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* adds elements to stamps
|
|
* @param {NodeList, Array, Element, or String} elems
|
|
*/
|
|
proto.stamp = function( elems ) {
|
|
elems = this._find( elems );
|
|
if ( !elems ) {
|
|
return;
|
|
}
|
|
|
|
this.stamps = this.stamps.concat( elems );
|
|
// ignore
|
|
elems.forEach( this.ignore, this );
|
|
};
|
|
|
|
/**
|
|
* removes elements to stamps
|
|
* @param {NodeList, Array, or Element} elems
|
|
*/
|
|
proto.unstamp = function( elems ) {
|
|
elems = this._find( elems );
|
|
if ( !elems ){
|
|
return;
|
|
}
|
|
|
|
elems.forEach( function( elem ) {
|
|
// filter out removed stamp elements
|
|
utils.removeFrom( this.stamps, elem );
|
|
this.unignore( elem );
|
|
}, this );
|
|
};
|
|
|
|
/**
|
|
* finds child elements
|
|
* @param {NodeList, Array, Element, or String} elems
|
|
* @returns {Array} elems
|
|
*/
|
|
proto._find = function( elems ) {
|
|
if ( !elems ) {
|
|
return;
|
|
}
|
|
// if string, use argument as selector string
|
|
if ( typeof elems == 'string' ) {
|
|
elems = this.element.querySelectorAll( elems );
|
|
}
|
|
elems = utils.makeArray( elems );
|
|
return elems;
|
|
};
|
|
|
|
proto._manageStamps = function() {
|
|
if ( !this.stamps || !this.stamps.length ) {
|
|
return;
|
|
}
|
|
|
|
this._getBoundingRect();
|
|
|
|
this.stamps.forEach( this._manageStamp, this );
|
|
};
|
|
|
|
// update boundingLeft / Top
|
|
proto._getBoundingRect = function() {
|
|
// get bounding rect for container element
|
|
var boundingRect = this.element.getBoundingClientRect();
|
|
var size = this.size;
|
|
this._boundingRect = {
|
|
left: boundingRect.left + size.paddingLeft + size.borderLeftWidth,
|
|
top: boundingRect.top + size.paddingTop + size.borderTopWidth,
|
|
right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ),
|
|
bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth )
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @param {Element} stamp
|
|
**/
|
|
proto._manageStamp = noop;
|
|
|
|
/**
|
|
* get x/y position of element relative to container element
|
|
* @param {Element} elem
|
|
* @returns {Object} offset - has left, top, right, bottom
|
|
*/
|
|
proto._getElementOffset = function( elem ) {
|
|
var boundingRect = elem.getBoundingClientRect();
|
|
var thisRect = this._boundingRect;
|
|
var size = getSize( elem );
|
|
var offset = {
|
|
left: boundingRect.left - thisRect.left - size.marginLeft,
|
|
top: boundingRect.top - thisRect.top - size.marginTop,
|
|
right: thisRect.right - boundingRect.right - size.marginRight,
|
|
bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom
|
|
};
|
|
return offset;
|
|
};
|
|
|
|
// -------------------------- resize -------------------------- //
|
|
|
|
// enable event handlers for listeners
|
|
// i.e. resize -> onresize
|
|
proto.handleEvent = utils.handleEvent;
|
|
|
|
/**
|
|
* Bind layout to window resizing
|
|
*/
|
|
proto.bindResize = function() {
|
|
window.addEventListener( 'resize', this );
|
|
this.isResizeBound = true;
|
|
};
|
|
|
|
/**
|
|
* Unbind layout to window resizing
|
|
*/
|
|
proto.unbindResize = function() {
|
|
window.removeEventListener( 'resize', this );
|
|
this.isResizeBound = false;
|
|
};
|
|
|
|
proto.onresize = function() {
|
|
this.resize();
|
|
};
|
|
|
|
utils.debounceMethod( Outlayer, 'onresize', 100 );
|
|
|
|
proto.resize = function() {
|
|
// don't trigger if size did not change
|
|
// or if resize was unbound. See #9
|
|
if ( !this.isResizeBound || !this.needsResizeLayout() ) {
|
|
return;
|
|
}
|
|
|
|
this.layout();
|
|
};
|
|
|
|
/**
|
|
* check if layout is needed post layout
|
|
* @returns Boolean
|
|
*/
|
|
proto.needsResizeLayout = function() {
|
|
var size = getSize( this.element );
|
|
// check that this.size and size are there
|
|
// IE8 triggers resize on body size change, so they might not be
|
|
var hasSizes = this.size && size;
|
|
return hasSizes && size.innerWidth !== this.size.innerWidth;
|
|
};
|
|
|
|
// -------------------------- methods -------------------------- //
|
|
|
|
/**
|
|
* add items to Outlayer instance
|
|
* @param {Array or NodeList or Element} elems
|
|
* @returns {Array} items - Outlayer.Items
|
|
**/
|
|
proto.addItems = function( elems ) {
|
|
var items = this._itemize( elems );
|
|
// add items to collection
|
|
if ( items.length ) {
|
|
this.items = this.items.concat( items );
|
|
}
|
|
return items;
|
|
};
|
|
|
|
/**
|
|
* Layout newly-appended item elements
|
|
* @param {Array or NodeList or Element} elems
|
|
*/
|
|
proto.appended = function( elems ) {
|
|
var items = this.addItems( elems );
|
|
if ( !items.length ) {
|
|
return;
|
|
}
|
|
// layout and reveal just the new items
|
|
this.layoutItems( items, true );
|
|
this.reveal( items );
|
|
};
|
|
|
|
/**
|
|
* Layout prepended elements
|
|
* @param {Array or NodeList or Element} elems
|
|
*/
|
|
proto.prepended = function( elems ) {
|
|
var items = this._itemize( elems );
|
|
if ( !items.length ) {
|
|
return;
|
|
}
|
|
// add items to beginning of collection
|
|
var previousItems = this.items.slice(0);
|
|
this.items = items.concat( previousItems );
|
|
// start new layout
|
|
this._resetLayout();
|
|
this._manageStamps();
|
|
// layout new stuff without transition
|
|
this.layoutItems( items, true );
|
|
this.reveal( items );
|
|
// layout previous items
|
|
this.layoutItems( previousItems );
|
|
};
|
|
|
|
/**
|
|
* reveal a collection of items
|
|
* @param {Array of Outlayer.Items} items
|
|
*/
|
|
proto.reveal = function( items ) {
|
|
this._emitCompleteOnItems( 'reveal', items );
|
|
if ( !items || !items.length ) {
|
|
return;
|
|
}
|
|
var stagger = this.updateStagger();
|
|
items.forEach( function( item, i ) {
|
|
item.stagger( i * stagger );
|
|
item.reveal();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* hide a collection of items
|
|
* @param {Array of Outlayer.Items} items
|
|
*/
|
|
proto.hide = function( items ) {
|
|
this._emitCompleteOnItems( 'hide', items );
|
|
if ( !items || !items.length ) {
|
|
return;
|
|
}
|
|
var stagger = this.updateStagger();
|
|
items.forEach( function( item, i ) {
|
|
item.stagger( i * stagger );
|
|
item.hide();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* reveal item elements
|
|
* @param {Array}, {Element}, {NodeList} items
|
|
*/
|
|
proto.revealItemElements = function( elems ) {
|
|
var items = this.getItems( elems );
|
|
this.reveal( items );
|
|
};
|
|
|
|
/**
|
|
* hide item elements
|
|
* @param {Array}, {Element}, {NodeList} items
|
|
*/
|
|
proto.hideItemElements = function( elems ) {
|
|
var items = this.getItems( elems );
|
|
this.hide( items );
|
|
};
|
|
|
|
/**
|
|
* get Outlayer.Item, given an Element
|
|
* @param {Element} elem
|
|
* @param {Function} callback
|
|
* @returns {Outlayer.Item} item
|
|
*/
|
|
proto.getItem = function( elem ) {
|
|
// loop through items to get the one that matches
|
|
for ( var i=0; i < this.items.length; i++ ) {
|
|
var item = this.items[i];
|
|
if ( item.element == elem ) {
|
|
// return item
|
|
return item;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* get collection of Outlayer.Items, given Elements
|
|
* @param {Array} elems
|
|
* @returns {Array} items - Outlayer.Items
|
|
*/
|
|
proto.getItems = function( elems ) {
|
|
elems = utils.makeArray( elems );
|
|
var items = [];
|
|
elems.forEach( function( elem ) {
|
|
var item = this.getItem( elem );
|
|
if ( item ) {
|
|
items.push( item );
|
|
}
|
|
}, this );
|
|
|
|
return items;
|
|
};
|
|
|
|
/**
|
|
* remove element(s) from instance and DOM
|
|
* @param {Array or NodeList or Element} elems
|
|
*/
|
|
proto.remove = function( elems ) {
|
|
var removeItems = this.getItems( elems );
|
|
|
|
this._emitCompleteOnItems( 'remove', removeItems );
|
|
|
|
// bail if no items to remove
|
|
if ( !removeItems || !removeItems.length ) {
|
|
return;
|
|
}
|
|
|
|
removeItems.forEach( function( item ) {
|
|
item.remove();
|
|
// remove item from collection
|
|
utils.removeFrom( this.items, item );
|
|
}, this );
|
|
};
|
|
|
|
// ----- destroy ----- //
|
|
|
|
// remove and disable Outlayer instance
|
|
proto.destroy = function() {
|
|
// clean up dynamic styles
|
|
var style = this.element.style;
|
|
style.height = '';
|
|
style.position = '';
|
|
style.width = '';
|
|
// destroy items
|
|
this.items.forEach( function( item ) {
|
|
item.destroy();
|
|
});
|
|
|
|
this.unbindResize();
|
|
|
|
var id = this.element.outlayerGUID;
|
|
delete instances[ id ]; // remove reference to instance by id
|
|
delete this.element.outlayerGUID;
|
|
// remove data for jQuery
|
|
if ( jQuery ) {
|
|
jQuery.removeData( this.element, this.constructor.namespace );
|
|
}
|
|
|
|
};
|
|
|
|
// -------------------------- data -------------------------- //
|
|
|
|
/**
|
|
* get Outlayer instance from element
|
|
* @param {Element} elem
|
|
* @returns {Outlayer}
|
|
*/
|
|
Outlayer.data = function( elem ) {
|
|
elem = utils.getQueryElement( elem );
|
|
var id = elem && elem.outlayerGUID;
|
|
return id && instances[ id ];
|
|
};
|
|
|
|
|
|
// -------------------------- create Outlayer class -------------------------- //
|
|
|
|
/**
|
|
* create a layout class
|
|
* @param {String} namespace
|
|
*/
|
|
Outlayer.create = function( namespace, options ) {
|
|
// sub-class Outlayer
|
|
var Layout = subclass( Outlayer );
|
|
// apply new options and compatOptions
|
|
Layout.defaults = utils.extend( {}, Outlayer.defaults );
|
|
utils.extend( Layout.defaults, options );
|
|
Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions );
|
|
|
|
Layout.namespace = namespace;
|
|
|
|
Layout.data = Outlayer.data;
|
|
|
|
// sub-class Item
|
|
Layout.Item = subclass( Item );
|
|
|
|
// -------------------------- declarative -------------------------- //
|
|
|
|
utils.htmlInit( Layout, namespace );
|
|
|
|
// -------------------------- jQuery bridge -------------------------- //
|
|
|
|
// make into jQuery plugin
|
|
if ( jQuery && jQuery.bridget ) {
|
|
jQuery.bridget( namespace, Layout );
|
|
}
|
|
|
|
return Layout;
|
|
};
|
|
|
|
function subclass( Parent ) {
|
|
function SubClass() {
|
|
Parent.apply( this, arguments );
|
|
}
|
|
|
|
SubClass.prototype = Object.create( Parent.prototype );
|
|
SubClass.prototype.constructor = SubClass;
|
|
|
|
return SubClass;
|
|
}
|
|
|
|
// ----- helpers ----- //
|
|
|
|
// how many milliseconds are in each unit
|
|
var msUnits = {
|
|
ms: 1,
|
|
s: 1000
|
|
};
|
|
|
|
// munge time-like parameter into millisecond number
|
|
// '0.4s' -> 40
|
|
function getMilliseconds( time ) {
|
|
if ( typeof time == 'number' ) {
|
|
return time;
|
|
}
|
|
var matches = time.match( /(^\d*\.?\d*)(\w*)/ );
|
|
var num = matches && matches[1];
|
|
var unit = matches && matches[2];
|
|
if ( !num.length ) {
|
|
return 0;
|
|
}
|
|
num = parseFloat( num );
|
|
var mult = msUnits[ unit ] || 1;
|
|
return num * mult;
|
|
}
|
|
|
|
// ----- fin ----- //
|
|
|
|
// back in global
|
|
Outlayer.Item = Item;
|
|
|
|
return Outlayer;
|
|
|
|
}));
|
|
|
|
|
|
/***/ },
|
|
|
|
/***/ 658
|
|
(module) {
|
|
|
|
"use strict";
|
|
/* jshint browser:true, node:true */
|
|
|
|
|
|
module.exports = function(global) {
|
|
|
|
/* Set up a RequestAnimationFrame shim so we can animate efficiently FOR
|
|
* GREAT JUSTICE. */
|
|
var requestInterval, cancelInterval;
|
|
|
|
(function() {
|
|
var raf = global.requestAnimationFrame ||
|
|
global.webkitRequestAnimationFrame ||
|
|
global.mozRequestAnimationFrame ||
|
|
global.oRequestAnimationFrame ||
|
|
global.msRequestAnimationFrame ,
|
|
caf = global.cancelAnimationFrame ||
|
|
global.webkitCancelAnimationFrame ||
|
|
global.mozCancelAnimationFrame ||
|
|
global.oCancelAnimationFrame ||
|
|
global.msCancelAnimationFrame ;
|
|
|
|
if(raf && caf) {
|
|
requestInterval = function(fn, delay) {
|
|
var handle = {value: null};
|
|
|
|
function loop() {
|
|
handle.value = raf(loop);
|
|
fn();
|
|
}
|
|
|
|
loop();
|
|
return handle;
|
|
};
|
|
|
|
cancelInterval = function(handle) {
|
|
caf(handle.value);
|
|
};
|
|
}
|
|
|
|
else {
|
|
requestInterval = setInterval;
|
|
cancelInterval = clearInterval;
|
|
}
|
|
}());
|
|
|
|
/* Catmull-rom spline stuffs. */
|
|
/*
|
|
function upsample(n, spline) {
|
|
var polyline = [],
|
|
len = spline.length,
|
|
bx = spline[0],
|
|
by = spline[1],
|
|
cx = spline[2],
|
|
cy = spline[3],
|
|
dx = spline[4],
|
|
dy = spline[5],
|
|
i, j, ax, ay, px, qx, rx, sx, py, qy, ry, sy, t;
|
|
|
|
for(i = 6; i !== spline.length; i += 2) {
|
|
ax = bx;
|
|
bx = cx;
|
|
cx = dx;
|
|
dx = spline[i ];
|
|
px = -0.5 * ax + 1.5 * bx - 1.5 * cx + 0.5 * dx;
|
|
qx = ax - 2.5 * bx + 2.0 * cx - 0.5 * dx;
|
|
rx = -0.5 * ax + 0.5 * cx ;
|
|
sx = bx ;
|
|
|
|
ay = by;
|
|
by = cy;
|
|
cy = dy;
|
|
dy = spline[i + 1];
|
|
py = -0.5 * ay + 1.5 * by - 1.5 * cy + 0.5 * dy;
|
|
qy = ay - 2.5 * by + 2.0 * cy - 0.5 * dy;
|
|
ry = -0.5 * ay + 0.5 * cy ;
|
|
sy = by ;
|
|
|
|
for(j = 0; j !== n; ++j) {
|
|
t = j / n;
|
|
|
|
polyline.push(
|
|
((px * t + qx) * t + rx) * t + sx,
|
|
((py * t + qy) * t + ry) * t + sy
|
|
);
|
|
}
|
|
}
|
|
|
|
polyline.push(
|
|
px + qx + rx + sx,
|
|
py + qy + ry + sy
|
|
);
|
|
|
|
return polyline;
|
|
}
|
|
|
|
function downsample(n, polyline) {
|
|
var len = 0,
|
|
i, dx, dy;
|
|
|
|
for(i = 2; i !== polyline.length; i += 2) {
|
|
dx = polyline[i ] - polyline[i - 2];
|
|
dy = polyline[i + 1] - polyline[i - 1];
|
|
len += Math.sqrt(dx * dx + dy * dy);
|
|
}
|
|
|
|
len /= n;
|
|
|
|
var small = [],
|
|
target = len,
|
|
min = 0,
|
|
max, t;
|
|
|
|
small.push(polyline[0], polyline[1]);
|
|
|
|
for(i = 2; i !== polyline.length; i += 2) {
|
|
dx = polyline[i ] - polyline[i - 2];
|
|
dy = polyline[i + 1] - polyline[i - 1];
|
|
max = min + Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if(max > target) {
|
|
t = (target - min) / (max - min);
|
|
|
|
small.push(
|
|
polyline[i - 2] + dx * t,
|
|
polyline[i - 1] + dy * t
|
|
);
|
|
|
|
target += len;
|
|
}
|
|
|
|
min = max;
|
|
}
|
|
|
|
small.push(polyline[polyline.length - 2], polyline[polyline.length - 1]);
|
|
|
|
return small;
|
|
}
|
|
*/
|
|
|
|
/* Define skycon things. */
|
|
/* FIXME: I'm *really really* sorry that this code is so gross. Really, I am.
|
|
* I'll try to clean it up eventually! Promise! */
|
|
var KEYFRAME = 500,
|
|
STROKE = 0.08,
|
|
TAU = 2.0 * Math.PI,
|
|
TWO_OVER_SQRT_2 = 2.0 / Math.sqrt(2);
|
|
|
|
function circle(ctx, x, y, r) {
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, r, 0, TAU, false);
|
|
ctx.fill();
|
|
}
|
|
|
|
function line(ctx, ax, ay, bx, by) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(ax, ay);
|
|
ctx.lineTo(bx, by);
|
|
ctx.stroke();
|
|
}
|
|
|
|
function puff(ctx, t, cx, cy, rx, ry, rmin, rmax) {
|
|
var c = Math.cos(t * TAU),
|
|
s = Math.sin(t * TAU);
|
|
|
|
rmax -= rmin;
|
|
|
|
circle(
|
|
ctx,
|
|
cx - s * rx,
|
|
cy + c * ry + rmax * 0.5,
|
|
rmin + (1 - c * 0.5) * rmax
|
|
);
|
|
}
|
|
|
|
function puffs(ctx, t, cx, cy, rx, ry, rmin, rmax) {
|
|
var i;
|
|
|
|
for(i = 5; i--; )
|
|
puff(ctx, t + i / 5, cx, cy, rx, ry, rmin, rmax);
|
|
}
|
|
|
|
function cloud(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 30000;
|
|
|
|
var a = cw * 0.21,
|
|
b = cw * 0.12,
|
|
c = cw * 0.24,
|
|
d = cw * 0.28;
|
|
|
|
ctx.fillStyle = color;
|
|
puffs(ctx, t, cx, cy, a, b, c, d);
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
puffs(ctx, t, cx, cy, a, b, c - s, d - s);
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
function sun(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 120000;
|
|
|
|
var a = cw * 0.25 - s * 0.5,
|
|
b = cw * 0.32 + s * 0.5,
|
|
c = cw * 0.50 - s * 0.5,
|
|
i, p, cos, sin;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, a, 0, TAU, false);
|
|
ctx.stroke();
|
|
|
|
for(i = 8; i--; ) {
|
|
p = (t + i / 8) * TAU;
|
|
cos = Math.cos(p);
|
|
sin = Math.sin(p);
|
|
line(ctx, cx + cos * b, cy + sin * b, cx + cos * c, cy + sin * c);
|
|
}
|
|
}
|
|
|
|
function moon(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 15000;
|
|
|
|
var a = cw * 0.29 - s * 0.5,
|
|
b = cw * 0.05,
|
|
c = Math.cos(t * TAU),
|
|
p = c * TAU / -16;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
cx += c * b;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(cx, cy, a, p + TAU / 8, p + TAU * 7 / 8, false);
|
|
ctx.arc(cx + Math.cos(p) * a * TWO_OVER_SQRT_2, cy + Math.sin(p) * a * TWO_OVER_SQRT_2, a, p + TAU * 5 / 8, p + TAU * 3 / 8, true);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
}
|
|
|
|
function rain(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 1350;
|
|
|
|
var a = cw * 0.16,
|
|
b = TAU * 11 / 12,
|
|
c = TAU * 7 / 12,
|
|
i, p, x, y;
|
|
|
|
ctx.fillStyle = color;
|
|
|
|
for(i = 4; i--; ) {
|
|
p = (t + i / 4) % 1;
|
|
x = cx + ((i - 1.5) / 1.5) * (i === 1 || i === 2 ? -1 : 1) * a;
|
|
y = cy + p * p * cw;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, y - s * 1.5);
|
|
ctx.arc(x, y, s * 0.75, b, c, false);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
function sleet(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 750;
|
|
|
|
var a = cw * 0.1875,
|
|
b = TAU * 11 / 12,
|
|
c = TAU * 7 / 12,
|
|
i, p, x, y;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s * 0.5;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
for(i = 4; i--; ) {
|
|
p = (t + i / 4) % 1;
|
|
x = Math.floor(cx + ((i - 1.5) / 1.5) * (i === 1 || i === 2 ? -1 : 1) * a) + 0.5;
|
|
y = cy + p * cw;
|
|
line(ctx, x, y - s * 1.5, x, y + s * 1.5);
|
|
}
|
|
}
|
|
|
|
function snow(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 3000;
|
|
|
|
var a = cw * 0.16,
|
|
b = s * 0.75,
|
|
u = t * TAU * 0.7,
|
|
ux = Math.cos(u) * b,
|
|
uy = Math.sin(u) * b,
|
|
v = u + TAU / 3,
|
|
vx = Math.cos(v) * b,
|
|
vy = Math.sin(v) * b,
|
|
w = u + TAU * 2 / 3,
|
|
wx = Math.cos(w) * b,
|
|
wy = Math.sin(w) * b,
|
|
i, p, x, y;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s * 0.5;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
for(i = 4; i--; ) {
|
|
p = (t + i / 4) % 1;
|
|
x = cx + Math.sin((p + i / 4) * TAU) * a;
|
|
y = cy + p * cw;
|
|
|
|
line(ctx, x - ux, y - uy, x + ux, y + uy);
|
|
line(ctx, x - vx, y - vy, x + vx, y + vy);
|
|
line(ctx, x - wx, y - wy, x + wx, y + wy);
|
|
}
|
|
}
|
|
|
|
function fogbank(ctx, t, cx, cy, cw, s, color) {
|
|
t /= 30000;
|
|
|
|
var a = cw * 0.21,
|
|
b = cw * 0.06,
|
|
c = cw * 0.21,
|
|
d = cw * 0.28;
|
|
|
|
ctx.fillStyle = color;
|
|
puffs(ctx, t, cx, cy, a, b, c, d);
|
|
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
puffs(ctx, t, cx, cy, a, b, c - s, d - s);
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
}
|
|
|
|
/*
|
|
var WIND_PATHS = [
|
|
downsample(63, upsample(8, [
|
|
-1.00, -0.28,
|
|
-0.75, -0.18,
|
|
-0.50, 0.12,
|
|
-0.20, 0.12,
|
|
-0.04, -0.04,
|
|
-0.07, -0.18,
|
|
-0.19, -0.18,
|
|
-0.23, -0.05,
|
|
-0.12, 0.11,
|
|
0.02, 0.16,
|
|
0.20, 0.15,
|
|
0.50, 0.07,
|
|
0.75, 0.18,
|
|
1.00, 0.28
|
|
])),
|
|
downsample(31, upsample(16, [
|
|
-1.00, -0.10,
|
|
-0.75, 0.00,
|
|
-0.50, 0.10,
|
|
-0.25, 0.14,
|
|
0.00, 0.10,
|
|
0.25, 0.00,
|
|
0.50, -0.10,
|
|
0.75, -0.14,
|
|
1.00, -0.10
|
|
]))
|
|
];
|
|
*/
|
|
|
|
var WIND_PATHS = [
|
|
[
|
|
-0.7500, -0.1800, -0.7219, -0.1527, -0.6971, -0.1225,
|
|
-0.6739, -0.0910, -0.6516, -0.0588, -0.6298, -0.0262,
|
|
-0.6083, 0.0065, -0.5868, 0.0396, -0.5643, 0.0731,
|
|
-0.5372, 0.1041, -0.5033, 0.1259, -0.4662, 0.1406,
|
|
-0.4275, 0.1493, -0.3881, 0.1530, -0.3487, 0.1526,
|
|
-0.3095, 0.1488, -0.2708, 0.1421, -0.2319, 0.1342,
|
|
-0.1943, 0.1217, -0.1600, 0.1025, -0.1290, 0.0785,
|
|
-0.1012, 0.0509, -0.0764, 0.0206, -0.0547, -0.0120,
|
|
-0.0378, -0.0472, -0.0324, -0.0857, -0.0389, -0.1241,
|
|
-0.0546, -0.1599, -0.0814, -0.1876, -0.1193, -0.1964,
|
|
-0.1582, -0.1935, -0.1931, -0.1769, -0.2157, -0.1453,
|
|
-0.2290, -0.1085, -0.2327, -0.0697, -0.2240, -0.0317,
|
|
-0.2064, 0.0033, -0.1853, 0.0362, -0.1613, 0.0672,
|
|
-0.1350, 0.0961, -0.1051, 0.1213, -0.0706, 0.1397,
|
|
-0.0332, 0.1512, 0.0053, 0.1580, 0.0442, 0.1624,
|
|
0.0833, 0.1636, 0.1224, 0.1615, 0.1613, 0.1565,
|
|
0.1999, 0.1500, 0.2378, 0.1402, 0.2749, 0.1279,
|
|
0.3118, 0.1147, 0.3487, 0.1015, 0.3858, 0.0892,
|
|
0.4236, 0.0787, 0.4621, 0.0715, 0.5012, 0.0702,
|
|
0.5398, 0.0766, 0.5768, 0.0890, 0.6123, 0.1055,
|
|
0.6466, 0.1244, 0.6805, 0.1440, 0.7147, 0.1630,
|
|
0.7500, 0.1800
|
|
],
|
|
[
|
|
-0.7500, 0.0000, -0.7033, 0.0195, -0.6569, 0.0399,
|
|
-0.6104, 0.0600, -0.5634, 0.0789, -0.5155, 0.0954,
|
|
-0.4667, 0.1089, -0.4174, 0.1206, -0.3676, 0.1299,
|
|
-0.3174, 0.1365, -0.2669, 0.1398, -0.2162, 0.1391,
|
|
-0.1658, 0.1347, -0.1157, 0.1271, -0.0661, 0.1169,
|
|
-0.0170, 0.1046, 0.0316, 0.0903, 0.0791, 0.0728,
|
|
0.1259, 0.0534, 0.1723, 0.0331, 0.2188, 0.0129,
|
|
0.2656, -0.0064, 0.3122, -0.0263, 0.3586, -0.0466,
|
|
0.4052, -0.0665, 0.4525, -0.0847, 0.5007, -0.1002,
|
|
0.5497, -0.1130, 0.5991, -0.1240, 0.6491, -0.1325,
|
|
0.6994, -0.1380, 0.7500, -0.1400
|
|
]
|
|
],
|
|
WIND_OFFSETS = [
|
|
{start: 0.36, end: 0.11},
|
|
{start: 0.56, end: 0.16}
|
|
];
|
|
|
|
function leaf(ctx, t, x, y, cw, s, color) {
|
|
var a = cw / 8,
|
|
b = a / 3,
|
|
c = 2 * b,
|
|
d = (t % 1) * TAU,
|
|
e = Math.cos(d),
|
|
f = Math.sin(d);
|
|
|
|
ctx.fillStyle = color;
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(x , y , a, d , d + Math.PI, false);
|
|
ctx.arc(x - b * e, y - b * f, c, d + Math.PI, d , false);
|
|
ctx.arc(x + c * e, y + c * f, b, d + Math.PI, d , true );
|
|
ctx.globalCompositeOperation = 'destination-out';
|
|
ctx.fill();
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.stroke();
|
|
}
|
|
|
|
function swoosh(ctx, t, cx, cy, cw, s, index, total, color) {
|
|
t /= 2500;
|
|
|
|
var path = WIND_PATHS[index],
|
|
a = (t + index - WIND_OFFSETS[index].start) % total,
|
|
c = (t + index - WIND_OFFSETS[index].end ) % total,
|
|
e = (t + index ) % total,
|
|
b, d, f, i;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = s;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
if(a < 1) {
|
|
ctx.beginPath();
|
|
|
|
a *= path.length / 2 - 1;
|
|
b = Math.floor(a);
|
|
a -= b;
|
|
b *= 2;
|
|
b += 2;
|
|
|
|
ctx.moveTo(
|
|
cx + (path[b - 2] * (1 - a) + path[b ] * a) * cw,
|
|
cy + (path[b - 1] * (1 - a) + path[b + 1] * a) * cw
|
|
);
|
|
|
|
if(c < 1) {
|
|
c *= path.length / 2 - 1;
|
|
d = Math.floor(c);
|
|
c -= d;
|
|
d *= 2;
|
|
d += 2;
|
|
|
|
for(i = b; i !== d; i += 2)
|
|
ctx.lineTo(cx + path[i] * cw, cy + path[i + 1] * cw);
|
|
|
|
ctx.lineTo(
|
|
cx + (path[d - 2] * (1 - c) + path[d ] * c) * cw,
|
|
cy + (path[d - 1] * (1 - c) + path[d + 1] * c) * cw
|
|
);
|
|
}
|
|
|
|
else
|
|
for(i = b; i !== path.length; i += 2)
|
|
ctx.lineTo(cx + path[i] * cw, cy + path[i + 1] * cw);
|
|
|
|
ctx.stroke();
|
|
}
|
|
|
|
else if(c < 1) {
|
|
ctx.beginPath();
|
|
|
|
c *= path.length / 2 - 1;
|
|
d = Math.floor(c);
|
|
c -= d;
|
|
d *= 2;
|
|
d += 2;
|
|
|
|
ctx.moveTo(cx + path[0] * cw, cy + path[1] * cw);
|
|
|
|
for(i = 2; i !== d; i += 2)
|
|
ctx.lineTo(cx + path[i] * cw, cy + path[i + 1] * cw);
|
|
|
|
ctx.lineTo(
|
|
cx + (path[d - 2] * (1 - c) + path[d ] * c) * cw,
|
|
cy + (path[d - 1] * (1 - c) + path[d + 1] * c) * cw
|
|
);
|
|
|
|
ctx.stroke();
|
|
}
|
|
|
|
if(e < 1) {
|
|
e *= path.length / 2 - 1;
|
|
f = Math.floor(e);
|
|
e -= f;
|
|
f *= 2;
|
|
f += 2;
|
|
|
|
leaf(
|
|
ctx,
|
|
t,
|
|
cx + (path[f - 2] * (1 - e) + path[f ] * e) * cw,
|
|
cy + (path[f - 1] * (1 - e) + path[f + 1] * e) * cw,
|
|
cw,
|
|
s,
|
|
color
|
|
);
|
|
}
|
|
}
|
|
|
|
var Skycons = function(opts) {
|
|
this.list = [];
|
|
this.interval = null;
|
|
this.color = opts && opts.color ? opts.color : "black";
|
|
this.resizeClear = !!(opts && opts.resizeClear);
|
|
};
|
|
|
|
Skycons.CLEAR_DAY = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
sun(ctx, t, w * 0.5, h * 0.5, s, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.CLEAR_NIGHT = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
moon(ctx, t, w * 0.5, h * 0.5, s, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.PARTLY_CLOUDY_DAY = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
sun(ctx, t, w * 0.625, h * 0.375, s * 0.75, s * STROKE, color);
|
|
cloud(ctx, t, w * 0.375, h * 0.625, s * 0.75, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.PARTLY_CLOUDY_NIGHT = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
moon(ctx, t, w * 0.667, h * 0.375, s * 0.75, s * STROKE, color);
|
|
cloud(ctx, t, w * 0.375, h * 0.625, s * 0.75, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.CLOUDY = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
cloud(ctx, t, w * 0.5, h * 0.5, s, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.RAIN = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
rain(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
cloud(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.SLEET = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
sleet(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
cloud(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.SNOW = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
snow(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
cloud(ctx, t, w * 0.5, h * 0.37, s * 0.9, s * STROKE, color);
|
|
};
|
|
|
|
Skycons.WIND = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h);
|
|
|
|
swoosh(ctx, t, w * 0.5, h * 0.5, s, s * STROKE, 0, 2, color);
|
|
swoosh(ctx, t, w * 0.5, h * 0.5, s, s * STROKE, 1, 2, color);
|
|
};
|
|
|
|
Skycons.FOG = function(ctx, t, color) {
|
|
var w = ctx.canvas.width,
|
|
h = ctx.canvas.height,
|
|
s = Math.min(w, h),
|
|
k = s * STROKE;
|
|
|
|
fogbank(ctx, t, w * 0.5, h * 0.32, s * 0.75, k, color);
|
|
|
|
t /= 5000;
|
|
|
|
var a = Math.cos((t ) * TAU) * s * 0.02,
|
|
b = Math.cos((t + 0.25) * TAU) * s * 0.02,
|
|
c = Math.cos((t + 0.50) * TAU) * s * 0.02,
|
|
d = Math.cos((t + 0.75) * TAU) * s * 0.02,
|
|
n = h * 0.936,
|
|
e = Math.floor(n - k * 0.5) + 0.5,
|
|
f = Math.floor(n - k * 2.5) + 0.5;
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = k;
|
|
ctx.lineCap = "round";
|
|
ctx.lineJoin = "round";
|
|
|
|
line(ctx, a + w * 0.2 + k * 0.5, e, b + w * 0.8 - k * 0.5, e);
|
|
line(ctx, c + w * 0.2 + k * 0.5, f, d + w * 0.8 - k * 0.5, f);
|
|
};
|
|
|
|
Skycons.prototype = {
|
|
_determineDrawingFunction: function(draw) {
|
|
if(typeof draw === "string")
|
|
draw = Skycons[draw.toUpperCase().replace(/-/g, "_")] || null;
|
|
|
|
return draw;
|
|
},
|
|
add: function(el, draw) {
|
|
var obj;
|
|
|
|
if(typeof el === "string")
|
|
el = document.getElementById(el);
|
|
|
|
// Does nothing if canvas name doesn't exists
|
|
if(el === null)
|
|
return;
|
|
|
|
draw = this._determineDrawingFunction(draw);
|
|
|
|
// Does nothing if the draw function isn't actually a function
|
|
if(typeof draw !== "function")
|
|
return;
|
|
|
|
obj = {
|
|
element: el,
|
|
context: el.getContext("2d"),
|
|
drawing: draw
|
|
};
|
|
|
|
this.list.push(obj);
|
|
this.draw(obj, KEYFRAME);
|
|
},
|
|
set: function(el, draw) {
|
|
var i;
|
|
|
|
if(typeof el === "string")
|
|
el = document.getElementById(el);
|
|
|
|
for(i = this.list.length; i--; )
|
|
if(this.list[i].element === el) {
|
|
this.list[i].drawing = this._determineDrawingFunction(draw);
|
|
this.draw(this.list[i], KEYFRAME);
|
|
return;
|
|
}
|
|
|
|
this.add(el, draw);
|
|
},
|
|
remove: function(el) {
|
|
var i;
|
|
|
|
if(typeof el === "string")
|
|
el = document.getElementById(el);
|
|
|
|
for(i = this.list.length; i--; )
|
|
if(this.list[i].element === el) {
|
|
this.list.splice(i, 1);
|
|
return;
|
|
}
|
|
},
|
|
draw: function(obj, time) {
|
|
var canvas = obj.context.canvas;
|
|
|
|
if(this.resizeClear)
|
|
canvas.width = canvas.width;
|
|
|
|
else
|
|
obj.context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
obj.drawing(obj.context, time, this.color);
|
|
},
|
|
play: function() {
|
|
var self = this;
|
|
|
|
this.pause();
|
|
this.interval = requestInterval(function() {
|
|
var now = Date.now(),
|
|
i;
|
|
|
|
for(i = self.list.length; i--; )
|
|
self.draw(self.list[i], now);
|
|
}, 1000 / 60);
|
|
},
|
|
pause: function() {
|
|
var i;
|
|
|
|
if(this.interval) {
|
|
cancelInterval(this.interval);
|
|
this.interval = null;
|
|
}
|
|
}
|
|
};
|
|
return Skycons;
|
|
};
|
|
|
|
|
|
/***/ }
|
|
|
|
/******/ });
|
|
/************************************************************************/
|
|
/******/ // The module cache
|
|
/******/ var __webpack_module_cache__ = {};
|
|
/******/
|
|
/******/ // The require function
|
|
/******/ function __webpack_require__(moduleId) {
|
|
/******/ // Check if module is in cache
|
|
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
/******/ if (cachedModule !== undefined) {
|
|
/******/ return cachedModule.exports;
|
|
/******/ }
|
|
/******/ // Create a new module (and put it into the cache)
|
|
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
/******/ // no module.id needed
|
|
/******/ // no module.loaded needed
|
|
/******/ exports: {}
|
|
/******/ };
|
|
/******/
|
|
/******/ // Execute the module function
|
|
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
/******/
|
|
/******/ // Return the exports of the module
|
|
/******/ return module.exports;
|
|
/******/ }
|
|
/******/
|
|
/************************************************************************/
|
|
/******/ /* webpack/runtime/compat get default export */
|
|
/******/ (() => {
|
|
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
/******/ __webpack_require__.n = (module) => {
|
|
/******/ var getter = module && module.__esModule ?
|
|
/******/ () => (module['default']) :
|
|
/******/ () => (module);
|
|
/******/ __webpack_require__.d(getter, { a: getter });
|
|
/******/ return getter;
|
|
/******/ };
|
|
/******/ })();
|
|
/******/
|
|
/******/ /* webpack/runtime/define property getters */
|
|
/******/ (() => {
|
|
/******/ // define getter functions for harmony exports
|
|
/******/ __webpack_require__.d = (exports, definition) => {
|
|
/******/ for(var key in definition) {
|
|
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
/******/ }
|
|
/******/ }
|
|
/******/ };
|
|
/******/ })();
|
|
/******/
|
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
/******/ (() => {
|
|
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
/******/ })();
|
|
/******/
|
|
/************************************************************************/
|
|
var __webpack_exports__ = {};
|
|
// This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
|
|
(() => {
|
|
"use strict";
|
|
|
|
;// ../assets/scripts/utils/dom.js
|
|
/**
|
|
* Adminator DOM Utility Functions
|
|
* Provides jQuery-like functionality using vanilla JavaScript
|
|
*
|
|
* @module utils/dom
|
|
* @example
|
|
* import { DOM } from './utils/dom';
|
|
*
|
|
* // Select elements
|
|
* const button = DOM.select('.my-button');
|
|
* const items = DOM.selectAll('.list-item');
|
|
*
|
|
* // Add event listeners
|
|
* DOM.on(button, 'click', () => console.log('Clicked!'));
|
|
*
|
|
* // Manipulate classes
|
|
* DOM.addClass(button, 'active');
|
|
* DOM.toggleClass(button, 'loading');
|
|
*/
|
|
|
|
/**
|
|
* DOM utility object providing jQuery-like methods
|
|
* @namespace
|
|
*/
|
|
const DOM = {
|
|
/**
|
|
* Select a single element matching the selector
|
|
* Replaces jQuery's $('selector').first()
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @param {Document|Element} [context=document] - Context to search within
|
|
* @returns {Element|null} The matched element or null
|
|
*
|
|
* @example
|
|
* const header = DOM.select('.header');
|
|
* const navItem = DOM.select('.nav-item', sidebar);
|
|
*/
|
|
select: function (selector) {
|
|
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
|
|
return context.querySelector(selector);
|
|
},
|
|
/**
|
|
* Select all elements matching the selector
|
|
* Replaces jQuery's $('selector')
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @param {Document|Element} [context=document] - Context to search within
|
|
* @returns {Element[]} Array of matched elements
|
|
*
|
|
* @example
|
|
* const buttons = DOM.selectAll('.btn');
|
|
* buttons.forEach(btn => DOM.addClass(btn, 'initialized'));
|
|
*/
|
|
selectAll: function (selector) {
|
|
let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document;
|
|
return Array.from(context.querySelectorAll(selector));
|
|
},
|
|
/**
|
|
* Check if an element matching the selector exists
|
|
*
|
|
* @param {string} selector - CSS selector
|
|
* @returns {boolean} True if element exists
|
|
*
|
|
* @example
|
|
* if (DOM.exists('.sidebar')) {
|
|
* initSidebar();
|
|
* }
|
|
*/
|
|
exists: selector => {
|
|
return document.querySelector(selector) !== null;
|
|
},
|
|
/**
|
|
* Add an event listener to an element
|
|
* Replaces jQuery's $.on()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} event - Event name (e.g., 'click', 'change')
|
|
* @param {Function} handler - Event handler function
|
|
* @param {Object} [options={}] - addEventListener options
|
|
*
|
|
* @example
|
|
* DOM.on('.btn', 'click', handleClick);
|
|
* DOM.on(button, 'click', handleClick, { once: true });
|
|
*/
|
|
on: function (element, event, handler) {
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.addEventListener(event, handler, options);
|
|
}
|
|
},
|
|
/**
|
|
* Remove an event listener from an element
|
|
* Replaces jQuery's $.off()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} event - Event name
|
|
* @param {Function} handler - Event handler to remove
|
|
*
|
|
* @example
|
|
* DOM.off(button, 'click', handleClick);
|
|
*/
|
|
off: (element, event, handler) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.removeEventListener(event, handler);
|
|
}
|
|
},
|
|
/**
|
|
* Add a class to an element
|
|
* Replaces jQuery's $.addClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to add
|
|
*
|
|
* @example
|
|
* DOM.addClass('.menu', 'open');
|
|
*/
|
|
addClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.add(className);
|
|
}
|
|
},
|
|
/**
|
|
* Remove a class from an element
|
|
* Replaces jQuery's $.removeClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to remove
|
|
*
|
|
* @example
|
|
* DOM.removeClass('.menu', 'open');
|
|
*/
|
|
removeClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.remove(className);
|
|
}
|
|
},
|
|
/**
|
|
* Toggle a class on an element
|
|
* Replaces jQuery's $.toggleClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to toggle
|
|
*
|
|
* @example
|
|
* DOM.toggleClass('.dropdown', 'show');
|
|
*/
|
|
toggleClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.classList.toggle(className);
|
|
}
|
|
},
|
|
/**
|
|
* Check if an element has a class
|
|
* Replaces jQuery's $.hasClass()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} className - Class name to check
|
|
* @returns {boolean} True if element has the class
|
|
*
|
|
* @example
|
|
* if (DOM.hasClass('.menu', 'open')) {
|
|
* closeMenu();
|
|
* }
|
|
*/
|
|
hasClass: (element, className) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
return element ? element.classList.contains(className) : false;
|
|
},
|
|
/**
|
|
* Get or set an attribute on an element
|
|
* Replaces jQuery's $.attr()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} name - Attribute name
|
|
* @param {string} [value] - Value to set (omit to get)
|
|
* @returns {string|Element|null} Attribute value when getting, element when setting
|
|
*
|
|
* @example
|
|
* // Get attribute
|
|
* const href = DOM.attr(link, 'href');
|
|
*
|
|
* // Set attribute
|
|
* DOM.attr(link, 'href', '/new-page');
|
|
*/
|
|
attr: (element, name, value) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (value === undefined) {
|
|
return element.getAttribute(name);
|
|
} else {
|
|
element.setAttribute(name, value);
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set a data attribute on an element
|
|
* Replaces jQuery's $.data()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} name - Data attribute name (without 'data-' prefix)
|
|
* @param {string} [value] - Value to set (omit to get)
|
|
* @returns {string|Element|null} Data value when getting, element when setting
|
|
*
|
|
* @example
|
|
* // Get data attribute
|
|
* const id = DOM.data(row, 'id'); // Gets data-id
|
|
*
|
|
* // Set data attribute
|
|
* DOM.data(row, 'id', '123');
|
|
*/
|
|
data: (element, name, value) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
const dataName = `data-${name}`;
|
|
if (value === undefined) {
|
|
return element.getAttribute(dataName);
|
|
} else {
|
|
element.setAttribute(dataName, value);
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set text content of an element
|
|
* Replaces jQuery's $.text()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [content] - Text to set (omit to get)
|
|
* @returns {string|Element|null} Text content when getting, element when setting
|
|
*
|
|
* @example
|
|
* const text = DOM.text('.title');
|
|
* DOM.text('.title', 'New Title');
|
|
*/
|
|
text: (element, content) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (content === undefined) {
|
|
return element.textContent;
|
|
} else {
|
|
element.textContent = content;
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Get or set HTML content of an element
|
|
* Replaces jQuery's $.html()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [content] - HTML to set (omit to get)
|
|
* @returns {string|Element|null} HTML content when getting, element when setting
|
|
*
|
|
* @example
|
|
* const html = DOM.html('.container');
|
|
* DOM.html('.container', '<p>New content</p>');
|
|
*/
|
|
html: (element, content) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
if (content === undefined) {
|
|
return element.innerHTML;
|
|
} else {
|
|
element.innerHTML = content;
|
|
return element;
|
|
}
|
|
},
|
|
/**
|
|
* Hide an element
|
|
* Replaces jQuery's $.hide()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
*
|
|
* @example
|
|
* DOM.hide('.modal');
|
|
*/
|
|
hide: element => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.style.display = 'none';
|
|
}
|
|
},
|
|
/**
|
|
* Show an element
|
|
* Replaces jQuery's $.show()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [display='block'] - Display value to use
|
|
*
|
|
* @example
|
|
* DOM.show('.modal');
|
|
* DOM.show('.flex-item', 'flex');
|
|
*/
|
|
show: function (element) {
|
|
let display = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'block';
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
element.style.display = display;
|
|
}
|
|
},
|
|
/**
|
|
* Toggle element visibility
|
|
* Replaces jQuery's $.toggle()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} [display='block'] - Display value when showing
|
|
*
|
|
* @example
|
|
* DOM.toggle('.menu');
|
|
*/
|
|
toggle: function (element) {
|
|
let display = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'block';
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (element) {
|
|
if (element.style.display === 'none') {
|
|
element.style.display = display;
|
|
} else {
|
|
element.style.display = 'none';
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Animate element sliding up (collapsing)
|
|
* Replaces jQuery's $.slideUp()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.slideUp('.panel');
|
|
*/
|
|
slideUp: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
const height = element.scrollHeight;
|
|
element.style.height = `${height}px`;
|
|
element.style.overflow = 'hidden';
|
|
element.animate([{
|
|
height: `${height}px`
|
|
}, {
|
|
height: '0px'
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.display = 'none';
|
|
element.style.height = '';
|
|
element.style.overflow = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element sliding down (expanding)
|
|
* Replaces jQuery's $.slideDown()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.slideDown('.panel');
|
|
*/
|
|
slideDown: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.style.display = 'block';
|
|
element.style.height = '0px';
|
|
element.style.overflow = 'hidden';
|
|
const height = element.scrollHeight;
|
|
element.animate([{
|
|
height: '0px'
|
|
}, {
|
|
height: `${height}px`
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.height = 'auto';
|
|
element.style.overflow = 'visible';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element fading in
|
|
* Replaces jQuery's $.fadeIn()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.fadeIn('.modal');
|
|
*/
|
|
fadeIn: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.style.opacity = '0';
|
|
element.style.display = 'block';
|
|
element.animate([{
|
|
opacity: 0
|
|
}, {
|
|
opacity: 1
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.opacity = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Animate element fading out
|
|
* Replaces jQuery's $.fadeOut()
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {number} [duration=300] - Animation duration in ms
|
|
* @returns {Promise<void>} Resolves when animation completes
|
|
*
|
|
* @example
|
|
* await DOM.fadeOut('.modal');
|
|
*/
|
|
fadeOut: function (element) {
|
|
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 300;
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return Promise.resolve();
|
|
return new Promise(resolve => {
|
|
element.animate([{
|
|
opacity: 1
|
|
}, {
|
|
opacity: 0
|
|
}], {
|
|
duration,
|
|
easing: 'ease-in-out'
|
|
}).onfinish = () => {
|
|
element.style.display = 'none';
|
|
element.style.opacity = '';
|
|
resolve();
|
|
};
|
|
});
|
|
},
|
|
/**
|
|
* Get element dimensions and position relative to viewport
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @returns {Object|null} Dimensions object or null
|
|
* @property {number} width - Element width
|
|
* @property {number} height - Element height
|
|
* @property {number} top - Distance from viewport top
|
|
* @property {number} left - Distance from viewport left
|
|
* @property {number} bottom - Distance from viewport bottom
|
|
* @property {number} right - Distance from viewport right
|
|
*
|
|
* @example
|
|
* const { width, height, top, left } = DOM.dimensions('.card');
|
|
*/
|
|
dimensions: element => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
if (!element) return null;
|
|
const rect = element.getBoundingClientRect();
|
|
return {
|
|
width: rect.width,
|
|
height: rect.height,
|
|
top: rect.top,
|
|
left: rect.left,
|
|
bottom: rect.bottom,
|
|
right: rect.right
|
|
};
|
|
},
|
|
/**
|
|
* Execute callback when DOM is ready
|
|
* Replaces jQuery's $(document).ready()
|
|
*
|
|
* @param {Function} callback - Function to execute when DOM is ready
|
|
*
|
|
* @example
|
|
* DOM.ready(() => {
|
|
* initApp();
|
|
* });
|
|
*/
|
|
ready: callback => {
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', callback);
|
|
} else {
|
|
callback();
|
|
}
|
|
},
|
|
/**
|
|
* Create an element with optional attributes and children
|
|
*
|
|
* @param {string} tag - HTML tag name
|
|
* @param {Object} [attrs={}] - Attributes to set
|
|
* @param {Array<Element|string>} [children=[]] - Child elements or text
|
|
* @returns {Element} The created element
|
|
*
|
|
* @example
|
|
* const button = DOM.create('button', { class: 'btn', type: 'submit' }, ['Submit']);
|
|
*/
|
|
create: function (tag) {
|
|
let attrs = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
let children = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
|
|
const el = document.createElement(tag);
|
|
Object.entries(attrs).forEach(_ref => {
|
|
let [key, value] = _ref;
|
|
if (key === 'class') {
|
|
el.className = value;
|
|
} else if (key.startsWith('data-')) {
|
|
el.setAttribute(key, value);
|
|
} else {
|
|
el[key] = value;
|
|
}
|
|
});
|
|
children.forEach(child => {
|
|
if (typeof child === 'string') {
|
|
el.appendChild(document.createTextNode(child));
|
|
} else if (child instanceof Element) {
|
|
el.appendChild(child);
|
|
}
|
|
});
|
|
return el;
|
|
},
|
|
/**
|
|
* Find the closest ancestor matching a selector
|
|
*
|
|
* @param {Element|string} element - Element or selector
|
|
* @param {string} selector - Selector to match
|
|
* @returns {Element|null} Closest matching ancestor or null
|
|
*
|
|
* @example
|
|
* const form = DOM.closest(input, 'form');
|
|
*/
|
|
closest: (element, selector) => {
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
return element ? element.closest(selector) : null;
|
|
}
|
|
};
|
|
/* harmony default export */ const dom = ((/* unused pure expression or super */ null && (DOM)));
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/dayjs.min.js
|
|
var dayjs_min = __webpack_require__(464);
|
|
var dayjs_min_default = /*#__PURE__*/__webpack_require__.n(dayjs_min);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/utc.js
|
|
var utc = __webpack_require__(657);
|
|
var utc_default = /*#__PURE__*/__webpack_require__.n(utc);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/timezone.js
|
|
var timezone = __webpack_require__(168);
|
|
var timezone_default = /*#__PURE__*/__webpack_require__.n(timezone);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/relativeTime.js
|
|
var relativeTime = __webpack_require__(562);
|
|
var relativeTime_default = /*#__PURE__*/__webpack_require__.n(relativeTime);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/customParseFormat.js
|
|
var customParseFormat = __webpack_require__(630);
|
|
var customParseFormat_default = /*#__PURE__*/__webpack_require__.n(customParseFormat);
|
|
// EXTERNAL MODULE: ../../node_modules/dayjs/plugin/advancedFormat.js
|
|
var advancedFormat = __webpack_require__(258);
|
|
var advancedFormat_default = /*#__PURE__*/__webpack_require__.n(advancedFormat);
|
|
;// ../assets/scripts/utils/date.js
|
|
/**
|
|
* Modern Date Utilities
|
|
* Using Day.js (2KB) instead of Moment.js (67KB) - 97% size reduction
|
|
* Provides consistent date formatting and manipulation across the application
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Enable Day.js plugins
|
|
dayjs_min_default().extend((utc_default()));
|
|
dayjs_min_default().extend((timezone_default()));
|
|
dayjs_min_default().extend((relativeTime_default()));
|
|
dayjs_min_default().extend((customParseFormat_default()));
|
|
dayjs_min_default().extend((advancedFormat_default()));
|
|
const DateUtils = {
|
|
/**
|
|
* Get current date/time
|
|
*/
|
|
now: () => dayjs_min_default()(),
|
|
/**
|
|
* Parse date from string or Date object
|
|
*/
|
|
parse: function (input) {
|
|
let format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
return format ? dayjs_min_default()(input, format) : dayjs_min_default()(input);
|
|
},
|
|
/**
|
|
* Format date for display
|
|
*/
|
|
format: function (date) {
|
|
let format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'YYYY-MM-DD';
|
|
return dayjs_min_default()(date).format(format);
|
|
},
|
|
/**
|
|
* Common date formatting presets
|
|
*/
|
|
formatters: {
|
|
// Dashboard display formats
|
|
shortDate: date => dayjs_min_default()(date).format('MMM DD, YYYY'),
|
|
longDate: date => dayjs_min_default()(date).format('MMMM DD, YYYY'),
|
|
dateTime: date => dayjs_min_default()(date).format('MMM DD, YYYY h:mm A'),
|
|
// Calendar formats
|
|
calendarDate: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
calendarDateTime: date => dayjs_min_default()(date).format('YYYY-MM-DD HH:mm:ss'),
|
|
// Form input formats
|
|
inputDate: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
inputDateTime: date => dayjs_min_default()(date).format('YYYY-MM-DDTHH:mm'),
|
|
// Display formats
|
|
timeOnly: date => dayjs_min_default()(date).format('h:mm A'),
|
|
monthYear: date => dayjs_min_default()(date).format('MMMM YYYY'),
|
|
dayMonth: date => dayjs_min_default()(date).format('DD MMM'),
|
|
// Relative time
|
|
relative: date => dayjs_min_default()(date).fromNow(),
|
|
relativeCalendar: date => {
|
|
const now = dayjs_min_default()();
|
|
const target = dayjs_min_default()(date);
|
|
const diffDays = now.diff(target, 'day');
|
|
if (diffDays === 0) return 'Today';
|
|
if (diffDays === 1) return 'Yesterday';
|
|
if (diffDays === -1) return 'Tomorrow';
|
|
if (diffDays > 1 && diffDays < 7) return `${diffDays} days ago`;
|
|
if (diffDays < -1 && diffDays > -7) return `In ${Math.abs(diffDays)} days`;
|
|
return target.format('MMM DD, YYYY');
|
|
}
|
|
},
|
|
/**
|
|
* Date manipulation
|
|
*/
|
|
add: (date, amount, unit) => dayjs_min_default()(date).add(amount, unit),
|
|
subtract: (date, amount, unit) => dayjs_min_default()(date).subtract(amount, unit),
|
|
startOf: (date, unit) => dayjs_min_default()(date).startOf(unit),
|
|
endOf: (date, unit) => dayjs_min_default()(date).endOf(unit),
|
|
/**
|
|
* Date comparison
|
|
*/
|
|
isBefore: (date1, date2) => dayjs_min_default()(date1).isBefore(dayjs_min_default()(date2)),
|
|
isAfter: (date1, date2) => dayjs_min_default()(date1).isAfter(dayjs_min_default()(date2)),
|
|
isSame: function (date1, date2) {
|
|
let unit = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'day';
|
|
return dayjs_min_default()(date1).isSame(dayjs_min_default()(date2), unit);
|
|
},
|
|
isBetween: (date, start, end) => dayjs_min_default()(date).isBetween(dayjs_min_default()(start), dayjs_min_default()(end)),
|
|
/**
|
|
* Date validation
|
|
*/
|
|
isValid: date => dayjs_min_default()(date).isValid(),
|
|
/**
|
|
* Timezone utilities
|
|
*/
|
|
timezone: {
|
|
convert: (date, tz) => dayjs_min_default()(date).tz(tz),
|
|
utc: date => dayjs_min_default()(date).utc(),
|
|
local: date => dayjs_min_default()(date).local(),
|
|
guess: () => dayjs_min_default().tz.guess()
|
|
},
|
|
/**
|
|
* Calendar utilities
|
|
*/
|
|
calendar: {
|
|
// Get calendar month data for building calendar views
|
|
getMonthData: function () {
|
|
let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
|
const target = date ? dayjs_min_default()(date) : dayjs_min_default()();
|
|
const startOfMonth = target.startOf('month');
|
|
const endOfMonth = target.endOf('month');
|
|
const startOfCalendar = startOfMonth.startOf('week');
|
|
const endOfCalendar = endOfMonth.endOf('week');
|
|
const days = [];
|
|
let current = startOfCalendar;
|
|
while (current.isBefore(endOfCalendar) || current.isSame(endOfCalendar, 'day')) {
|
|
days.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
day: current.date(),
|
|
isCurrentMonth: current.isSame(target, 'month'),
|
|
isToday: current.isSame(dayjs_min_default()(), 'day'),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, 'day');
|
|
}
|
|
return {
|
|
month: target.format('MMMM YYYY'),
|
|
year: target.year(),
|
|
monthIndex: target.month(),
|
|
days
|
|
};
|
|
},
|
|
// Get week data
|
|
getWeekData: function () {
|
|
let date = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
|
|
const target = date ? dayjs_min_default()(date) : dayjs_min_default()();
|
|
const startOfWeek = target.startOf('week');
|
|
const endOfWeek = target.endOf('week');
|
|
const days = [];
|
|
let current = startOfWeek;
|
|
while (current.isBefore(endOfWeek) || current.isSame(endOfWeek, 'day')) {
|
|
days.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
day: current.date(),
|
|
dayName: current.format('dddd'),
|
|
shortDayName: current.format('ddd'),
|
|
isToday: current.isSame(dayjs_min_default()(), 'day'),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, 'day');
|
|
}
|
|
return {
|
|
weekStart: startOfWeek.format('MMM DD'),
|
|
weekEnd: endOfWeek.format('MMM DD, YYYY'),
|
|
days
|
|
};
|
|
}
|
|
},
|
|
/**
|
|
* Form utilities
|
|
*/
|
|
form: {
|
|
// Convert date to HTML5 input format
|
|
toInputValue: date => dayjs_min_default()(date).format('YYYY-MM-DD'),
|
|
toDateTimeInputValue: date => dayjs_min_default()(date).format('YYYY-MM-DDTHH:mm'),
|
|
// Parse from HTML5 input
|
|
fromInputValue: value => dayjs_min_default()(value),
|
|
// Validate date input
|
|
validateDateInput: value => {
|
|
const parsed = dayjs_min_default()(value);
|
|
return parsed.isValid() && value.length >= 8; // Basic validation
|
|
}
|
|
},
|
|
/**
|
|
* Chart/Data utilities
|
|
*/
|
|
charts: {
|
|
// Generate date ranges for charts
|
|
generateDateRange: function (start, end) {
|
|
let interval = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'day';
|
|
const dates = [];
|
|
let current = dayjs_min_default()(start);
|
|
const endDate = dayjs_min_default()(end);
|
|
while (current.isBefore(endDate) || current.isSame(endDate, interval)) {
|
|
dates.push({
|
|
date: current.format('YYYY-MM-DD'),
|
|
label: current.format('MMM DD'),
|
|
value: current.toISOString(),
|
|
dayjs: current.clone()
|
|
});
|
|
current = current.add(1, interval);
|
|
}
|
|
return dates;
|
|
},
|
|
// Get common chart date labels
|
|
getChartLabels: function () {
|
|
let period = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'week';
|
|
const now = dayjs_min_default()();
|
|
switch (period) {
|
|
case 'week':
|
|
return Array.from({
|
|
length: 7
|
|
}, (_, i) => now.subtract(6 - i, 'day').format('ddd'));
|
|
case 'month':
|
|
return Array.from({
|
|
length: 30
|
|
}, (_, i) => now.subtract(29 - i, 'day').format('DD'));
|
|
case 'year':
|
|
return Array.from({
|
|
length: 12
|
|
}, (_, i) => now.subtract(11 - i, 'month').format('MMM'));
|
|
default:
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Export dayjs instance for direct use when needed
|
|
|
|
|
|
// Default export
|
|
/* harmony default export */ const date = (DateUtils);
|
|
;// ../assets/scripts/utils/theme.js
|
|
/**
|
|
* Adminator Theme Manager
|
|
* Handles light/dark mode switching with localStorage persistence
|
|
*
|
|
* @module utils/theme
|
|
* @example
|
|
* // Get current theme
|
|
* const current = Theme.current(); // 'light' or 'dark'
|
|
*
|
|
* // Toggle theme
|
|
* Theme.toggle();
|
|
*
|
|
* // Apply specific theme
|
|
* Theme.apply('dark');
|
|
*
|
|
* // Listen for theme changes
|
|
* window.addEventListener('adminator:themeChanged', (e) => {
|
|
* console.log('New theme:', e.detail.theme);
|
|
* });
|
|
*/
|
|
|
|
/* global Chart */
|
|
|
|
/** @constant {string} Storage key for theme preference */
|
|
const THEME_KEY = 'adminator-theme';
|
|
|
|
/** @constant {string[]} Valid theme values */
|
|
const VALID_THEMES = ['light', 'dark'];
|
|
|
|
/**
|
|
* Safe localStorage wrapper
|
|
* Handles cases where localStorage is unavailable (private browsing, etc.)
|
|
* @private
|
|
*/
|
|
const Storage = {
|
|
/**
|
|
* Get item from localStorage
|
|
* @param {string} key - Storage key
|
|
* @returns {string|null} Stored value or null
|
|
*/
|
|
get(key) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
/**
|
|
* Set item in localStorage
|
|
* @param {string} key - Storage key
|
|
* @param {string} value - Value to store
|
|
* @returns {boolean} Success status
|
|
*/
|
|
set(key, value) {
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Theme Manager
|
|
* @namespace
|
|
*/
|
|
const Theme = {
|
|
/**
|
|
* Apply a theme to the document
|
|
* Updates Chart.js defaults if available
|
|
*
|
|
* @param {('light'|'dark')} theme - Theme to apply
|
|
* @fires adminator:themeChanged
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Theme.apply('dark');
|
|
*/
|
|
apply(theme) {
|
|
// Validate theme
|
|
if (!VALID_THEMES.includes(theme)) {
|
|
console.warn(`[Adminator] Invalid theme "${theme}". Using "light".`);
|
|
theme = 'light';
|
|
}
|
|
|
|
// Apply to document
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
|
|
// Update Chart.js defaults if available
|
|
if (window.Chart && Chart.defaults) {
|
|
const isDark = theme === 'dark';
|
|
const textColor = isDark ? '#FFFFFF' : '#212529';
|
|
const mutedColor = isDark ? '#D1D5DB' : '#6C757D';
|
|
const borderColor = isDark ? '#374151' : '#E2E5E8';
|
|
const gridColor = isDark ? 'rgba(209, 213, 219, 0.15)' : 'rgba(0, 0, 0, 0.05)';
|
|
const tooltipBg = isDark ? '#1F2937' : 'rgba(255, 255, 255, 0.95)';
|
|
|
|
// Set global defaults
|
|
Chart.defaults.color = textColor;
|
|
Chart.defaults.borderColor = borderColor;
|
|
Chart.defaults.backgroundColor = tooltipBg;
|
|
|
|
// Set plugin defaults
|
|
Chart.defaults.plugins.legend.labels.color = textColor;
|
|
Chart.defaults.plugins.tooltip.backgroundColor = tooltipBg;
|
|
Chart.defaults.plugins.tooltip.titleColor = textColor;
|
|
Chart.defaults.plugins.tooltip.bodyColor = textColor;
|
|
Chart.defaults.plugins.tooltip.borderColor = borderColor;
|
|
|
|
// Set scale defaults
|
|
const scales = ['category', 'linear', 'logarithmic', 'time', 'radialLinear'];
|
|
scales.forEach(scale => {
|
|
if (Chart.defaults.scales[scale]) {
|
|
Chart.defaults.scales[scale].ticks.color = mutedColor;
|
|
Chart.defaults.scales[scale].grid.color = gridColor;
|
|
}
|
|
});
|
|
|
|
// RadialLinear specific
|
|
if (Chart.defaults.scales.radialLinear) {
|
|
Chart.defaults.scales.radialLinear.pointLabels.color = mutedColor;
|
|
Chart.defaults.scales.radialLinear.angleLines.color = gridColor;
|
|
}
|
|
}
|
|
|
|
// Persist to storage
|
|
Storage.set(THEME_KEY, theme);
|
|
|
|
// Update toggle accessibility state if exists
|
|
const toggle = document.getElementById('theme-toggle');
|
|
if (toggle) {
|
|
toggle.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
|
|
}
|
|
|
|
// Dispatch event for other components
|
|
window.dispatchEvent(new CustomEvent('adminator:themeChanged', {
|
|
detail: {
|
|
theme
|
|
}
|
|
}));
|
|
return true;
|
|
},
|
|
/**
|
|
* Toggle between light and dark themes
|
|
*
|
|
* @returns {('light'|'dark')} The new theme
|
|
*
|
|
* @example
|
|
* const newTheme = Theme.toggle();
|
|
* console.log('Switched to:', newTheme);
|
|
*/
|
|
toggle() {
|
|
const next = this.current() === 'dark' ? 'light' : 'dark';
|
|
this.apply(next);
|
|
return next;
|
|
},
|
|
/**
|
|
* Get the current theme
|
|
*
|
|
* @returns {('light'|'dark')} Current theme
|
|
*
|
|
* @example
|
|
* if (Theme.current() === 'dark') {
|
|
* // Dark mode specific logic
|
|
* }
|
|
*/
|
|
current() {
|
|
const stored = Storage.get(THEME_KEY);
|
|
return VALID_THEMES.includes(stored) ? stored : 'light';
|
|
},
|
|
/**
|
|
* Initialize the theme system
|
|
* Detects OS preference on first visit, otherwise uses stored preference
|
|
*
|
|
* @returns {('light'|'dark')} The applied theme
|
|
*
|
|
* @example
|
|
* // Call once on app initialization
|
|
* Theme.init();
|
|
*/
|
|
init() {
|
|
const stored = Storage.get(THEME_KEY);
|
|
if (!stored) {
|
|
// First visit - detect OS preference
|
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
const theme = prefersDark ? 'dark' : 'light';
|
|
this.apply(theme);
|
|
return theme;
|
|
}
|
|
|
|
// Use stored preference
|
|
this.apply(this.current());
|
|
return this.current();
|
|
},
|
|
/**
|
|
* Get a CSS variable value from the document
|
|
*
|
|
* @param {string} varName - CSS variable name (with or without --)
|
|
* @returns {string} The CSS variable value
|
|
*
|
|
* @example
|
|
* const bgColor = Theme.getCSSVar('--c-bkg-body');
|
|
*/
|
|
getCSSVar(varName) {
|
|
const name = varName.startsWith('--') ? varName : `--${varName}`;
|
|
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
},
|
|
/**
|
|
* Get theme-aware colors for vector maps
|
|
*
|
|
* @returns {Object} Vector map color configuration
|
|
* @property {string} backgroundColor - Map background color
|
|
* @property {string} borderColor - Border color
|
|
* @property {string} regionColor - Default region fill color
|
|
* @property {string} markerFill - Marker fill color
|
|
* @property {string} markerStroke - Marker stroke color
|
|
* @property {string} hoverColor - Region hover color
|
|
* @property {string} selectedColor - Selected region color
|
|
* @property {string} scaleStart - Scale gradient start
|
|
* @property {string} scaleEnd - Scale gradient end
|
|
*/
|
|
getVectorMapColors() {
|
|
return {
|
|
backgroundColor: this.getCSSVar('--vmap-bg-color'),
|
|
borderColor: this.getCSSVar('--vmap-border-color'),
|
|
regionColor: this.getCSSVar('--vmap-region-color'),
|
|
markerFill: this.getCSSVar('--vmap-marker-fill'),
|
|
markerStroke: this.getCSSVar('--vmap-marker-stroke'),
|
|
hoverColor: this.getCSSVar('--vmap-hover-color'),
|
|
selectedColor: this.getCSSVar('--vmap-selected-color'),
|
|
scaleStart: this.getCSSVar('--vmap-scale-start'),
|
|
scaleEnd: this.getCSSVar('--vmap-scale-end'),
|
|
scaleLight: this.getCSSVar('--vmap-scale-light'),
|
|
scaleDark: this.getCSSVar('--vmap-scale-dark')
|
|
};
|
|
},
|
|
/**
|
|
* Get theme-aware colors for sparkline charts
|
|
*
|
|
* @returns {Object} Sparkline color configuration
|
|
*/
|
|
getSparklineColors() {
|
|
return {
|
|
success: this.getCSSVar('--sparkline-success'),
|
|
purple: this.getCSSVar('--sparkline-purple'),
|
|
info: this.getCSSVar('--sparkline-info'),
|
|
danger: this.getCSSVar('--sparkline-danger'),
|
|
light: this.getCSSVar('--sparkline-light')
|
|
};
|
|
},
|
|
/**
|
|
* Get theme-aware colors for Chart.js charts
|
|
*
|
|
* @returns {Object} Chart color configuration
|
|
* @property {string} textColor - Main text color
|
|
* @property {string} mutedColor - Muted/secondary text color
|
|
* @property {string} borderColor - Border color
|
|
* @property {string} gridColor - Grid line color
|
|
* @property {string} tooltipBg - Tooltip background color
|
|
*/
|
|
getChartColors() {
|
|
const isDark = this.current() === 'dark';
|
|
return {
|
|
textColor: isDark ? '#FFFFFF' : '#212529',
|
|
mutedColor: isDark ? '#D1D5DB' : '#6C757D',
|
|
borderColor: isDark ? '#374151' : '#E2E5E8',
|
|
gridColor: isDark ? 'rgba(209, 213, 219, 0.15)' : 'rgba(0, 0, 0, 0.05)',
|
|
tooltipBg: isDark ? '#1F2937' : 'rgba(255, 255, 255, 0.95)'
|
|
};
|
|
},
|
|
/**
|
|
* Check if dark mode is currently active
|
|
*
|
|
* @returns {boolean} True if dark mode is active
|
|
*
|
|
* @example
|
|
* if (Theme.isDark()) {
|
|
* // Apply dark-specific styles
|
|
* }
|
|
*/
|
|
isDark() {
|
|
return this.current() === 'dark';
|
|
},
|
|
/**
|
|
* Check if light mode is currently active
|
|
*
|
|
* @returns {boolean} True if light mode is active
|
|
*/
|
|
isLight() {
|
|
return this.current() === 'light';
|
|
}
|
|
};
|
|
/* harmony default export */ const theme = (Theme);
|
|
;// ../assets/scripts/utils/events.js
|
|
/**
|
|
* Adminator Event Utilities
|
|
* Provides efficient event handling with delegation and cleanup
|
|
*
|
|
* @module utils/events
|
|
*/
|
|
|
|
/**
|
|
* Store for event handlers to enable proper cleanup
|
|
* @type {WeakMap<Element, Map<string, Set<Function>>>}
|
|
*/
|
|
const handlerRegistry = new WeakMap();
|
|
|
|
/**
|
|
* Store for AbortControllers to enable easy cleanup
|
|
* @type {WeakMap<Element, AbortController>}
|
|
*/
|
|
const controllerRegistry = new WeakMap();
|
|
|
|
/**
|
|
* Event utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Events = {
|
|
/**
|
|
* Add an event listener with automatic cleanup support
|
|
* Uses AbortController for efficient removal
|
|
*
|
|
* @param {Element} element - Target element
|
|
* @param {string} event - Event type
|
|
* @param {Function} handler - Event handler
|
|
* @param {Object} [options={}] - Event listener options
|
|
* @returns {Function} Cleanup function to remove the listener
|
|
*
|
|
* @example
|
|
* const cleanup = Events.on(button, 'click', handleClick);
|
|
* // Later: cleanup() to remove
|
|
*/
|
|
on(element, event, handler) {
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
if (!element) return () => {};
|
|
|
|
// Get or create AbortController for this element
|
|
let controller = controllerRegistry.get(element);
|
|
if (!controller) {
|
|
controller = new AbortController();
|
|
controllerRegistry.set(element, controller);
|
|
}
|
|
|
|
// Register handler
|
|
if (!handlerRegistry.has(element)) {
|
|
handlerRegistry.set(element, new Map());
|
|
}
|
|
const elementHandlers = handlerRegistry.get(element);
|
|
if (!elementHandlers.has(event)) {
|
|
elementHandlers.set(event, new Set());
|
|
}
|
|
elementHandlers.get(event).add(handler);
|
|
|
|
// Add listener with abort signal
|
|
element.addEventListener(event, handler, {
|
|
...options,
|
|
signal: controller.signal
|
|
});
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
var _handlerRegistry$get;
|
|
element.removeEventListener(event, handler, options);
|
|
const handlers = (_handlerRegistry$get = handlerRegistry.get(element)) === null || _handlerRegistry$get === void 0 ? void 0 : _handlerRegistry$get.get(event);
|
|
if (handlers) {
|
|
handlers.delete(handler);
|
|
}
|
|
};
|
|
},
|
|
/**
|
|
* Remove all event listeners from an element
|
|
*
|
|
* @param {Element} element - Target element
|
|
*
|
|
* @example
|
|
* Events.off(element); // Removes all listeners
|
|
*/
|
|
off(element) {
|
|
if (!element) return;
|
|
const controller = controllerRegistry.get(element);
|
|
if (controller) {
|
|
controller.abort();
|
|
controllerRegistry.delete(element);
|
|
}
|
|
handlerRegistry.delete(element);
|
|
},
|
|
/**
|
|
* Add event delegation - listen on parent for events from children
|
|
* More efficient than adding listeners to many elements
|
|
*
|
|
* @param {Element} parent - Parent element to listen on
|
|
* @param {string} event - Event type
|
|
* @param {string} selector - CSS selector for target elements
|
|
* @param {Function} handler - Event handler (receives event and matched element)
|
|
* @param {Object} [options={}] - Event listener options
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* // Instead of adding click to every .btn
|
|
* Events.delegate(container, 'click', '.btn', (e, btn) => {
|
|
* console.log('Button clicked:', btn);
|
|
* });
|
|
*/
|
|
delegate(parent, event, selector, handler) {
|
|
let options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
|
|
if (!parent) return () => {};
|
|
const delegatedHandler = e => {
|
|
const target = e.target.closest(selector);
|
|
if (target && parent.contains(target)) {
|
|
handler.call(target, e, target);
|
|
}
|
|
};
|
|
return this.on(parent, event, delegatedHandler, options);
|
|
},
|
|
/**
|
|
* Add a one-time event listener
|
|
*
|
|
* @param {Element} element - Target element
|
|
* @param {string} event - Event type
|
|
* @param {Function} handler - Event handler
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* Events.once(button, 'click', handleFirstClick);
|
|
*/
|
|
once(element, event, handler) {
|
|
return this.on(element, event, handler, {
|
|
once: true
|
|
});
|
|
},
|
|
/**
|
|
* Create a debounced event handler
|
|
*
|
|
* @param {Function} handler - Original handler
|
|
* @param {number} [delay=250] - Debounce delay in ms
|
|
* @returns {Function} Debounced handler
|
|
*
|
|
* @example
|
|
* const debouncedResize = Events.debounce(handleResize, 200);
|
|
* window.addEventListener('resize', debouncedResize);
|
|
*/
|
|
debounce(handler) {
|
|
let delay = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 250;
|
|
let timeoutId;
|
|
return function debounced() {
|
|
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
args[_key] = arguments[_key];
|
|
}
|
|
clearTimeout(timeoutId);
|
|
timeoutId = setTimeout(() => handler.apply(this, args), delay);
|
|
};
|
|
},
|
|
/**
|
|
* Create a throttled event handler
|
|
*
|
|
* @param {Function} handler - Original handler
|
|
* @param {number} [limit=250] - Throttle limit in ms
|
|
* @returns {Function} Throttled handler
|
|
*
|
|
* @example
|
|
* const throttledScroll = Events.throttle(handleScroll, 100);
|
|
* window.addEventListener('scroll', throttledScroll);
|
|
*/
|
|
throttle(handler) {
|
|
let limit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 250;
|
|
let inThrottle;
|
|
return function throttled() {
|
|
if (!inThrottle) {
|
|
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
|
|
args[_key2] = arguments[_key2];
|
|
}
|
|
handler.apply(this, args);
|
|
inThrottle = true;
|
|
setTimeout(() => inThrottle = false, limit);
|
|
}
|
|
};
|
|
},
|
|
/**
|
|
* Dispatch a custom event
|
|
*
|
|
* @param {Element|Window} target - Target to dispatch on
|
|
* @param {string} eventName - Event name
|
|
* @param {Object} [detail={}] - Event detail data
|
|
* @param {Object} [options={}] - Event options (bubbles, cancelable)
|
|
* @returns {boolean} Whether the event was not cancelled
|
|
*
|
|
* @example
|
|
* Events.emit(element, 'custom:event', { data: 'value' });
|
|
*/
|
|
emit(target, eventName) {
|
|
let detail = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
let options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
|
|
const event = new CustomEvent(eventName, {
|
|
detail,
|
|
bubbles: options.bubbles ?? true,
|
|
cancelable: options.cancelable ?? true
|
|
});
|
|
return target.dispatchEvent(event);
|
|
}
|
|
};
|
|
/* harmony default export */ const events = (Events);
|
|
;// ../assets/scripts/utils/performance.js
|
|
/**
|
|
* Adminator Performance Utilities
|
|
* Provides ResizeObserver, IntersectionObserver, and lazy loading utilities
|
|
*
|
|
* @module utils/performance
|
|
*/
|
|
|
|
/**
|
|
* Store for ResizeObserver instances
|
|
* @type {WeakMap<Element, Function>}
|
|
*/
|
|
const resizeCallbacks = new WeakMap();
|
|
|
|
/**
|
|
* Shared ResizeObserver instance for efficiency
|
|
* @type {ResizeObserver|null}
|
|
*/
|
|
let sharedResizeObserver = null;
|
|
|
|
/**
|
|
* Store for IntersectionObserver callbacks
|
|
* @type {WeakMap<Element, Function>}
|
|
*/
|
|
const intersectionCallbacks = new WeakMap();
|
|
|
|
/**
|
|
* Map of IntersectionObservers by threshold
|
|
* @type {Map<string, IntersectionObserver>}
|
|
*/
|
|
const intersectionObservers = new Map();
|
|
|
|
/**
|
|
* Performance utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Performance = {
|
|
/**
|
|
* Observe element resize events efficiently
|
|
* Uses shared ResizeObserver for better performance
|
|
*
|
|
* @param {Element} element - Element to observe
|
|
* @param {Function} callback - Callback receiving { width, height, entry }
|
|
* @returns {Function} Cleanup function to stop observing
|
|
*
|
|
* @example
|
|
* const unobserve = Performance.onResize(chart, ({ width, height }) => {
|
|
* chart.resize(width, height);
|
|
* });
|
|
*/
|
|
onResize(element, callback) {
|
|
if (!element || typeof callback !== 'function') {
|
|
return () => {};
|
|
}
|
|
|
|
// Create shared observer if needed
|
|
if (!sharedResizeObserver) {
|
|
sharedResizeObserver = new ResizeObserver(entries => {
|
|
for (const entry of entries) {
|
|
const cb = resizeCallbacks.get(entry.target);
|
|
if (cb) {
|
|
const {
|
|
width,
|
|
height
|
|
} = entry.contentRect;
|
|
cb({
|
|
width,
|
|
height,
|
|
entry
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Store callback and observe
|
|
resizeCallbacks.set(element, callback);
|
|
sharedResizeObserver.observe(element);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
var _sharedResizeObserver;
|
|
resizeCallbacks.delete(element);
|
|
(_sharedResizeObserver = sharedResizeObserver) === null || _sharedResizeObserver === void 0 || _sharedResizeObserver.unobserve(element);
|
|
};
|
|
},
|
|
/**
|
|
* Observe when element enters/exits viewport
|
|
* Useful for lazy loading and animations
|
|
*
|
|
* @param {Element} element - Element to observe
|
|
* @param {Function} callback - Callback receiving { isIntersecting, entry }
|
|
* @param {Object} [options={}] - IntersectionObserver options
|
|
* @param {number} [options.threshold=0] - Visibility threshold (0-1)
|
|
* @param {string} [options.rootMargin='0px'] - Root margin
|
|
* @returns {Function} Cleanup function to stop observing
|
|
*
|
|
* @example
|
|
* const unobserve = Performance.onVisible(element, ({ isIntersecting }) => {
|
|
* if (isIntersecting) {
|
|
* loadContent();
|
|
* unobserve(); // Stop after first trigger
|
|
* }
|
|
* });
|
|
*/
|
|
onVisible(element, callback) {
|
|
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
if (!element || typeof callback !== 'function') {
|
|
return () => {};
|
|
}
|
|
const {
|
|
threshold = 0,
|
|
rootMargin = '0px'
|
|
} = options;
|
|
const key = `${threshold}-${rootMargin}`;
|
|
|
|
// Get or create observer for this threshold
|
|
if (!intersectionObservers.has(key)) {
|
|
const observer = new IntersectionObserver(entries => {
|
|
for (const entry of entries) {
|
|
const cb = intersectionCallbacks.get(entry.target);
|
|
if (cb) {
|
|
cb({
|
|
isIntersecting: entry.isIntersecting,
|
|
ratio: entry.intersectionRatio,
|
|
entry
|
|
});
|
|
}
|
|
}
|
|
}, {
|
|
threshold,
|
|
rootMargin
|
|
});
|
|
intersectionObservers.set(key, observer);
|
|
}
|
|
const observer = intersectionObservers.get(key);
|
|
|
|
// Store callback and observe
|
|
intersectionCallbacks.set(element, callback);
|
|
observer.observe(element);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
intersectionCallbacks.delete(element);
|
|
observer.unobserve(element);
|
|
};
|
|
},
|
|
/**
|
|
* Lazy load an element when it becomes visible
|
|
* Automatically handles cleanup after loading
|
|
*
|
|
* @param {Element} element - Element to lazy load
|
|
* @param {Function} loadFn - Function to call when visible
|
|
* @param {Object} [options={}] - Options
|
|
* @param {string} [options.rootMargin='100px'] - Preload margin
|
|
* @returns {Function} Cleanup function
|
|
*
|
|
* @example
|
|
* Performance.lazyLoad(chartContainer, () => {
|
|
* initializeChart(chartContainer);
|
|
* });
|
|
*/
|
|
lazyLoad(element, loadFn) {
|
|
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
const {
|
|
rootMargin = '100px'
|
|
} = options;
|
|
let loaded = false;
|
|
const unobserve = this.onVisible(element, _ref => {
|
|
let {
|
|
isIntersecting
|
|
} = _ref;
|
|
if (isIntersecting && !loaded) {
|
|
loaded = true;
|
|
loadFn();
|
|
unobserve();
|
|
}
|
|
}, {
|
|
rootMargin
|
|
});
|
|
return unobserve;
|
|
},
|
|
/**
|
|
* Batch DOM reads and writes to prevent layout thrashing
|
|
*
|
|
* @param {Function} readFn - Function that reads from DOM
|
|
* @param {Function} writeFn - Function that writes to DOM
|
|
*
|
|
* @example
|
|
* Performance.batch(
|
|
* () => element.offsetHeight, // Read
|
|
* (height) => element.style.minHeight = height + 'px' // Write
|
|
* );
|
|
*/
|
|
batch(readFn, writeFn) {
|
|
// Use requestAnimationFrame for batching
|
|
requestAnimationFrame(() => {
|
|
const value = readFn();
|
|
requestAnimationFrame(() => {
|
|
writeFn(value);
|
|
});
|
|
});
|
|
},
|
|
/**
|
|
* Execute callback on next animation frame
|
|
*
|
|
* @param {Function} callback - Function to execute
|
|
* @returns {number} Request ID for cancellation
|
|
*
|
|
* @example
|
|
* const id = Performance.nextFrame(() => updateUI());
|
|
* // Cancel: cancelAnimationFrame(id);
|
|
*/
|
|
nextFrame(callback) {
|
|
return requestAnimationFrame(callback);
|
|
},
|
|
/**
|
|
* Execute callback when browser is idle
|
|
* Falls back to setTimeout if requestIdleCallback not available
|
|
*
|
|
* @param {Function} callback - Function to execute
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.timeout=1000] - Maximum wait time
|
|
* @returns {number} Request ID for cancellation
|
|
*
|
|
* @example
|
|
* Performance.whenIdle(() => {
|
|
* // Non-critical work
|
|
* preloadNextPage();
|
|
* });
|
|
*/
|
|
whenIdle(callback) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
const {
|
|
timeout = 1000
|
|
} = options;
|
|
if ('requestIdleCallback' in window) {
|
|
return requestIdleCallback(callback, {
|
|
timeout
|
|
});
|
|
}
|
|
|
|
// Fallback for Safari
|
|
return setTimeout(callback, 1);
|
|
},
|
|
/**
|
|
* Cancel an idle callback
|
|
*
|
|
* @param {number} id - Request ID from whenIdle
|
|
*/
|
|
cancelIdle(id) {
|
|
if ('cancelIdleCallback' in window) {
|
|
cancelIdleCallback(id);
|
|
} else {
|
|
clearTimeout(id);
|
|
}
|
|
},
|
|
/**
|
|
* Preload a resource (image, script, etc.)
|
|
*
|
|
* @param {string} url - URL to preload
|
|
* @param {string} [as='image'] - Resource type (image, script, style, font)
|
|
* @returns {Promise<void>} Resolves when loaded
|
|
*
|
|
* @example
|
|
* await Performance.preload('/images/hero.jpg', 'image');
|
|
*/
|
|
preload(url) {
|
|
let as = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'image';
|
|
return new Promise((resolve, reject) => {
|
|
const link = document.createElement('link');
|
|
link.rel = 'preload';
|
|
link.as = as;
|
|
link.href = url;
|
|
link.onload = resolve;
|
|
link.onerror = reject;
|
|
document.head.appendChild(link);
|
|
});
|
|
},
|
|
/**
|
|
* Measure execution time of a function
|
|
*
|
|
* @param {string} label - Label for the measurement
|
|
* @param {Function} fn - Function to measure
|
|
* @returns {*} Return value of the function
|
|
*
|
|
* @example
|
|
* const result = Performance.measure('render', () => renderChart());
|
|
*/
|
|
measure(label, fn) {
|
|
const start = performance.now();
|
|
const result = fn();
|
|
const end = performance.now();
|
|
if (false) // removed by dead control flow
|
|
{}
|
|
return result;
|
|
},
|
|
/**
|
|
* Cleanup all observers
|
|
* Call this when destroying the app
|
|
*/
|
|
cleanup() {
|
|
// Cleanup resize observer
|
|
if (sharedResizeObserver) {
|
|
sharedResizeObserver.disconnect();
|
|
sharedResizeObserver = null;
|
|
}
|
|
|
|
// Cleanup intersection observers
|
|
for (const observer of intersectionObservers.values()) {
|
|
observer.disconnect();
|
|
}
|
|
intersectionObservers.clear();
|
|
}
|
|
};
|
|
/* harmony default export */ const utils_performance = (Performance);
|
|
;// ../assets/scripts/utils/logger.js
|
|
/**
|
|
* Adminator Logger Utility
|
|
* Development-only logging utility for debugging
|
|
*
|
|
* @module utils/logger
|
|
*/
|
|
|
|
/**
|
|
* Check if we're in development mode
|
|
* @returns {boolean}
|
|
*/
|
|
const isDev = () => {
|
|
try {
|
|
return false || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Logger object with development-only output
|
|
* All methods are no-ops in production for zero overhead
|
|
*/
|
|
const Logger = {
|
|
/**
|
|
* Log informational messages (development only)
|
|
* @param {string} message - The message to log
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
info(message, context) {
|
|
if (isDev()) {
|
|
console.info(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log warning messages (development only)
|
|
* @param {string} message - The warning message
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
warn(message, context) {
|
|
if (isDev()) {
|
|
console.warn(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log error messages (development only)
|
|
* @param {string} message - The error message
|
|
* @param {Error|Object} [context] - Error object or context data
|
|
*/
|
|
error(message, context) {
|
|
if (isDev()) {
|
|
console.error(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Log debug messages (development only)
|
|
* @param {string} message - The debug message
|
|
* @param {Object} [context] - Additional context data
|
|
*/
|
|
debug(message, context) {
|
|
if (isDev()) {
|
|
console.debug(`[Adminator] ${message}`, context || '');
|
|
}
|
|
},
|
|
/**
|
|
* Group related log messages (development only)
|
|
* @param {string} label - Group label
|
|
*/
|
|
group(label) {
|
|
if (isDev()) {
|
|
console.group(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* End a log group (development only)
|
|
*/
|
|
groupEnd() {
|
|
if (isDev()) {
|
|
console.groupEnd();
|
|
}
|
|
},
|
|
/**
|
|
* Log with timing information (development only)
|
|
* @param {string} label - Timer label
|
|
*/
|
|
time(label) {
|
|
if (isDev()) {
|
|
console.time(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* End timing and log result (development only)
|
|
* @param {string} label - Timer label (must match time() call)
|
|
*/
|
|
timeEnd(label) {
|
|
if (isDev()) {
|
|
console.timeEnd(`[Adminator] ${label}`);
|
|
}
|
|
},
|
|
/**
|
|
* Log a table of data (development only)
|
|
* @param {Array|Object} data - Data to display as table
|
|
*/
|
|
table(data) {
|
|
if (isDev()) {
|
|
console.table(data);
|
|
}
|
|
}
|
|
};
|
|
/* harmony default export */ const logger = (Logger);
|
|
;// ../assets/scripts/components/Sidebar.js
|
|
/**
|
|
* Modern Sidebar Component
|
|
* Replaces jQuery-based sidebar functionality with vanilla JavaScript
|
|
*/
|
|
|
|
class Sidebar {
|
|
constructor() {
|
|
this.sidebar = document.querySelector('.sidebar');
|
|
this.sidebarMenu = document.querySelector('.sidebar .sidebar-menu');
|
|
this.sidebarToggleLinks = document.querySelectorAll('.sidebar-toggle a');
|
|
this.sidebarToggleById = document.querySelector('#sidebar-toggle');
|
|
this.app = document.querySelector('.app');
|
|
this.init();
|
|
}
|
|
init() {
|
|
if (!this.sidebar || !this.sidebarMenu) {
|
|
return;
|
|
}
|
|
this.setupMenuToggle();
|
|
this.setupSidebarToggle();
|
|
this.setActiveLink();
|
|
}
|
|
|
|
/**
|
|
* Setup dropdown menu functionality
|
|
*/
|
|
setupMenuToggle() {
|
|
const menuLinks = this.sidebarMenu.querySelectorAll('li a');
|
|
menuLinks.forEach(link => {
|
|
link.addEventListener('click', e => {
|
|
const listItem = link.parentElement;
|
|
const dropdownMenu = listItem.querySelector('.dropdown-menu');
|
|
|
|
// If this is a regular navigation link (not dropdown), allow normal navigation
|
|
if (!dropdownMenu) {
|
|
// Don't prevent default for regular navigation links
|
|
return;
|
|
}
|
|
|
|
// Only prevent default for dropdown toggles
|
|
e.preventDefault();
|
|
if (listItem.classList.contains('open')) {
|
|
this.closeDropdown(listItem, dropdownMenu);
|
|
} else {
|
|
this.closeAllDropdowns();
|
|
this.openDropdown(listItem, dropdownMenu);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open dropdown with smooth animation
|
|
*/
|
|
openDropdown(listItem, dropdownMenu) {
|
|
listItem.classList.add('open');
|
|
dropdownMenu.style.display = 'block';
|
|
dropdownMenu.style.height = '0px';
|
|
dropdownMenu.style.overflow = 'hidden';
|
|
|
|
// Get the natural height
|
|
const height = dropdownMenu.scrollHeight;
|
|
|
|
// Animate to full height
|
|
dropdownMenu.animate([{
|
|
height: '0px'
|
|
}, {
|
|
height: `${height}px`
|
|
}], {
|
|
duration: 200,
|
|
easing: 'ease-out'
|
|
}).onfinish = () => {
|
|
dropdownMenu.style.height = 'auto';
|
|
dropdownMenu.style.overflow = 'visible';
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close dropdown with smooth animation
|
|
*/
|
|
closeDropdown(listItem, dropdownMenu) {
|
|
const height = dropdownMenu.scrollHeight;
|
|
dropdownMenu.style.height = `${height}px`;
|
|
dropdownMenu.style.overflow = 'hidden';
|
|
dropdownMenu.animate([{
|
|
height: `${height}px`
|
|
}, {
|
|
height: '0px'
|
|
}], {
|
|
duration: 200,
|
|
easing: 'ease-in'
|
|
}).onfinish = () => {
|
|
listItem.classList.remove('open');
|
|
dropdownMenu.style.display = 'none';
|
|
dropdownMenu.style.height = '';
|
|
dropdownMenu.style.overflow = '';
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close all open dropdowns
|
|
*/
|
|
closeAllDropdowns() {
|
|
const openItems = this.sidebarMenu.querySelectorAll('li.open');
|
|
openItems.forEach(item => {
|
|
const dropdownMenu = item.querySelector('.dropdown-menu');
|
|
if (dropdownMenu) {
|
|
this.closeDropdown(item, dropdownMenu);
|
|
}
|
|
|
|
// Also remove the has-active-child class
|
|
item.classList.remove('has-active-child');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup sidebar toggle functionality
|
|
*/
|
|
setupSidebarToggle() {
|
|
// Handle mobile sidebar toggle links (inside .sidebar-toggle divs)
|
|
this.sidebarToggleLinks.forEach(link => {
|
|
if (link && this.app) {
|
|
link.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggleSidebar();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle the main topbar sidebar toggle
|
|
if (this.sidebarToggleById && this.app) {
|
|
this.sidebarToggleById.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggleSidebar();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle sidebar and handle resize events properly
|
|
*/
|
|
toggleSidebar() {
|
|
this.app.classList.toggle('is-collapsed');
|
|
|
|
// Only trigger resize for masonry, but avoid chart redraw issues
|
|
setTimeout(() => {
|
|
// Dispatch a custom event instead of generic resize to avoid chart issues
|
|
window.dispatchEvent(new CustomEvent('sidebar:toggle', {
|
|
detail: {
|
|
collapsed: this.app.classList.contains('is-collapsed')
|
|
}
|
|
}));
|
|
|
|
// Still trigger resize for masonry but with a specific check
|
|
if (window.EVENT) {
|
|
window.dispatchEvent(window.EVENT);
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* Set active link based on current URL
|
|
*/
|
|
setActiveLink() {
|
|
// Remove active class from all nav items (including dropdown items)
|
|
const allNavItems = this.sidebar.querySelectorAll('.nav-item');
|
|
allNavItems.forEach(item => {
|
|
item.classList.remove('actived');
|
|
});
|
|
|
|
// Close all dropdowns first
|
|
this.closeAllDropdowns();
|
|
|
|
// Get current page filename
|
|
const currentPath = window.location.pathname;
|
|
const currentPage = currentPath.split('/').pop() || 'index.html';
|
|
|
|
// Find and activate the correct nav item
|
|
const allLinks = this.sidebar.querySelectorAll('a[href]');
|
|
allLinks.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (!href || href === 'javascript:void(0);' || href === 'javascript:void(0)') return;
|
|
|
|
// Extract filename from href
|
|
const linkPage = href.split('/').pop();
|
|
if (linkPage === currentPage) {
|
|
const navItem = link.closest('.nav-item');
|
|
if (navItem) {
|
|
navItem.classList.add('actived');
|
|
|
|
// If this is inside a dropdown, handle parent dropdown specially
|
|
const parentDropdown = navItem.closest('.dropdown-menu');
|
|
if (parentDropdown) {
|
|
const parentDropdownItem = parentDropdown.closest('.nav-item.dropdown');
|
|
if (parentDropdownItem) {
|
|
// Open the parent dropdown
|
|
parentDropdownItem.classList.add('open');
|
|
parentDropdown.style.display = 'block';
|
|
|
|
// Add special styling to indicate parent has active child
|
|
parentDropdownItem.classList.add('has-active-child');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Public method to refresh active links (useful for SPA navigation)
|
|
*/
|
|
refreshActiveLink() {
|
|
this.setActiveLink();
|
|
}
|
|
|
|
/**
|
|
* Public method to toggle sidebar programmatically
|
|
*/
|
|
toggle() {
|
|
if (this.app) {
|
|
this.app.classList.toggle('is-collapsed');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Public method to check if sidebar is collapsed
|
|
*/
|
|
isCollapsed() {
|
|
return this.app ? this.app.classList.contains('is-collapsed') : false;
|
|
}
|
|
}
|
|
/* harmony default export */ const components_Sidebar = (Sidebar);
|
|
;// ../../node_modules/@kurkle/color/dist/color.esm.js
|
|
/*!
|
|
* @kurkle/color v0.3.4
|
|
* https://github.com/kurkle/color#readme
|
|
* (c) 2024 Jukka Kurkela
|
|
* Released under the MIT License
|
|
*/
|
|
function round(v) {
|
|
return v + 0.5 | 0;
|
|
}
|
|
const lim = (v, l, h) => Math.max(Math.min(v, h), l);
|
|
function p2b(v) {
|
|
return lim(round(v * 2.55), 0, 255);
|
|
}
|
|
function b2p(v) {
|
|
return lim(round(v / 2.55), 0, 100);
|
|
}
|
|
function n2b(v) {
|
|
return lim(round(v * 255), 0, 255);
|
|
}
|
|
function b2n(v) {
|
|
return lim(round(v / 2.55) / 100, 0, 1);
|
|
}
|
|
function n2p(v) {
|
|
return lim(round(v * 100), 0, 100);
|
|
}
|
|
|
|
const map$1 = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15};
|
|
const hex = [...'0123456789ABCDEF'];
|
|
const h1 = b => hex[b & 0xF];
|
|
const h2 = b => hex[(b & 0xF0) >> 4] + hex[b & 0xF];
|
|
const eq = b => ((b & 0xF0) >> 4) === (b & 0xF);
|
|
const isShort = v => eq(v.r) && eq(v.g) && eq(v.b) && eq(v.a);
|
|
function hexParse(str) {
|
|
var len = str.length;
|
|
var ret;
|
|
if (str[0] === '#') {
|
|
if (len === 4 || len === 5) {
|
|
ret = {
|
|
r: 255 & map$1[str[1]] * 17,
|
|
g: 255 & map$1[str[2]] * 17,
|
|
b: 255 & map$1[str[3]] * 17,
|
|
a: len === 5 ? map$1[str[4]] * 17 : 255
|
|
};
|
|
} else if (len === 7 || len === 9) {
|
|
ret = {
|
|
r: map$1[str[1]] << 4 | map$1[str[2]],
|
|
g: map$1[str[3]] << 4 | map$1[str[4]],
|
|
b: map$1[str[5]] << 4 | map$1[str[6]],
|
|
a: len === 9 ? (map$1[str[7]] << 4 | map$1[str[8]]) : 255
|
|
};
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
const alpha = (a, f) => a < 255 ? f(a) : '';
|
|
function hexString(v) {
|
|
var f = isShort(v) ? h1 : h2;
|
|
return v
|
|
? '#' + f(v.r) + f(v.g) + f(v.b) + alpha(v.a, f)
|
|
: undefined;
|
|
}
|
|
|
|
const HUE_RE = /^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;
|
|
function hsl2rgbn(h, s, l) {
|
|
const a = s * Math.min(l, 1 - l);
|
|
const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
return [f(0), f(8), f(4)];
|
|
}
|
|
function hsv2rgbn(h, s, v) {
|
|
const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);
|
|
return [f(5), f(3), f(1)];
|
|
}
|
|
function hwb2rgbn(h, w, b) {
|
|
const rgb = hsl2rgbn(h, 1, 0.5);
|
|
let i;
|
|
if (w + b > 1) {
|
|
i = 1 / (w + b);
|
|
w *= i;
|
|
b *= i;
|
|
}
|
|
for (i = 0; i < 3; i++) {
|
|
rgb[i] *= 1 - w - b;
|
|
rgb[i] += w;
|
|
}
|
|
return rgb;
|
|
}
|
|
function hueValue(r, g, b, d, max) {
|
|
if (r === max) {
|
|
return ((g - b) / d) + (g < b ? 6 : 0);
|
|
}
|
|
if (g === max) {
|
|
return (b - r) / d + 2;
|
|
}
|
|
return (r - g) / d + 4;
|
|
}
|
|
function rgb2hsl(v) {
|
|
const range = 255;
|
|
const r = v.r / range;
|
|
const g = v.g / range;
|
|
const b = v.b / range;
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const l = (max + min) / 2;
|
|
let h, s, d;
|
|
if (max !== min) {
|
|
d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
h = hueValue(r, g, b, d, max);
|
|
h = h * 60 + 0.5;
|
|
}
|
|
return [h | 0, s || 0, l];
|
|
}
|
|
function calln(f, a, b, c) {
|
|
return (
|
|
Array.isArray(a)
|
|
? f(a[0], a[1], a[2])
|
|
: f(a, b, c)
|
|
).map(n2b);
|
|
}
|
|
function hsl2rgb(h, s, l) {
|
|
return calln(hsl2rgbn, h, s, l);
|
|
}
|
|
function hwb2rgb(h, w, b) {
|
|
return calln(hwb2rgbn, h, w, b);
|
|
}
|
|
function hsv2rgb(h, s, v) {
|
|
return calln(hsv2rgbn, h, s, v);
|
|
}
|
|
function hue(h) {
|
|
return (h % 360 + 360) % 360;
|
|
}
|
|
function hueParse(str) {
|
|
const m = HUE_RE.exec(str);
|
|
let a = 255;
|
|
let v;
|
|
if (!m) {
|
|
return;
|
|
}
|
|
if (m[5] !== v) {
|
|
a = m[6] ? p2b(+m[5]) : n2b(+m[5]);
|
|
}
|
|
const h = hue(+m[2]);
|
|
const p1 = +m[3] / 100;
|
|
const p2 = +m[4] / 100;
|
|
if (m[1] === 'hwb') {
|
|
v = hwb2rgb(h, p1, p2);
|
|
} else if (m[1] === 'hsv') {
|
|
v = hsv2rgb(h, p1, p2);
|
|
} else {
|
|
v = hsl2rgb(h, p1, p2);
|
|
}
|
|
return {
|
|
r: v[0],
|
|
g: v[1],
|
|
b: v[2],
|
|
a: a
|
|
};
|
|
}
|
|
function rotate(v, deg) {
|
|
var h = rgb2hsl(v);
|
|
h[0] = hue(h[0] + deg);
|
|
h = hsl2rgb(h);
|
|
v.r = h[0];
|
|
v.g = h[1];
|
|
v.b = h[2];
|
|
}
|
|
function hslString(v) {
|
|
if (!v) {
|
|
return;
|
|
}
|
|
const a = rgb2hsl(v);
|
|
const h = a[0];
|
|
const s = n2p(a[1]);
|
|
const l = n2p(a[2]);
|
|
return v.a < 255
|
|
? `hsla(${h}, ${s}%, ${l}%, ${b2n(v.a)})`
|
|
: `hsl(${h}, ${s}%, ${l}%)`;
|
|
}
|
|
|
|
const map = {
|
|
x: 'dark',
|
|
Z: 'light',
|
|
Y: 're',
|
|
X: 'blu',
|
|
W: 'gr',
|
|
V: 'medium',
|
|
U: 'slate',
|
|
A: 'ee',
|
|
T: 'ol',
|
|
S: 'or',
|
|
B: 'ra',
|
|
C: 'lateg',
|
|
D: 'ights',
|
|
R: 'in',
|
|
Q: 'turquois',
|
|
E: 'hi',
|
|
P: 'ro',
|
|
O: 'al',
|
|
N: 'le',
|
|
M: 'de',
|
|
L: 'yello',
|
|
F: 'en',
|
|
K: 'ch',
|
|
G: 'arks',
|
|
H: 'ea',
|
|
I: 'ightg',
|
|
J: 'wh'
|
|
};
|
|
const names$1 = {
|
|
OiceXe: 'f0f8ff',
|
|
antiquewEte: 'faebd7',
|
|
aqua: 'ffff',
|
|
aquamarRe: '7fffd4',
|
|
azuY: 'f0ffff',
|
|
beige: 'f5f5dc',
|
|
bisque: 'ffe4c4',
|
|
black: '0',
|
|
blanKedOmond: 'ffebcd',
|
|
Xe: 'ff',
|
|
XeviTet: '8a2be2',
|
|
bPwn: 'a52a2a',
|
|
burlywood: 'deb887',
|
|
caMtXe: '5f9ea0',
|
|
KartYuse: '7fff00',
|
|
KocTate: 'd2691e',
|
|
cSO: 'ff7f50',
|
|
cSnflowerXe: '6495ed',
|
|
cSnsilk: 'fff8dc',
|
|
crimson: 'dc143c',
|
|
cyan: 'ffff',
|
|
xXe: '8b',
|
|
xcyan: '8b8b',
|
|
xgTMnPd: 'b8860b',
|
|
xWay: 'a9a9a9',
|
|
xgYF: '6400',
|
|
xgYy: 'a9a9a9',
|
|
xkhaki: 'bdb76b',
|
|
xmagFta: '8b008b',
|
|
xTivegYF: '556b2f',
|
|
xSange: 'ff8c00',
|
|
xScEd: '9932cc',
|
|
xYd: '8b0000',
|
|
xsOmon: 'e9967a',
|
|
xsHgYF: '8fbc8f',
|
|
xUXe: '483d8b',
|
|
xUWay: '2f4f4f',
|
|
xUgYy: '2f4f4f',
|
|
xQe: 'ced1',
|
|
xviTet: '9400d3',
|
|
dAppRk: 'ff1493',
|
|
dApskyXe: 'bfff',
|
|
dimWay: '696969',
|
|
dimgYy: '696969',
|
|
dodgerXe: '1e90ff',
|
|
fiYbrick: 'b22222',
|
|
flSOwEte: 'fffaf0',
|
|
foYstWAn: '228b22',
|
|
fuKsia: 'ff00ff',
|
|
gaRsbSo: 'dcdcdc',
|
|
ghostwEte: 'f8f8ff',
|
|
gTd: 'ffd700',
|
|
gTMnPd: 'daa520',
|
|
Way: '808080',
|
|
gYF: '8000',
|
|
gYFLw: 'adff2f',
|
|
gYy: '808080',
|
|
honeyMw: 'f0fff0',
|
|
hotpRk: 'ff69b4',
|
|
RdianYd: 'cd5c5c',
|
|
Rdigo: '4b0082',
|
|
ivSy: 'fffff0',
|
|
khaki: 'f0e68c',
|
|
lavFMr: 'e6e6fa',
|
|
lavFMrXsh: 'fff0f5',
|
|
lawngYF: '7cfc00',
|
|
NmoncEffon: 'fffacd',
|
|
ZXe: 'add8e6',
|
|
ZcSO: 'f08080',
|
|
Zcyan: 'e0ffff',
|
|
ZgTMnPdLw: 'fafad2',
|
|
ZWay: 'd3d3d3',
|
|
ZgYF: '90ee90',
|
|
ZgYy: 'd3d3d3',
|
|
ZpRk: 'ffb6c1',
|
|
ZsOmon: 'ffa07a',
|
|
ZsHgYF: '20b2aa',
|
|
ZskyXe: '87cefa',
|
|
ZUWay: '778899',
|
|
ZUgYy: '778899',
|
|
ZstAlXe: 'b0c4de',
|
|
ZLw: 'ffffe0',
|
|
lime: 'ff00',
|
|
limegYF: '32cd32',
|
|
lRF: 'faf0e6',
|
|
magFta: 'ff00ff',
|
|
maPon: '800000',
|
|
VaquamarRe: '66cdaa',
|
|
VXe: 'cd',
|
|
VScEd: 'ba55d3',
|
|
VpurpN: '9370db',
|
|
VsHgYF: '3cb371',
|
|
VUXe: '7b68ee',
|
|
VsprRggYF: 'fa9a',
|
|
VQe: '48d1cc',
|
|
VviTetYd: 'c71585',
|
|
midnightXe: '191970',
|
|
mRtcYam: 'f5fffa',
|
|
mistyPse: 'ffe4e1',
|
|
moccasR: 'ffe4b5',
|
|
navajowEte: 'ffdead',
|
|
navy: '80',
|
|
Tdlace: 'fdf5e6',
|
|
Tive: '808000',
|
|
TivedBb: '6b8e23',
|
|
Sange: 'ffa500',
|
|
SangeYd: 'ff4500',
|
|
ScEd: 'da70d6',
|
|
pOegTMnPd: 'eee8aa',
|
|
pOegYF: '98fb98',
|
|
pOeQe: 'afeeee',
|
|
pOeviTetYd: 'db7093',
|
|
papayawEp: 'ffefd5',
|
|
pHKpuff: 'ffdab9',
|
|
peru: 'cd853f',
|
|
pRk: 'ffc0cb',
|
|
plum: 'dda0dd',
|
|
powMrXe: 'b0e0e6',
|
|
purpN: '800080',
|
|
YbeccapurpN: '663399',
|
|
Yd: 'ff0000',
|
|
Psybrown: 'bc8f8f',
|
|
PyOXe: '4169e1',
|
|
saddNbPwn: '8b4513',
|
|
sOmon: 'fa8072',
|
|
sandybPwn: 'f4a460',
|
|
sHgYF: '2e8b57',
|
|
sHshell: 'fff5ee',
|
|
siFna: 'a0522d',
|
|
silver: 'c0c0c0',
|
|
skyXe: '87ceeb',
|
|
UXe: '6a5acd',
|
|
UWay: '708090',
|
|
UgYy: '708090',
|
|
snow: 'fffafa',
|
|
sprRggYF: 'ff7f',
|
|
stAlXe: '4682b4',
|
|
tan: 'd2b48c',
|
|
teO: '8080',
|
|
tEstN: 'd8bfd8',
|
|
tomato: 'ff6347',
|
|
Qe: '40e0d0',
|
|
viTet: 'ee82ee',
|
|
JHt: 'f5deb3',
|
|
wEte: 'ffffff',
|
|
wEtesmoke: 'f5f5f5',
|
|
Lw: 'ffff00',
|
|
LwgYF: '9acd32'
|
|
};
|
|
function unpack() {
|
|
const unpacked = {};
|
|
const keys = Object.keys(names$1);
|
|
const tkeys = Object.keys(map);
|
|
let i, j, k, ok, nk;
|
|
for (i = 0; i < keys.length; i++) {
|
|
ok = nk = keys[i];
|
|
for (j = 0; j < tkeys.length; j++) {
|
|
k = tkeys[j];
|
|
nk = nk.replace(k, map[k]);
|
|
}
|
|
k = parseInt(names$1[ok], 16);
|
|
unpacked[nk] = [k >> 16 & 0xFF, k >> 8 & 0xFF, k & 0xFF];
|
|
}
|
|
return unpacked;
|
|
}
|
|
|
|
let names;
|
|
function nameParse(str) {
|
|
if (!names) {
|
|
names = unpack();
|
|
names.transparent = [0, 0, 0, 0];
|
|
}
|
|
const a = names[str.toLowerCase()];
|
|
return a && {
|
|
r: a[0],
|
|
g: a[1],
|
|
b: a[2],
|
|
a: a.length === 4 ? a[3] : 255
|
|
};
|
|
}
|
|
|
|
const RGB_RE = /^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;
|
|
function rgbParse(str) {
|
|
const m = RGB_RE.exec(str);
|
|
let a = 255;
|
|
let r, g, b;
|
|
if (!m) {
|
|
return;
|
|
}
|
|
if (m[7] !== r) {
|
|
const v = +m[7];
|
|
a = m[8] ? p2b(v) : lim(v * 255, 0, 255);
|
|
}
|
|
r = +m[1];
|
|
g = +m[3];
|
|
b = +m[5];
|
|
r = 255 & (m[2] ? p2b(r) : lim(r, 0, 255));
|
|
g = 255 & (m[4] ? p2b(g) : lim(g, 0, 255));
|
|
b = 255 & (m[6] ? p2b(b) : lim(b, 0, 255));
|
|
return {
|
|
r: r,
|
|
g: g,
|
|
b: b,
|
|
a: a
|
|
};
|
|
}
|
|
function rgbString(v) {
|
|
return v && (
|
|
v.a < 255
|
|
? `rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})`
|
|
: `rgb(${v.r}, ${v.g}, ${v.b})`
|
|
);
|
|
}
|
|
|
|
const to = v => v <= 0.0031308 ? v * 12.92 : Math.pow(v, 1.0 / 2.4) * 1.055 - 0.055;
|
|
const from = v => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
function interpolate(rgb1, rgb2, t) {
|
|
const r = from(b2n(rgb1.r));
|
|
const g = from(b2n(rgb1.g));
|
|
const b = from(b2n(rgb1.b));
|
|
return {
|
|
r: n2b(to(r + t * (from(b2n(rgb2.r)) - r))),
|
|
g: n2b(to(g + t * (from(b2n(rgb2.g)) - g))),
|
|
b: n2b(to(b + t * (from(b2n(rgb2.b)) - b))),
|
|
a: rgb1.a + t * (rgb2.a - rgb1.a)
|
|
};
|
|
}
|
|
|
|
function modHSL(v, i, ratio) {
|
|
if (v) {
|
|
let tmp = rgb2hsl(v);
|
|
tmp[i] = Math.max(0, Math.min(tmp[i] + tmp[i] * ratio, i === 0 ? 360 : 1));
|
|
tmp = hsl2rgb(tmp);
|
|
v.r = tmp[0];
|
|
v.g = tmp[1];
|
|
v.b = tmp[2];
|
|
}
|
|
}
|
|
function clone(v, proto) {
|
|
return v ? Object.assign(proto || {}, v) : v;
|
|
}
|
|
function fromObject(input) {
|
|
var v = {r: 0, g: 0, b: 0, a: 255};
|
|
if (Array.isArray(input)) {
|
|
if (input.length >= 3) {
|
|
v = {r: input[0], g: input[1], b: input[2], a: 255};
|
|
if (input.length > 3) {
|
|
v.a = n2b(input[3]);
|
|
}
|
|
}
|
|
} else {
|
|
v = clone(input, {r: 0, g: 0, b: 0, a: 1});
|
|
v.a = n2b(v.a);
|
|
}
|
|
return v;
|
|
}
|
|
function functionParse(str) {
|
|
if (str.charAt(0) === 'r') {
|
|
return rgbParse(str);
|
|
}
|
|
return hueParse(str);
|
|
}
|
|
class Color {
|
|
constructor(input) {
|
|
if (input instanceof Color) {
|
|
return input;
|
|
}
|
|
const type = typeof input;
|
|
let v;
|
|
if (type === 'object') {
|
|
v = fromObject(input);
|
|
} else if (type === 'string') {
|
|
v = hexParse(input) || nameParse(input) || functionParse(input);
|
|
}
|
|
this._rgb = v;
|
|
this._valid = !!v;
|
|
}
|
|
get valid() {
|
|
return this._valid;
|
|
}
|
|
get rgb() {
|
|
var v = clone(this._rgb);
|
|
if (v) {
|
|
v.a = b2n(v.a);
|
|
}
|
|
return v;
|
|
}
|
|
set rgb(obj) {
|
|
this._rgb = fromObject(obj);
|
|
}
|
|
rgbString() {
|
|
return this._valid ? rgbString(this._rgb) : undefined;
|
|
}
|
|
hexString() {
|
|
return this._valid ? hexString(this._rgb) : undefined;
|
|
}
|
|
hslString() {
|
|
return this._valid ? hslString(this._rgb) : undefined;
|
|
}
|
|
mix(color, weight) {
|
|
if (color) {
|
|
const c1 = this.rgb;
|
|
const c2 = color.rgb;
|
|
let w2;
|
|
const p = weight === w2 ? 0.5 : weight;
|
|
const w = 2 * p - 1;
|
|
const a = c1.a - c2.a;
|
|
const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0;
|
|
w2 = 1 - w1;
|
|
c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5;
|
|
c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5;
|
|
c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5;
|
|
c1.a = p * c1.a + (1 - p) * c2.a;
|
|
this.rgb = c1;
|
|
}
|
|
return this;
|
|
}
|
|
interpolate(color, t) {
|
|
if (color) {
|
|
this._rgb = interpolate(this._rgb, color._rgb, t);
|
|
}
|
|
return this;
|
|
}
|
|
clone() {
|
|
return new Color(this.rgb);
|
|
}
|
|
alpha(a) {
|
|
this._rgb.a = n2b(a);
|
|
return this;
|
|
}
|
|
clearer(ratio) {
|
|
const rgb = this._rgb;
|
|
rgb.a *= 1 - ratio;
|
|
return this;
|
|
}
|
|
greyscale() {
|
|
const rgb = this._rgb;
|
|
const val = round(rgb.r * 0.3 + rgb.g * 0.59 + rgb.b * 0.11);
|
|
rgb.r = rgb.g = rgb.b = val;
|
|
return this;
|
|
}
|
|
opaquer(ratio) {
|
|
const rgb = this._rgb;
|
|
rgb.a *= 1 + ratio;
|
|
return this;
|
|
}
|
|
negate() {
|
|
const v = this._rgb;
|
|
v.r = 255 - v.r;
|
|
v.g = 255 - v.g;
|
|
v.b = 255 - v.b;
|
|
return this;
|
|
}
|
|
lighten(ratio) {
|
|
modHSL(this._rgb, 2, ratio);
|
|
return this;
|
|
}
|
|
darken(ratio) {
|
|
modHSL(this._rgb, 2, -ratio);
|
|
return this;
|
|
}
|
|
saturate(ratio) {
|
|
modHSL(this._rgb, 1, ratio);
|
|
return this;
|
|
}
|
|
desaturate(ratio) {
|
|
modHSL(this._rgb, 1, -ratio);
|
|
return this;
|
|
}
|
|
rotate(deg) {
|
|
rotate(this._rgb, deg);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
function index_esm(input) {
|
|
return new Color(input);
|
|
}
|
|
|
|
|
|
|
|
;// ../../node_modules/chart.js/dist/chunks/helpers.dataset.js
|
|
/*!
|
|
* Chart.js v4.5.1
|
|
* https://www.chartjs.org
|
|
* (c) 2025 Chart.js Contributors
|
|
* Released under the MIT License
|
|
*/
|
|
|
|
|
|
/**
|
|
* @namespace Chart.helpers
|
|
*/ /**
|
|
* An empty function that can be used, for example, for optional callback.
|
|
*/ function noop() {
|
|
/* noop */ }
|
|
/**
|
|
* Returns a unique id, sequentially generated from a global variable.
|
|
*/ const uid = (()=>{
|
|
let id = 0;
|
|
return ()=>id++;
|
|
})();
|
|
/**
|
|
* Returns true if `value` is neither null nor undefined, else returns false.
|
|
* @param value - The value to test.
|
|
* @since 2.7.0
|
|
*/ function isNullOrUndef(value) {
|
|
return value === null || value === undefined;
|
|
}
|
|
/**
|
|
* Returns true if `value` is an array (including typed arrays), else returns false.
|
|
* @param value - The value to test.
|
|
* @function
|
|
*/ function isArray(value) {
|
|
if (Array.isArray && Array.isArray(value)) {
|
|
return true;
|
|
}
|
|
const type = Object.prototype.toString.call(value);
|
|
if (type.slice(0, 7) === '[object' && type.slice(-6) === 'Array]') {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Returns true if `value` is an object (excluding null), else returns false.
|
|
* @param value - The value to test.
|
|
* @since 2.7.0
|
|
*/ function isObject(value) {
|
|
return value !== null && Object.prototype.toString.call(value) === '[object Object]';
|
|
}
|
|
/**
|
|
* Returns true if `value` is a finite number, else returns false
|
|
* @param value - The value to test.
|
|
*/ function isNumberFinite(value) {
|
|
return (typeof value === 'number' || value instanceof Number) && isFinite(+value);
|
|
}
|
|
/**
|
|
* Returns `value` if finite, else returns `defaultValue`.
|
|
* @param value - The value to return if defined.
|
|
* @param defaultValue - The value to return if `value` is not finite.
|
|
*/ function finiteOrDefault(value, defaultValue) {
|
|
return isNumberFinite(value) ? value : defaultValue;
|
|
}
|
|
/**
|
|
* Returns `value` if defined, else returns `defaultValue`.
|
|
* @param value - The value to return if defined.
|
|
* @param defaultValue - The value to return if `value` is undefined.
|
|
*/ function valueOrDefault(value, defaultValue) {
|
|
return typeof value === 'undefined' ? defaultValue : value;
|
|
}
|
|
const toPercentage = (value, dimension)=>typeof value === 'string' && value.endsWith('%') ? parseFloat(value) / 100 : +value / dimension;
|
|
const toDimension = (value, dimension)=>typeof value === 'string' && value.endsWith('%') ? parseFloat(value) / 100 * dimension : +value;
|
|
/**
|
|
* Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the
|
|
* value returned by `fn`. If `fn` is not a function, this method returns undefined.
|
|
* @param fn - The function to call.
|
|
* @param args - The arguments with which `fn` should be called.
|
|
* @param [thisArg] - The value of `this` provided for the call to `fn`.
|
|
*/ function callback(fn, args, thisArg) {
|
|
if (fn && typeof fn.call === 'function') {
|
|
return fn.apply(thisArg, args);
|
|
}
|
|
}
|
|
function each(loopable, fn, thisArg, reverse) {
|
|
let i, len, keys;
|
|
if (isArray(loopable)) {
|
|
len = loopable.length;
|
|
if (reverse) {
|
|
for(i = len - 1; i >= 0; i--){
|
|
fn.call(thisArg, loopable[i], i);
|
|
}
|
|
} else {
|
|
for(i = 0; i < len; i++){
|
|
fn.call(thisArg, loopable[i], i);
|
|
}
|
|
}
|
|
} else if (isObject(loopable)) {
|
|
keys = Object.keys(loopable);
|
|
len = keys.length;
|
|
for(i = 0; i < len; i++){
|
|
fn.call(thisArg, loopable[keys[i]], keys[i]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Returns true if the `a0` and `a1` arrays have the same content, else returns false.
|
|
* @param a0 - The array to compare
|
|
* @param a1 - The array to compare
|
|
* @private
|
|
*/ function _elementsEqual(a0, a1) {
|
|
let i, ilen, v0, v1;
|
|
if (!a0 || !a1 || a0.length !== a1.length) {
|
|
return false;
|
|
}
|
|
for(i = 0, ilen = a0.length; i < ilen; ++i){
|
|
v0 = a0[i];
|
|
v1 = a1[i];
|
|
if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Returns a deep copy of `source` without keeping references on objects and arrays.
|
|
* @param source - The value to clone.
|
|
*/ function helpers_dataset_clone(source) {
|
|
if (isArray(source)) {
|
|
return source.map(helpers_dataset_clone);
|
|
}
|
|
if (isObject(source)) {
|
|
const target = Object.create(null);
|
|
const keys = Object.keys(source);
|
|
const klen = keys.length;
|
|
let k = 0;
|
|
for(; k < klen; ++k){
|
|
target[keys[k]] = helpers_dataset_clone(source[keys[k]]);
|
|
}
|
|
return target;
|
|
}
|
|
return source;
|
|
}
|
|
function isValidKey(key) {
|
|
return [
|
|
'__proto__',
|
|
'prototype',
|
|
'constructor'
|
|
].indexOf(key) === -1;
|
|
}
|
|
/**
|
|
* The default merger when Chart.helpers.merge is called without merger option.
|
|
* Note(SB): also used by mergeConfig and mergeScaleConfig as fallback.
|
|
* @private
|
|
*/ function _merger(key, target, source, options) {
|
|
if (!isValidKey(key)) {
|
|
return;
|
|
}
|
|
const tval = target[key];
|
|
const sval = source[key];
|
|
if (isObject(tval) && isObject(sval)) {
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
merge(tval, sval, options);
|
|
} else {
|
|
target[key] = helpers_dataset_clone(sval);
|
|
}
|
|
}
|
|
function merge(target, source, options) {
|
|
const sources = isArray(source) ? source : [
|
|
source
|
|
];
|
|
const ilen = sources.length;
|
|
if (!isObject(target)) {
|
|
return target;
|
|
}
|
|
options = options || {};
|
|
const merger = options.merger || _merger;
|
|
let current;
|
|
for(let i = 0; i < ilen; ++i){
|
|
current = sources[i];
|
|
if (!isObject(current)) {
|
|
continue;
|
|
}
|
|
const keys = Object.keys(current);
|
|
for(let k = 0, klen = keys.length; k < klen; ++k){
|
|
merger(keys[k], target, current, options);
|
|
}
|
|
}
|
|
return target;
|
|
}
|
|
function mergeIf(target, source) {
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
return merge(target, source, {
|
|
merger: _mergerIf
|
|
});
|
|
}
|
|
/**
|
|
* Merges source[key] in target[key] only if target[key] is undefined.
|
|
* @private
|
|
*/ function _mergerIf(key, target, source) {
|
|
if (!isValidKey(key)) {
|
|
return;
|
|
}
|
|
const tval = target[key];
|
|
const sval = source[key];
|
|
if (isObject(tval) && isObject(sval)) {
|
|
mergeIf(tval, sval);
|
|
} else if (!Object.prototype.hasOwnProperty.call(target, key)) {
|
|
target[key] = helpers_dataset_clone(sval);
|
|
}
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _deprecated(scope, value, previous, current) {
|
|
if (value !== undefined) {
|
|
console.warn(scope + ': "' + previous + '" is deprecated. Please use "' + current + '" instead');
|
|
}
|
|
}
|
|
// resolveObjectKey resolver cache
|
|
const keyResolvers = {
|
|
// Chart.helpers.core resolveObjectKey should resolve empty key to root object
|
|
'': (v)=>v,
|
|
// default resolvers
|
|
x: (o)=>o.x,
|
|
y: (o)=>o.y
|
|
};
|
|
/**
|
|
* @private
|
|
*/ function _splitKey(key) {
|
|
const parts = key.split('.');
|
|
const keys = [];
|
|
let tmp = '';
|
|
for (const part of parts){
|
|
tmp += part;
|
|
if (tmp.endsWith('\\')) {
|
|
tmp = tmp.slice(0, -1) + '.';
|
|
} else {
|
|
keys.push(tmp);
|
|
tmp = '';
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
function _getKeyResolver(key) {
|
|
const keys = _splitKey(key);
|
|
return (obj)=>{
|
|
for (const k of keys){
|
|
if (k === '') {
|
|
break;
|
|
}
|
|
obj = obj && obj[k];
|
|
}
|
|
return obj;
|
|
};
|
|
}
|
|
function resolveObjectKey(obj, key) {
|
|
const resolver = keyResolvers[key] || (keyResolvers[key] = _getKeyResolver(key));
|
|
return resolver(obj);
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _capitalize(str) {
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
}
|
|
const defined = (value)=>typeof value !== 'undefined';
|
|
const isFunction = (value)=>typeof value === 'function';
|
|
// Adapted from https://stackoverflow.com/questions/31128855/comparing-ecma6-sets-for-equality#31129384
|
|
const setsEqual = (a, b)=>{
|
|
if (a.size !== b.size) {
|
|
return false;
|
|
}
|
|
for (const item of a){
|
|
if (!b.has(item)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
/**
|
|
* @param e - The event
|
|
* @private
|
|
*/ function _isClickEvent(e) {
|
|
return e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu';
|
|
}
|
|
|
|
/**
|
|
* @alias Chart.helpers.math
|
|
* @namespace
|
|
*/ const PI = Math.PI;
|
|
const TAU = 2 * PI;
|
|
const PITAU = TAU + PI;
|
|
const INFINITY = Number.POSITIVE_INFINITY;
|
|
const RAD_PER_DEG = PI / 180;
|
|
const HALF_PI = PI / 2;
|
|
const QUARTER_PI = PI / 4;
|
|
const TWO_THIRDS_PI = PI * 2 / 3;
|
|
const log10 = Math.log10;
|
|
const sign = Math.sign;
|
|
function almostEquals(x, y, epsilon) {
|
|
return Math.abs(x - y) < epsilon;
|
|
}
|
|
/**
|
|
* Implementation of the nice number algorithm used in determining where axis labels will go
|
|
*/ function niceNum(range) {
|
|
const roundedRange = Math.round(range);
|
|
range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range;
|
|
const niceRange = Math.pow(10, Math.floor(log10(range)));
|
|
const fraction = range / niceRange;
|
|
const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;
|
|
return niceFraction * niceRange;
|
|
}
|
|
/**
|
|
* Returns an array of factors sorted from 1 to sqrt(value)
|
|
* @private
|
|
*/ function _factorize(value) {
|
|
const result = [];
|
|
const sqrt = Math.sqrt(value);
|
|
let i;
|
|
for(i = 1; i < sqrt; i++){
|
|
if (value % i === 0) {
|
|
result.push(i);
|
|
result.push(value / i);
|
|
}
|
|
}
|
|
if (sqrt === (sqrt | 0)) {
|
|
result.push(sqrt);
|
|
}
|
|
result.sort((a, b)=>a - b).pop();
|
|
return result;
|
|
}
|
|
/**
|
|
* Verifies that attempting to coerce n to string or number won't throw a TypeError.
|
|
*/ function isNonPrimitive(n) {
|
|
return typeof n === 'symbol' || typeof n === 'object' && n !== null && !(Symbol.toPrimitive in n || 'toString' in n || 'valueOf' in n);
|
|
}
|
|
function isNumber(n) {
|
|
return !isNonPrimitive(n) && !isNaN(parseFloat(n)) && isFinite(n);
|
|
}
|
|
function almostWhole(x, epsilon) {
|
|
const rounded = Math.round(x);
|
|
return rounded - epsilon <= x && rounded + epsilon >= x;
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _setMinAndMaxByKey(array, target, property) {
|
|
let i, ilen, value;
|
|
for(i = 0, ilen = array.length; i < ilen; i++){
|
|
value = array[i][property];
|
|
if (!isNaN(value)) {
|
|
target.min = Math.min(target.min, value);
|
|
target.max = Math.max(target.max, value);
|
|
}
|
|
}
|
|
}
|
|
function toRadians(degrees) {
|
|
return degrees * (PI / 180);
|
|
}
|
|
function toDegrees(radians) {
|
|
return radians * (180 / PI);
|
|
}
|
|
/**
|
|
* Returns the number of decimal places
|
|
* i.e. the number of digits after the decimal point, of the value of this Number.
|
|
* @param x - A number.
|
|
* @returns The number of decimal places.
|
|
* @private
|
|
*/ function _decimalPlaces(x) {
|
|
if (!isNumberFinite(x)) {
|
|
return;
|
|
}
|
|
let e = 1;
|
|
let p = 0;
|
|
while(Math.round(x * e) / e !== x){
|
|
e *= 10;
|
|
p++;
|
|
}
|
|
return p;
|
|
}
|
|
// Gets the angle from vertical upright to the point about a centre.
|
|
function getAngleFromPoint(centrePoint, anglePoint) {
|
|
const distanceFromXCenter = anglePoint.x - centrePoint.x;
|
|
const distanceFromYCenter = anglePoint.y - centrePoint.y;
|
|
const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
|
|
let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter);
|
|
if (angle < -0.5 * PI) {
|
|
angle += TAU; // make sure the returned angle is in the range of (-PI/2, 3PI/2]
|
|
}
|
|
return {
|
|
angle,
|
|
distance: radialDistanceFromCenter
|
|
};
|
|
}
|
|
function distanceBetweenPoints(pt1, pt2) {
|
|
return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
|
|
}
|
|
/**
|
|
* Shortest distance between angles, in either direction.
|
|
* @private
|
|
*/ function _angleDiff(a, b) {
|
|
return (a - b + PITAU) % TAU - PI;
|
|
}
|
|
/**
|
|
* Normalize angle to be between 0 and 2*PI
|
|
* @private
|
|
*/ function _normalizeAngle(a) {
|
|
return (a % TAU + TAU) % TAU;
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _angleBetween(angle, start, end, sameAngleIsFullCircle) {
|
|
const a = _normalizeAngle(angle);
|
|
const s = _normalizeAngle(start);
|
|
const e = _normalizeAngle(end);
|
|
const angleToStart = _normalizeAngle(s - a);
|
|
const angleToEnd = _normalizeAngle(e - a);
|
|
const startToAngle = _normalizeAngle(a - s);
|
|
const endToAngle = _normalizeAngle(a - e);
|
|
return a === s || a === e || sameAngleIsFullCircle && s === e || angleToStart > angleToEnd && startToAngle < endToAngle;
|
|
}
|
|
/**
|
|
* Limit `value` between `min` and `max`
|
|
* @param value
|
|
* @param min
|
|
* @param max
|
|
* @private
|
|
*/ function _limitValue(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
/**
|
|
* @param {number} value
|
|
* @private
|
|
*/ function _int16Range(value) {
|
|
return _limitValue(value, -32768, 32767);
|
|
}
|
|
/**
|
|
* @param value
|
|
* @param start
|
|
* @param end
|
|
* @param [epsilon]
|
|
* @private
|
|
*/ function _isBetween(value, start, end, epsilon = 1e-6) {
|
|
return value >= Math.min(start, end) - epsilon && value <= Math.max(start, end) + epsilon;
|
|
}
|
|
|
|
function _lookup(table, value, cmp) {
|
|
cmp = cmp || ((index)=>table[index] < value);
|
|
let hi = table.length - 1;
|
|
let lo = 0;
|
|
let mid;
|
|
while(hi - lo > 1){
|
|
mid = lo + hi >> 1;
|
|
if (cmp(mid)) {
|
|
lo = mid;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
return {
|
|
lo,
|
|
hi
|
|
};
|
|
}
|
|
/**
|
|
* Binary search
|
|
* @param table - the table search. must be sorted!
|
|
* @param key - property name for the value in each entry
|
|
* @param value - value to find
|
|
* @param last - lookup last index
|
|
* @private
|
|
*/ const _lookupByKey = (table, key, value, last)=>_lookup(table, value, last ? (index)=>{
|
|
const ti = table[index][key];
|
|
return ti < value || ti === value && table[index + 1][key] === value;
|
|
} : (index)=>table[index][key] < value);
|
|
/**
|
|
* Reverse binary search
|
|
* @param table - the table search. must be sorted!
|
|
* @param key - property name for the value in each entry
|
|
* @param value - value to find
|
|
* @private
|
|
*/ const _rlookupByKey = (table, key, value)=>_lookup(table, value, (index)=>table[index][key] >= value);
|
|
/**
|
|
* Return subset of `values` between `min` and `max` inclusive.
|
|
* Values are assumed to be in sorted order.
|
|
* @param values - sorted array of values
|
|
* @param min - min value
|
|
* @param max - max value
|
|
*/ function _filterBetween(values, min, max) {
|
|
let start = 0;
|
|
let end = values.length;
|
|
while(start < end && values[start] < min){
|
|
start++;
|
|
}
|
|
while(end > start && values[end - 1] > max){
|
|
end--;
|
|
}
|
|
return start > 0 || end < values.length ? values.slice(start, end) : values;
|
|
}
|
|
const arrayEvents = [
|
|
'push',
|
|
'pop',
|
|
'shift',
|
|
'splice',
|
|
'unshift'
|
|
];
|
|
function listenArrayEvents(array, listener) {
|
|
if (array._chartjs) {
|
|
array._chartjs.listeners.push(listener);
|
|
return;
|
|
}
|
|
Object.defineProperty(array, '_chartjs', {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: {
|
|
listeners: [
|
|
listener
|
|
]
|
|
}
|
|
});
|
|
arrayEvents.forEach((key)=>{
|
|
const method = '_onData' + _capitalize(key);
|
|
const base = array[key];
|
|
Object.defineProperty(array, key, {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value (...args) {
|
|
const res = base.apply(this, args);
|
|
array._chartjs.listeners.forEach((object)=>{
|
|
if (typeof object[method] === 'function') {
|
|
object[method](...args);
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
function unlistenArrayEvents(array, listener) {
|
|
const stub = array._chartjs;
|
|
if (!stub) {
|
|
return;
|
|
}
|
|
const listeners = stub.listeners;
|
|
const index = listeners.indexOf(listener);
|
|
if (index !== -1) {
|
|
listeners.splice(index, 1);
|
|
}
|
|
if (listeners.length > 0) {
|
|
return;
|
|
}
|
|
arrayEvents.forEach((key)=>{
|
|
delete array[key];
|
|
});
|
|
delete array._chartjs;
|
|
}
|
|
/**
|
|
* @param items
|
|
*/ function _arrayUnique(items) {
|
|
const set = new Set(items);
|
|
if (set.size === items.length) {
|
|
return items;
|
|
}
|
|
return Array.from(set);
|
|
}
|
|
|
|
function fontString(pixelSize, fontStyle, fontFamily) {
|
|
return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;
|
|
}
|
|
/**
|
|
* Request animation polyfill
|
|
*/ const requestAnimFrame = function() {
|
|
if (typeof window === 'undefined') {
|
|
return function(callback) {
|
|
return callback();
|
|
};
|
|
}
|
|
return window.requestAnimationFrame;
|
|
}();
|
|
/**
|
|
* Throttles calling `fn` once per animation frame
|
|
* Latest arguments are used on the actual call
|
|
*/ function throttled(fn, thisArg) {
|
|
let argsToUse = [];
|
|
let ticking = false;
|
|
return function(...args) {
|
|
// Save the args for use later
|
|
argsToUse = args;
|
|
if (!ticking) {
|
|
ticking = true;
|
|
requestAnimFrame.call(window, ()=>{
|
|
ticking = false;
|
|
fn.apply(thisArg, argsToUse);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Debounces calling `fn` for `delay` ms
|
|
*/ function debounce(fn, delay) {
|
|
let timeout;
|
|
return function(...args) {
|
|
if (delay) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(fn, delay, args);
|
|
} else {
|
|
fn.apply(this, args);
|
|
}
|
|
return delay;
|
|
};
|
|
}
|
|
/**
|
|
* Converts 'start' to 'left', 'end' to 'right' and others to 'center'
|
|
* @private
|
|
*/ const _toLeftRightCenter = (align)=>align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';
|
|
/**
|
|
* Returns `start`, `end` or `(start + end) / 2` depending on `align`. Defaults to `center`
|
|
* @private
|
|
*/ const _alignStartEnd = (align, start, end)=>align === 'start' ? start : align === 'end' ? end : (start + end) / 2;
|
|
/**
|
|
* Returns `left`, `right` or `(left + right) / 2` depending on `align`. Defaults to `left`
|
|
* @private
|
|
*/ const _textX = (align, left, right, rtl)=>{
|
|
const check = rtl ? 'left' : 'right';
|
|
return align === check ? right : align === 'center' ? (left + right) / 2 : left;
|
|
};
|
|
/**
|
|
* Return start and count of visible points.
|
|
* @private
|
|
*/ function _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled) {
|
|
const pointCount = points.length;
|
|
let start = 0;
|
|
let count = pointCount;
|
|
if (meta._sorted) {
|
|
const { iScale , vScale , _parsed } = meta;
|
|
const spanGaps = meta.dataset ? meta.dataset.options ? meta.dataset.options.spanGaps : null : null;
|
|
const axis = iScale.axis;
|
|
const { min , max , minDefined , maxDefined } = iScale.getUserBounds();
|
|
if (minDefined) {
|
|
start = Math.min(// @ts-expect-error Need to type _parsed
|
|
_lookupByKey(_parsed, axis, min).lo, // @ts-expect-error Need to fix types on _lookupByKey
|
|
animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo);
|
|
if (spanGaps) {
|
|
const distanceToDefinedLo = _parsed.slice(0, start + 1).reverse().findIndex((point)=>!isNullOrUndef(point[vScale.axis]));
|
|
start -= Math.max(0, distanceToDefinedLo);
|
|
}
|
|
start = _limitValue(start, 0, pointCount - 1);
|
|
}
|
|
if (maxDefined) {
|
|
let end = Math.max(// @ts-expect-error Need to type _parsed
|
|
_lookupByKey(_parsed, iScale.axis, max, true).hi + 1, // @ts-expect-error Need to fix types on _lookupByKey
|
|
animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1);
|
|
if (spanGaps) {
|
|
const distanceToDefinedHi = _parsed.slice(end - 1).findIndex((point)=>!isNullOrUndef(point[vScale.axis]));
|
|
end += Math.max(0, distanceToDefinedHi);
|
|
}
|
|
count = _limitValue(end, start, pointCount) - start;
|
|
} else {
|
|
count = pointCount - start;
|
|
}
|
|
}
|
|
return {
|
|
start,
|
|
count
|
|
};
|
|
}
|
|
/**
|
|
* Checks if the scale ranges have changed.
|
|
* @param {object} meta - dataset meta.
|
|
* @returns {boolean}
|
|
* @private
|
|
*/ function _scaleRangesChanged(meta) {
|
|
const { xScale , yScale , _scaleRanges } = meta;
|
|
const newRanges = {
|
|
xmin: xScale.min,
|
|
xmax: xScale.max,
|
|
ymin: yScale.min,
|
|
ymax: yScale.max
|
|
};
|
|
if (!_scaleRanges) {
|
|
meta._scaleRanges = newRanges;
|
|
return true;
|
|
}
|
|
const changed = _scaleRanges.xmin !== xScale.min || _scaleRanges.xmax !== xScale.max || _scaleRanges.ymin !== yScale.min || _scaleRanges.ymax !== yScale.max;
|
|
Object.assign(_scaleRanges, newRanges);
|
|
return changed;
|
|
}
|
|
|
|
const atEdge = (t)=>t === 0 || t === 1;
|
|
const elasticIn = (t, s, p)=>-(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p));
|
|
const elasticOut = (t, s, p)=>Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1;
|
|
/**
|
|
* Easing functions adapted from Robert Penner's easing equations.
|
|
* @namespace Chart.helpers.easing.effects
|
|
* @see http://www.robertpenner.com/easing/
|
|
*/ const effects = {
|
|
linear: (t)=>t,
|
|
easeInQuad: (t)=>t * t,
|
|
easeOutQuad: (t)=>-t * (t - 2),
|
|
easeInOutQuad: (t)=>(t /= 0.5) < 1 ? 0.5 * t * t : -0.5 * (--t * (t - 2) - 1),
|
|
easeInCubic: (t)=>t * t * t,
|
|
easeOutCubic: (t)=>(t -= 1) * t * t + 1,
|
|
easeInOutCubic: (t)=>(t /= 0.5) < 1 ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2),
|
|
easeInQuart: (t)=>t * t * t * t,
|
|
easeOutQuart: (t)=>-((t -= 1) * t * t * t - 1),
|
|
easeInOutQuart: (t)=>(t /= 0.5) < 1 ? 0.5 * t * t * t * t : -0.5 * ((t -= 2) * t * t * t - 2),
|
|
easeInQuint: (t)=>t * t * t * t * t,
|
|
easeOutQuint: (t)=>(t -= 1) * t * t * t * t + 1,
|
|
easeInOutQuint: (t)=>(t /= 0.5) < 1 ? 0.5 * t * t * t * t * t : 0.5 * ((t -= 2) * t * t * t * t + 2),
|
|
easeInSine: (t)=>-Math.cos(t * HALF_PI) + 1,
|
|
easeOutSine: (t)=>Math.sin(t * HALF_PI),
|
|
easeInOutSine: (t)=>-0.5 * (Math.cos(PI * t) - 1),
|
|
easeInExpo: (t)=>t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
|
|
easeOutExpo: (t)=>t === 1 ? 1 : -Math.pow(2, -10 * t) + 1,
|
|
easeInOutExpo: (t)=>atEdge(t) ? t : t < 0.5 ? 0.5 * Math.pow(2, 10 * (t * 2 - 1)) : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2),
|
|
easeInCirc: (t)=>t >= 1 ? t : -(Math.sqrt(1 - t * t) - 1),
|
|
easeOutCirc: (t)=>Math.sqrt(1 - (t -= 1) * t),
|
|
easeInOutCirc: (t)=>(t /= 0.5) < 1 ? -0.5 * (Math.sqrt(1 - t * t) - 1) : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1),
|
|
easeInElastic: (t)=>atEdge(t) ? t : elasticIn(t, 0.075, 0.3),
|
|
easeOutElastic: (t)=>atEdge(t) ? t : elasticOut(t, 0.075, 0.3),
|
|
easeInOutElastic (t) {
|
|
const s = 0.1125;
|
|
const p = 0.45;
|
|
return atEdge(t) ? t : t < 0.5 ? 0.5 * elasticIn(t * 2, s, p) : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p);
|
|
},
|
|
easeInBack (t) {
|
|
const s = 1.70158;
|
|
return t * t * ((s + 1) * t - s);
|
|
},
|
|
easeOutBack (t) {
|
|
const s = 1.70158;
|
|
return (t -= 1) * t * ((s + 1) * t + s) + 1;
|
|
},
|
|
easeInOutBack (t) {
|
|
let s = 1.70158;
|
|
if ((t /= 0.5) < 1) {
|
|
return 0.5 * (t * t * (((s *= 1.525) + 1) * t - s));
|
|
}
|
|
return 0.5 * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2);
|
|
},
|
|
easeInBounce: (t)=>1 - effects.easeOutBounce(1 - t),
|
|
easeOutBounce (t) {
|
|
const m = 7.5625;
|
|
const d = 2.75;
|
|
if (t < 1 / d) {
|
|
return m * t * t;
|
|
}
|
|
if (t < 2 / d) {
|
|
return m * (t -= 1.5 / d) * t + 0.75;
|
|
}
|
|
if (t < 2.5 / d) {
|
|
return m * (t -= 2.25 / d) * t + 0.9375;
|
|
}
|
|
return m * (t -= 2.625 / d) * t + 0.984375;
|
|
},
|
|
easeInOutBounce: (t)=>t < 0.5 ? effects.easeInBounce(t * 2) * 0.5 : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5
|
|
};
|
|
|
|
function isPatternOrGradient(value) {
|
|
if (value && typeof value === 'object') {
|
|
const type = value.toString();
|
|
return type === '[object CanvasPattern]' || type === '[object CanvasGradient]';
|
|
}
|
|
return false;
|
|
}
|
|
function color(value) {
|
|
return isPatternOrGradient(value) ? value : new Color(value);
|
|
}
|
|
function getHoverColor(value) {
|
|
return isPatternOrGradient(value) ? value : new Color(value).saturate(0.5).darken(0.1).hexString();
|
|
}
|
|
|
|
const numbers = [
|
|
'x',
|
|
'y',
|
|
'borderWidth',
|
|
'radius',
|
|
'tension'
|
|
];
|
|
const colors = [
|
|
'color',
|
|
'borderColor',
|
|
'backgroundColor'
|
|
];
|
|
function applyAnimationsDefaults(defaults) {
|
|
defaults.set('animation', {
|
|
delay: undefined,
|
|
duration: 1000,
|
|
easing: 'easeOutQuart',
|
|
fn: undefined,
|
|
from: undefined,
|
|
loop: undefined,
|
|
to: undefined,
|
|
type: undefined
|
|
});
|
|
defaults.describe('animation', {
|
|
_fallback: false,
|
|
_indexable: false,
|
|
_scriptable: (name)=>name !== 'onProgress' && name !== 'onComplete' && name !== 'fn'
|
|
});
|
|
defaults.set('animations', {
|
|
colors: {
|
|
type: 'color',
|
|
properties: colors
|
|
},
|
|
numbers: {
|
|
type: 'number',
|
|
properties: numbers
|
|
}
|
|
});
|
|
defaults.describe('animations', {
|
|
_fallback: 'animation'
|
|
});
|
|
defaults.set('transitions', {
|
|
active: {
|
|
animation: {
|
|
duration: 400
|
|
}
|
|
},
|
|
resize: {
|
|
animation: {
|
|
duration: 0
|
|
}
|
|
},
|
|
show: {
|
|
animations: {
|
|
colors: {
|
|
from: 'transparent'
|
|
},
|
|
visible: {
|
|
type: 'boolean',
|
|
duration: 0
|
|
}
|
|
}
|
|
},
|
|
hide: {
|
|
animations: {
|
|
colors: {
|
|
to: 'transparent'
|
|
},
|
|
visible: {
|
|
type: 'boolean',
|
|
easing: 'linear',
|
|
fn: (v)=>v | 0
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyLayoutsDefaults(defaults) {
|
|
defaults.set('layout', {
|
|
autoPadding: true,
|
|
padding: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
}
|
|
});
|
|
}
|
|
|
|
const intlCache = new Map();
|
|
function getNumberFormat(locale, options) {
|
|
options = options || {};
|
|
const cacheKey = locale + JSON.stringify(options);
|
|
let formatter = intlCache.get(cacheKey);
|
|
if (!formatter) {
|
|
formatter = new Intl.NumberFormat(locale, options);
|
|
intlCache.set(cacheKey, formatter);
|
|
}
|
|
return formatter;
|
|
}
|
|
function formatNumber(num, locale, options) {
|
|
return getNumberFormat(locale, options).format(num);
|
|
}
|
|
|
|
const formatters = {
|
|
values (value) {
|
|
return isArray(value) ? value : '' + value;
|
|
},
|
|
numeric (tickValue, index, ticks) {
|
|
if (tickValue === 0) {
|
|
return '0';
|
|
}
|
|
const locale = this.chart.options.locale;
|
|
let notation;
|
|
let delta = tickValue;
|
|
if (ticks.length > 1) {
|
|
const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));
|
|
if (maxTick < 1e-4 || maxTick > 1e+15) {
|
|
notation = 'scientific';
|
|
}
|
|
delta = calculateDelta(tickValue, ticks);
|
|
}
|
|
const logDelta = log10(Math.abs(delta));
|
|
const numDecimal = isNaN(logDelta) ? 1 : Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0);
|
|
const options = {
|
|
notation,
|
|
minimumFractionDigits: numDecimal,
|
|
maximumFractionDigits: numDecimal
|
|
};
|
|
Object.assign(options, this.options.ticks.format);
|
|
return formatNumber(tickValue, locale, options);
|
|
},
|
|
logarithmic (tickValue, index, ticks) {
|
|
if (tickValue === 0) {
|
|
return '0';
|
|
}
|
|
const remain = ticks[index].significand || tickValue / Math.pow(10, Math.floor(log10(tickValue)));
|
|
if ([
|
|
1,
|
|
2,
|
|
3,
|
|
5,
|
|
10,
|
|
15
|
|
].includes(remain) || index > 0.8 * ticks.length) {
|
|
return formatters.numeric.call(this, tickValue, index, ticks);
|
|
}
|
|
return '';
|
|
}
|
|
};
|
|
function calculateDelta(tickValue, ticks) {
|
|
let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value;
|
|
if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) {
|
|
delta = tickValue - Math.floor(tickValue);
|
|
}
|
|
return delta;
|
|
}
|
|
var Ticks = {
|
|
formatters
|
|
};
|
|
|
|
function applyScaleDefaults(defaults) {
|
|
defaults.set('scale', {
|
|
display: true,
|
|
offset: false,
|
|
reverse: false,
|
|
beginAtZero: false,
|
|
bounds: 'ticks',
|
|
clip: true,
|
|
grace: 0,
|
|
grid: {
|
|
display: true,
|
|
lineWidth: 1,
|
|
drawOnChartArea: true,
|
|
drawTicks: true,
|
|
tickLength: 8,
|
|
tickWidth: (_ctx, options)=>options.lineWidth,
|
|
tickColor: (_ctx, options)=>options.color,
|
|
offset: false
|
|
},
|
|
border: {
|
|
display: true,
|
|
dash: [],
|
|
dashOffset: 0.0,
|
|
width: 1
|
|
},
|
|
title: {
|
|
display: false,
|
|
text: '',
|
|
padding: {
|
|
top: 4,
|
|
bottom: 4
|
|
}
|
|
},
|
|
ticks: {
|
|
minRotation: 0,
|
|
maxRotation: 50,
|
|
mirror: false,
|
|
textStrokeWidth: 0,
|
|
textStrokeColor: '',
|
|
padding: 3,
|
|
display: true,
|
|
autoSkip: true,
|
|
autoSkipPadding: 3,
|
|
labelOffset: 0,
|
|
callback: Ticks.formatters.values,
|
|
minor: {},
|
|
major: {},
|
|
align: 'center',
|
|
crossAlign: 'near',
|
|
showLabelBackdrop: false,
|
|
backdropColor: 'rgba(255, 255, 255, 0.75)',
|
|
backdropPadding: 2
|
|
}
|
|
});
|
|
defaults.route('scale.ticks', 'color', '', 'color');
|
|
defaults.route('scale.grid', 'color', '', 'borderColor');
|
|
defaults.route('scale.border', 'color', '', 'borderColor');
|
|
defaults.route('scale.title', 'color', '', 'color');
|
|
defaults.describe('scale', {
|
|
_fallback: false,
|
|
_scriptable: (name)=>!name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser',
|
|
_indexable: (name)=>name !== 'borderDash' && name !== 'tickBorderDash' && name !== 'dash'
|
|
});
|
|
defaults.describe('scales', {
|
|
_fallback: 'scale'
|
|
});
|
|
defaults.describe('scale.ticks', {
|
|
_scriptable: (name)=>name !== 'backdropPadding' && name !== 'callback',
|
|
_indexable: (name)=>name !== 'backdropPadding'
|
|
});
|
|
}
|
|
|
|
const overrides = Object.create(null);
|
|
const descriptors = Object.create(null);
|
|
function getScope$1(node, key) {
|
|
if (!key) {
|
|
return node;
|
|
}
|
|
const keys = key.split('.');
|
|
for(let i = 0, n = keys.length; i < n; ++i){
|
|
const k = keys[i];
|
|
node = node[k] || (node[k] = Object.create(null));
|
|
}
|
|
return node;
|
|
}
|
|
function set(root, scope, values) {
|
|
if (typeof scope === 'string') {
|
|
return merge(getScope$1(root, scope), values);
|
|
}
|
|
return merge(getScope$1(root, ''), scope);
|
|
}
|
|
class Defaults {
|
|
constructor(_descriptors, _appliers){
|
|
this.animation = undefined;
|
|
this.backgroundColor = 'rgba(0,0,0,0.1)';
|
|
this.borderColor = 'rgba(0,0,0,0.1)';
|
|
this.color = '#666';
|
|
this.datasets = {};
|
|
this.devicePixelRatio = (context)=>context.chart.platform.getDevicePixelRatio();
|
|
this.elements = {};
|
|
this.events = [
|
|
'mousemove',
|
|
'mouseout',
|
|
'click',
|
|
'touchstart',
|
|
'touchmove'
|
|
];
|
|
this.font = {
|
|
family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
|
|
size: 12,
|
|
style: 'normal',
|
|
lineHeight: 1.2,
|
|
weight: null
|
|
};
|
|
this.hover = {};
|
|
this.hoverBackgroundColor = (ctx, options)=>getHoverColor(options.backgroundColor);
|
|
this.hoverBorderColor = (ctx, options)=>getHoverColor(options.borderColor);
|
|
this.hoverColor = (ctx, options)=>getHoverColor(options.color);
|
|
this.indexAxis = 'x';
|
|
this.interaction = {
|
|
mode: 'nearest',
|
|
intersect: true,
|
|
includeInvisible: false
|
|
};
|
|
this.maintainAspectRatio = true;
|
|
this.onHover = null;
|
|
this.onClick = null;
|
|
this.parsing = true;
|
|
this.plugins = {};
|
|
this.responsive = true;
|
|
this.scale = undefined;
|
|
this.scales = {};
|
|
this.showLine = true;
|
|
this.drawActiveElementsOnTop = true;
|
|
this.describe(_descriptors);
|
|
this.apply(_appliers);
|
|
}
|
|
set(scope, values) {
|
|
return set(this, scope, values);
|
|
}
|
|
get(scope) {
|
|
return getScope$1(this, scope);
|
|
}
|
|
describe(scope, values) {
|
|
return set(descriptors, scope, values);
|
|
}
|
|
override(scope, values) {
|
|
return set(overrides, scope, values);
|
|
}
|
|
route(scope, name, targetScope, targetName) {
|
|
const scopeObject = getScope$1(this, scope);
|
|
const targetScopeObject = getScope$1(this, targetScope);
|
|
const privateName = '_' + name;
|
|
Object.defineProperties(scopeObject, {
|
|
[privateName]: {
|
|
value: scopeObject[name],
|
|
writable: true
|
|
},
|
|
[name]: {
|
|
enumerable: true,
|
|
get () {
|
|
const local = this[privateName];
|
|
const target = targetScopeObject[targetName];
|
|
if (isObject(local)) {
|
|
return Object.assign({}, target, local);
|
|
}
|
|
return valueOrDefault(local, target);
|
|
},
|
|
set (value) {
|
|
this[privateName] = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
apply(appliers) {
|
|
appliers.forEach((apply)=>apply(this));
|
|
}
|
|
}
|
|
var defaults = /* #__PURE__ */ new Defaults({
|
|
_scriptable: (name)=>!name.startsWith('on'),
|
|
_indexable: (name)=>name !== 'events',
|
|
hover: {
|
|
_fallback: 'interaction'
|
|
},
|
|
interaction: {
|
|
_scriptable: false,
|
|
_indexable: false
|
|
}
|
|
}, [
|
|
applyAnimationsDefaults,
|
|
applyLayoutsDefaults,
|
|
applyScaleDefaults
|
|
]);
|
|
|
|
/**
|
|
* Converts the given font object into a CSS font string.
|
|
* @param font - A font object.
|
|
* @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
|
* @private
|
|
*/ function toFontString(font) {
|
|
if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) {
|
|
return null;
|
|
}
|
|
return (font.style ? font.style + ' ' : '') + (font.weight ? font.weight + ' ' : '') + font.size + 'px ' + font.family;
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _measureText(ctx, data, gc, longest, string) {
|
|
let textWidth = data[string];
|
|
if (!textWidth) {
|
|
textWidth = data[string] = ctx.measureText(string).width;
|
|
gc.push(string);
|
|
}
|
|
if (textWidth > longest) {
|
|
longest = textWidth;
|
|
}
|
|
return longest;
|
|
}
|
|
/**
|
|
* @private
|
|
*/ // eslint-disable-next-line complexity
|
|
function _longestText(ctx, font, arrayOfThings, cache) {
|
|
cache = cache || {};
|
|
let data = cache.data = cache.data || {};
|
|
let gc = cache.garbageCollect = cache.garbageCollect || [];
|
|
if (cache.font !== font) {
|
|
data = cache.data = {};
|
|
gc = cache.garbageCollect = [];
|
|
cache.font = font;
|
|
}
|
|
ctx.save();
|
|
ctx.font = font;
|
|
let longest = 0;
|
|
const ilen = arrayOfThings.length;
|
|
let i, j, jlen, thing, nestedThing;
|
|
for(i = 0; i < ilen; i++){
|
|
thing = arrayOfThings[i];
|
|
// Undefined strings and arrays should not be measured
|
|
if (thing !== undefined && thing !== null && !isArray(thing)) {
|
|
longest = _measureText(ctx, data, gc, longest, thing);
|
|
} else if (isArray(thing)) {
|
|
// if it is an array lets measure each element
|
|
// to do maybe simplify this function a bit so we can do this more recursively?
|
|
for(j = 0, jlen = thing.length; j < jlen; j++){
|
|
nestedThing = thing[j];
|
|
// Undefined strings and arrays should not be measured
|
|
if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) {
|
|
longest = _measureText(ctx, data, gc, longest, nestedThing);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ctx.restore();
|
|
const gcLen = gc.length / 2;
|
|
if (gcLen > arrayOfThings.length) {
|
|
for(i = 0; i < gcLen; i++){
|
|
delete data[gc[i]];
|
|
}
|
|
gc.splice(0, gcLen);
|
|
}
|
|
return longest;
|
|
}
|
|
/**
|
|
* Returns the aligned pixel value to avoid anti-aliasing blur
|
|
* @param chart - The chart instance.
|
|
* @param pixel - A pixel value.
|
|
* @param width - The width of the element.
|
|
* @returns The aligned pixel value.
|
|
* @private
|
|
*/ function _alignPixel(chart, pixel, width) {
|
|
const devicePixelRatio = chart.currentDevicePixelRatio;
|
|
const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0;
|
|
return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth;
|
|
}
|
|
/**
|
|
* Clears the entire canvas.
|
|
*/ function clearCanvas(canvas, ctx) {
|
|
if (!ctx && !canvas) {
|
|
return;
|
|
}
|
|
ctx = ctx || canvas.getContext('2d');
|
|
ctx.save();
|
|
// canvas.width and canvas.height do not consider the canvas transform,
|
|
// while clearRect does
|
|
ctx.resetTransform();
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.restore();
|
|
}
|
|
function drawPoint(ctx, options, x, y) {
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
drawPointLegend(ctx, options, x, y, null);
|
|
}
|
|
// eslint-disable-next-line complexity
|
|
function drawPointLegend(ctx, options, x, y, w) {
|
|
let type, xOffset, yOffset, size, cornerRadius, width, xOffsetW, yOffsetW;
|
|
const style = options.pointStyle;
|
|
const rotation = options.rotation;
|
|
const radius = options.radius;
|
|
let rad = (rotation || 0) * RAD_PER_DEG;
|
|
if (style && typeof style === 'object') {
|
|
type = style.toString();
|
|
if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(rad);
|
|
ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height);
|
|
ctx.restore();
|
|
return;
|
|
}
|
|
}
|
|
if (isNaN(radius) || radius <= 0) {
|
|
return;
|
|
}
|
|
ctx.beginPath();
|
|
switch(style){
|
|
// Default includes circle
|
|
default:
|
|
if (w) {
|
|
ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU);
|
|
} else {
|
|
ctx.arc(x, y, radius, 0, TAU);
|
|
}
|
|
ctx.closePath();
|
|
break;
|
|
case 'triangle':
|
|
width = w ? w / 2 : radius;
|
|
ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
|
|
rad += TWO_THIRDS_PI;
|
|
ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
|
|
rad += TWO_THIRDS_PI;
|
|
ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);
|
|
ctx.closePath();
|
|
break;
|
|
case 'rectRounded':
|
|
// NOTE: the rounded rect implementation changed to use `arc` instead of
|
|
// `quadraticCurveTo` since it generates better results when rect is
|
|
// almost a circle. 0.516 (instead of 0.5) produces results with visually
|
|
// closer proportion to the previous impl and it is inscribed in the
|
|
// circle with `radius`. For more details, see the following PRs:
|
|
// https://github.com/chartjs/Chart.js/issues/5597
|
|
// https://github.com/chartjs/Chart.js/issues/5858
|
|
cornerRadius = radius * 0.516;
|
|
size = radius - cornerRadius;
|
|
xOffset = Math.cos(rad + QUARTER_PI) * size;
|
|
xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
|
|
yOffset = Math.sin(rad + QUARTER_PI) * size;
|
|
yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);
|
|
ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);
|
|
ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad);
|
|
ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI);
|
|
ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);
|
|
ctx.closePath();
|
|
break;
|
|
case 'rect':
|
|
if (!rotation) {
|
|
size = Math.SQRT1_2 * radius;
|
|
width = w ? w / 2 : size;
|
|
ctx.rect(x - width, y - size, 2 * width, 2 * size);
|
|
break;
|
|
}
|
|
rad += QUARTER_PI;
|
|
/* falls through */ case 'rectRot':
|
|
xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
|
|
xOffset = Math.cos(rad) * radius;
|
|
yOffset = Math.sin(rad) * radius;
|
|
yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
|
|
ctx.moveTo(x - xOffsetW, y - yOffset);
|
|
ctx.lineTo(x + yOffsetW, y - xOffset);
|
|
ctx.lineTo(x + xOffsetW, y + yOffset);
|
|
ctx.lineTo(x - yOffsetW, y + xOffset);
|
|
ctx.closePath();
|
|
break;
|
|
case 'crossRot':
|
|
rad += QUARTER_PI;
|
|
/* falls through */ case 'cross':
|
|
xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
|
|
xOffset = Math.cos(rad) * radius;
|
|
yOffset = Math.sin(rad) * radius;
|
|
yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
|
|
ctx.moveTo(x - xOffsetW, y - yOffset);
|
|
ctx.lineTo(x + xOffsetW, y + yOffset);
|
|
ctx.moveTo(x + yOffsetW, y - xOffset);
|
|
ctx.lineTo(x - yOffsetW, y + xOffset);
|
|
break;
|
|
case 'star':
|
|
xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
|
|
xOffset = Math.cos(rad) * radius;
|
|
yOffset = Math.sin(rad) * radius;
|
|
yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
|
|
ctx.moveTo(x - xOffsetW, y - yOffset);
|
|
ctx.lineTo(x + xOffsetW, y + yOffset);
|
|
ctx.moveTo(x + yOffsetW, y - xOffset);
|
|
ctx.lineTo(x - yOffsetW, y + xOffset);
|
|
rad += QUARTER_PI;
|
|
xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);
|
|
xOffset = Math.cos(rad) * radius;
|
|
yOffset = Math.sin(rad) * radius;
|
|
yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);
|
|
ctx.moveTo(x - xOffsetW, y - yOffset);
|
|
ctx.lineTo(x + xOffsetW, y + yOffset);
|
|
ctx.moveTo(x + yOffsetW, y - xOffset);
|
|
ctx.lineTo(x - yOffsetW, y + xOffset);
|
|
break;
|
|
case 'line':
|
|
xOffset = w ? w / 2 : Math.cos(rad) * radius;
|
|
yOffset = Math.sin(rad) * radius;
|
|
ctx.moveTo(x - xOffset, y - yOffset);
|
|
ctx.lineTo(x + xOffset, y + yOffset);
|
|
break;
|
|
case 'dash':
|
|
ctx.moveTo(x, y);
|
|
ctx.lineTo(x + Math.cos(rad) * (w ? w / 2 : radius), y + Math.sin(rad) * radius);
|
|
break;
|
|
case false:
|
|
ctx.closePath();
|
|
break;
|
|
}
|
|
ctx.fill();
|
|
if (options.borderWidth > 0) {
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
/**
|
|
* Returns true if the point is inside the rectangle
|
|
* @param point - The point to test
|
|
* @param area - The rectangle
|
|
* @param margin - allowed margin
|
|
* @private
|
|
*/ function _isPointInArea(point, area, margin) {
|
|
margin = margin || 0.5; // margin - default is to match rounded decimals
|
|
return !area || point && point.x > area.left - margin && point.x < area.right + margin && point.y > area.top - margin && point.y < area.bottom + margin;
|
|
}
|
|
function clipArea(ctx, area) {
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top);
|
|
ctx.clip();
|
|
}
|
|
function unclipArea(ctx) {
|
|
ctx.restore();
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _steppedLineTo(ctx, previous, target, flip, mode) {
|
|
if (!previous) {
|
|
return ctx.lineTo(target.x, target.y);
|
|
}
|
|
if (mode === 'middle') {
|
|
const midpoint = (previous.x + target.x) / 2.0;
|
|
ctx.lineTo(midpoint, previous.y);
|
|
ctx.lineTo(midpoint, target.y);
|
|
} else if (mode === 'after' !== !!flip) {
|
|
ctx.lineTo(previous.x, target.y);
|
|
} else {
|
|
ctx.lineTo(target.x, previous.y);
|
|
}
|
|
ctx.lineTo(target.x, target.y);
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _bezierCurveTo(ctx, previous, target, flip) {
|
|
if (!previous) {
|
|
return ctx.lineTo(target.x, target.y);
|
|
}
|
|
ctx.bezierCurveTo(flip ? previous.cp1x : previous.cp2x, flip ? previous.cp1y : previous.cp2y, flip ? target.cp2x : target.cp1x, flip ? target.cp2y : target.cp1y, target.x, target.y);
|
|
}
|
|
function setRenderOpts(ctx, opts) {
|
|
if (opts.translation) {
|
|
ctx.translate(opts.translation[0], opts.translation[1]);
|
|
}
|
|
if (!isNullOrUndef(opts.rotation)) {
|
|
ctx.rotate(opts.rotation);
|
|
}
|
|
if (opts.color) {
|
|
ctx.fillStyle = opts.color;
|
|
}
|
|
if (opts.textAlign) {
|
|
ctx.textAlign = opts.textAlign;
|
|
}
|
|
if (opts.textBaseline) {
|
|
ctx.textBaseline = opts.textBaseline;
|
|
}
|
|
}
|
|
function decorateText(ctx, x, y, line, opts) {
|
|
if (opts.strikethrough || opts.underline) {
|
|
/**
|
|
* Now that IE11 support has been dropped, we can use more
|
|
* of the TextMetrics object. The actual bounding boxes
|
|
* are unflagged in Chrome, Firefox, Edge, and Safari so they
|
|
* can be safely used.
|
|
* See https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Browser_compatibility
|
|
*/ const metrics = ctx.measureText(line);
|
|
const left = x - metrics.actualBoundingBoxLeft;
|
|
const right = x + metrics.actualBoundingBoxRight;
|
|
const top = y - metrics.actualBoundingBoxAscent;
|
|
const bottom = y + metrics.actualBoundingBoxDescent;
|
|
const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom;
|
|
ctx.strokeStyle = ctx.fillStyle;
|
|
ctx.beginPath();
|
|
ctx.lineWidth = opts.decorationWidth || 2;
|
|
ctx.moveTo(left, yDecoration);
|
|
ctx.lineTo(right, yDecoration);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
function drawBackdrop(ctx, opts) {
|
|
const oldColor = ctx.fillStyle;
|
|
ctx.fillStyle = opts.color;
|
|
ctx.fillRect(opts.left, opts.top, opts.width, opts.height);
|
|
ctx.fillStyle = oldColor;
|
|
}
|
|
/**
|
|
* Render text onto the canvas
|
|
*/ function renderText(ctx, text, x, y, font, opts = {}) {
|
|
const lines = isArray(text) ? text : [
|
|
text
|
|
];
|
|
const stroke = opts.strokeWidth > 0 && opts.strokeColor !== '';
|
|
let i, line;
|
|
ctx.save();
|
|
ctx.font = font.string;
|
|
setRenderOpts(ctx, opts);
|
|
for(i = 0; i < lines.length; ++i){
|
|
line = lines[i];
|
|
if (opts.backdrop) {
|
|
drawBackdrop(ctx, opts.backdrop);
|
|
}
|
|
if (stroke) {
|
|
if (opts.strokeColor) {
|
|
ctx.strokeStyle = opts.strokeColor;
|
|
}
|
|
if (!isNullOrUndef(opts.strokeWidth)) {
|
|
ctx.lineWidth = opts.strokeWidth;
|
|
}
|
|
ctx.strokeText(line, x, y, opts.maxWidth);
|
|
}
|
|
ctx.fillText(line, x, y, opts.maxWidth);
|
|
decorateText(ctx, x, y, line, opts);
|
|
y += Number(font.lineHeight);
|
|
}
|
|
ctx.restore();
|
|
}
|
|
/**
|
|
* Add a path of a rectangle with rounded corners to the current sub-path
|
|
* @param ctx - Context
|
|
* @param rect - Bounding rect
|
|
*/ function addRoundedRectPath(ctx, rect) {
|
|
const { x , y , w , h , radius } = rect;
|
|
// top left arc
|
|
ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, 1.5 * PI, PI, true);
|
|
// line from top left to bottom left
|
|
ctx.lineTo(x, y + h - radius.bottomLeft);
|
|
// bottom left arc
|
|
ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
|
|
// line from bottom left to bottom right
|
|
ctx.lineTo(x + w - radius.bottomRight, y + h);
|
|
// bottom right arc
|
|
ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
|
|
// line from bottom right to top right
|
|
ctx.lineTo(x + w, y + radius.topRight);
|
|
// top right arc
|
|
ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
|
|
// line from top right to top left
|
|
ctx.lineTo(x + radius.topLeft, y);
|
|
}
|
|
|
|
const LINE_HEIGHT = /^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/;
|
|
const FONT_STYLE = /^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;
|
|
/**
|
|
* @alias Chart.helpers.options
|
|
* @namespace
|
|
*/ /**
|
|
* Converts the given line height `value` in pixels for a specific font `size`.
|
|
* @param value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em').
|
|
* @param size - The font size (in pixels) used to resolve relative `value`.
|
|
* @returns The effective line height in pixels (size * 1.2 if value is invalid).
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height
|
|
* @since 2.7.0
|
|
*/ function toLineHeight(value, size) {
|
|
const matches = ('' + value).match(LINE_HEIGHT);
|
|
if (!matches || matches[1] === 'normal') {
|
|
return size * 1.2;
|
|
}
|
|
value = +matches[2];
|
|
switch(matches[3]){
|
|
case 'px':
|
|
return value;
|
|
case '%':
|
|
value /= 100;
|
|
break;
|
|
}
|
|
return size * value;
|
|
}
|
|
const numberOrZero = (v)=>+v || 0;
|
|
function _readValueToProps(value, props) {
|
|
const ret = {};
|
|
const objProps = isObject(props);
|
|
const keys = objProps ? Object.keys(props) : props;
|
|
const read = isObject(value) ? objProps ? (prop)=>valueOrDefault(value[prop], value[props[prop]]) : (prop)=>value[prop] : ()=>value;
|
|
for (const prop of keys){
|
|
ret[prop] = numberOrZero(read(prop));
|
|
}
|
|
return ret;
|
|
}
|
|
/**
|
|
* Converts the given value into a TRBL object.
|
|
* @param value - If a number, set the value to all TRBL component,
|
|
* else, if an object, use defined properties and sets undefined ones to 0.
|
|
* x / y are shorthands for same value for left/right and top/bottom.
|
|
* @returns The padding values (top, right, bottom, left)
|
|
* @since 3.0.0
|
|
*/ function toTRBL(value) {
|
|
return _readValueToProps(value, {
|
|
top: 'y',
|
|
right: 'x',
|
|
bottom: 'y',
|
|
left: 'x'
|
|
});
|
|
}
|
|
/**
|
|
* Converts the given value into a TRBL corners object (similar with css border-radius).
|
|
* @param value - If a number, set the value to all TRBL corner components,
|
|
* else, if an object, use defined properties and sets undefined ones to 0.
|
|
* @returns The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight)
|
|
* @since 3.0.0
|
|
*/ function toTRBLCorners(value) {
|
|
return _readValueToProps(value, [
|
|
'topLeft',
|
|
'topRight',
|
|
'bottomLeft',
|
|
'bottomRight'
|
|
]);
|
|
}
|
|
/**
|
|
* Converts the given value into a padding object with pre-computed width/height.
|
|
* @param value - If a number, set the value to all TRBL component,
|
|
* else, if an object, use defined properties and sets undefined ones to 0.
|
|
* x / y are shorthands for same value for left/right and top/bottom.
|
|
* @returns The padding values (top, right, bottom, left, width, height)
|
|
* @since 2.7.0
|
|
*/ function toPadding(value) {
|
|
const obj = toTRBL(value);
|
|
obj.width = obj.left + obj.right;
|
|
obj.height = obj.top + obj.bottom;
|
|
return obj;
|
|
}
|
|
/**
|
|
* Parses font options and returns the font object.
|
|
* @param options - A object that contains font options to be parsed.
|
|
* @param fallback - A object that contains fallback font options.
|
|
* @return The font object.
|
|
* @private
|
|
*/ function toFont(options, fallback) {
|
|
options = options || {};
|
|
fallback = fallback || defaults.font;
|
|
let size = valueOrDefault(options.size, fallback.size);
|
|
if (typeof size === 'string') {
|
|
size = parseInt(size, 10);
|
|
}
|
|
let style = valueOrDefault(options.style, fallback.style);
|
|
if (style && !('' + style).match(FONT_STYLE)) {
|
|
console.warn('Invalid font style specified: "' + style + '"');
|
|
style = undefined;
|
|
}
|
|
const font = {
|
|
family: valueOrDefault(options.family, fallback.family),
|
|
lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size),
|
|
size,
|
|
style,
|
|
weight: valueOrDefault(options.weight, fallback.weight),
|
|
string: ''
|
|
};
|
|
font.string = toFontString(font);
|
|
return font;
|
|
}
|
|
/**
|
|
* Evaluates the given `inputs` sequentially and returns the first defined value.
|
|
* @param inputs - An array of values, falling back to the last value.
|
|
* @param context - If defined and the current value is a function, the value
|
|
* is called with `context` as first argument and the result becomes the new input.
|
|
* @param index - If defined and the current value is an array, the value
|
|
* at `index` become the new input.
|
|
* @param info - object to return information about resolution in
|
|
* @param info.cacheable - Will be set to `false` if option is not cacheable.
|
|
* @since 2.7.0
|
|
*/ function resolve(inputs, context, index, info) {
|
|
let cacheable = true;
|
|
let i, ilen, value;
|
|
for(i = 0, ilen = inputs.length; i < ilen; ++i){
|
|
value = inputs[i];
|
|
if (value === undefined) {
|
|
continue;
|
|
}
|
|
if (context !== undefined && typeof value === 'function') {
|
|
value = value(context);
|
|
cacheable = false;
|
|
}
|
|
if (index !== undefined && isArray(value)) {
|
|
value = value[index % value.length];
|
|
cacheable = false;
|
|
}
|
|
if (value !== undefined) {
|
|
if (info && !cacheable) {
|
|
info.cacheable = false;
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @param minmax
|
|
* @param grace
|
|
* @param beginAtZero
|
|
* @private
|
|
*/ function _addGrace(minmax, grace, beginAtZero) {
|
|
const { min , max } = minmax;
|
|
const change = toDimension(grace, (max - min) / 2);
|
|
const keepZero = (value, add)=>beginAtZero && value === 0 ? 0 : value + add;
|
|
return {
|
|
min: keepZero(min, -Math.abs(change)),
|
|
max: keepZero(max, change)
|
|
};
|
|
}
|
|
function createContext(parentContext, context) {
|
|
return Object.assign(Object.create(parentContext), context);
|
|
}
|
|
|
|
/**
|
|
* Creates a Proxy for resolving raw values for options.
|
|
* @param scopes - The option scopes to look for values, in resolution order
|
|
* @param prefixes - The prefixes for values, in resolution order.
|
|
* @param rootScopes - The root option scopes
|
|
* @param fallback - Parent scopes fallback
|
|
* @param getTarget - callback for getting the target for changed values
|
|
* @returns Proxy
|
|
* @private
|
|
*/ function _createResolver(scopes, prefixes = [
|
|
''
|
|
], rootScopes, fallback, getTarget = ()=>scopes[0]) {
|
|
const finalRootScopes = rootScopes || scopes;
|
|
if (typeof fallback === 'undefined') {
|
|
fallback = _resolve('_fallback', scopes);
|
|
}
|
|
const cache = {
|
|
[Symbol.toStringTag]: 'Object',
|
|
_cacheable: true,
|
|
_scopes: scopes,
|
|
_rootScopes: finalRootScopes,
|
|
_fallback: fallback,
|
|
_getTarget: getTarget,
|
|
override: (scope)=>_createResolver([
|
|
scope,
|
|
...scopes
|
|
], prefixes, finalRootScopes, fallback)
|
|
};
|
|
return new Proxy(cache, {
|
|
/**
|
|
* A trap for the delete operator.
|
|
*/ deleteProperty (target, prop) {
|
|
delete target[prop]; // remove from cache
|
|
delete target._keys; // remove cached keys
|
|
delete scopes[0][prop]; // remove from top level scope
|
|
return true;
|
|
},
|
|
/**
|
|
* A trap for getting property values.
|
|
*/ get (target, prop) {
|
|
return _cached(target, prop, ()=>_resolveWithPrefixes(prop, prefixes, scopes, target));
|
|
},
|
|
/**
|
|
* A trap for Object.getOwnPropertyDescriptor.
|
|
* Also used by Object.hasOwnProperty.
|
|
*/ getOwnPropertyDescriptor (target, prop) {
|
|
return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop);
|
|
},
|
|
/**
|
|
* A trap for Object.getPrototypeOf.
|
|
*/ getPrototypeOf () {
|
|
return Reflect.getPrototypeOf(scopes[0]);
|
|
},
|
|
/**
|
|
* A trap for the in operator.
|
|
*/ has (target, prop) {
|
|
return getKeysFromAllScopes(target).includes(prop);
|
|
},
|
|
/**
|
|
* A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.
|
|
*/ ownKeys (target) {
|
|
return getKeysFromAllScopes(target);
|
|
},
|
|
/**
|
|
* A trap for setting property values.
|
|
*/ set (target, prop, value) {
|
|
const storage = target._storage || (target._storage = getTarget());
|
|
target[prop] = storage[prop] = value; // set to top level scope + cache
|
|
delete target._keys; // remove cached keys
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Returns an Proxy for resolving option values with context.
|
|
* @param proxy - The Proxy returned by `_createResolver`
|
|
* @param context - Context object for scriptable/indexable options
|
|
* @param subProxy - The proxy provided for scriptable options
|
|
* @param descriptorDefaults - Defaults for descriptors
|
|
* @private
|
|
*/ function _attachContext(proxy, context, subProxy, descriptorDefaults) {
|
|
const cache = {
|
|
_cacheable: false,
|
|
_proxy: proxy,
|
|
_context: context,
|
|
_subProxy: subProxy,
|
|
_stack: new Set(),
|
|
_descriptors: _descriptors(proxy, descriptorDefaults),
|
|
setContext: (ctx)=>_attachContext(proxy, ctx, subProxy, descriptorDefaults),
|
|
override: (scope)=>_attachContext(proxy.override(scope), context, subProxy, descriptorDefaults)
|
|
};
|
|
return new Proxy(cache, {
|
|
/**
|
|
* A trap for the delete operator.
|
|
*/ deleteProperty (target, prop) {
|
|
delete target[prop]; // remove from cache
|
|
delete proxy[prop]; // remove from proxy
|
|
return true;
|
|
},
|
|
/**
|
|
* A trap for getting property values.
|
|
*/ get (target, prop, receiver) {
|
|
return _cached(target, prop, ()=>_resolveWithContext(target, prop, receiver));
|
|
},
|
|
/**
|
|
* A trap for Object.getOwnPropertyDescriptor.
|
|
* Also used by Object.hasOwnProperty.
|
|
*/ getOwnPropertyDescriptor (target, prop) {
|
|
return target._descriptors.allKeys ? Reflect.has(proxy, prop) ? {
|
|
enumerable: true,
|
|
configurable: true
|
|
} : undefined : Reflect.getOwnPropertyDescriptor(proxy, prop);
|
|
},
|
|
/**
|
|
* A trap for Object.getPrototypeOf.
|
|
*/ getPrototypeOf () {
|
|
return Reflect.getPrototypeOf(proxy);
|
|
},
|
|
/**
|
|
* A trap for the in operator.
|
|
*/ has (target, prop) {
|
|
return Reflect.has(proxy, prop);
|
|
},
|
|
/**
|
|
* A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.
|
|
*/ ownKeys () {
|
|
return Reflect.ownKeys(proxy);
|
|
},
|
|
/**
|
|
* A trap for setting property values.
|
|
*/ set (target, prop, value) {
|
|
proxy[prop] = value; // set to proxy
|
|
delete target[prop]; // remove from cache
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _descriptors(proxy, defaults = {
|
|
scriptable: true,
|
|
indexable: true
|
|
}) {
|
|
const { _scriptable =defaults.scriptable , _indexable =defaults.indexable , _allKeys =defaults.allKeys } = proxy;
|
|
return {
|
|
allKeys: _allKeys,
|
|
scriptable: _scriptable,
|
|
indexable: _indexable,
|
|
isScriptable: isFunction(_scriptable) ? _scriptable : ()=>_scriptable,
|
|
isIndexable: isFunction(_indexable) ? _indexable : ()=>_indexable
|
|
};
|
|
}
|
|
const readKey = (prefix, name)=>prefix ? prefix + _capitalize(name) : name;
|
|
const needsSubResolver = (prop, value)=>isObject(value) && prop !== 'adapters' && (Object.getPrototypeOf(value) === null || value.constructor === Object);
|
|
function _cached(target, prop, resolve) {
|
|
if (Object.prototype.hasOwnProperty.call(target, prop) || prop === 'constructor') {
|
|
return target[prop];
|
|
}
|
|
const value = resolve();
|
|
// cache the resolved value
|
|
target[prop] = value;
|
|
return value;
|
|
}
|
|
function _resolveWithContext(target, prop, receiver) {
|
|
const { _proxy , _context , _subProxy , _descriptors: descriptors } = target;
|
|
let value = _proxy[prop]; // resolve from proxy
|
|
// resolve with context
|
|
if (isFunction(value) && descriptors.isScriptable(prop)) {
|
|
value = _resolveScriptable(prop, value, target, receiver);
|
|
}
|
|
if (isArray(value) && value.length) {
|
|
value = _resolveArray(prop, value, target, descriptors.isIndexable);
|
|
}
|
|
if (needsSubResolver(prop, value)) {
|
|
// if the resolved value is an object, create a sub resolver for it
|
|
value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors);
|
|
}
|
|
return value;
|
|
}
|
|
function _resolveScriptable(prop, getValue, target, receiver) {
|
|
const { _proxy , _context , _subProxy , _stack } = target;
|
|
if (_stack.has(prop)) {
|
|
throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop);
|
|
}
|
|
_stack.add(prop);
|
|
let value = getValue(_context, _subProxy || receiver);
|
|
_stack.delete(prop);
|
|
if (needsSubResolver(prop, value)) {
|
|
// When scriptable option returns an object, create a resolver on that.
|
|
value = createSubResolver(_proxy._scopes, _proxy, prop, value);
|
|
}
|
|
return value;
|
|
}
|
|
function _resolveArray(prop, value, target, isIndexable) {
|
|
const { _proxy , _context , _subProxy , _descriptors: descriptors } = target;
|
|
if (typeof _context.index !== 'undefined' && isIndexable(prop)) {
|
|
return value[_context.index % value.length];
|
|
} else if (isObject(value[0])) {
|
|
// Array of objects, return array or resolvers
|
|
const arr = value;
|
|
const scopes = _proxy._scopes.filter((s)=>s !== arr);
|
|
value = [];
|
|
for (const item of arr){
|
|
const resolver = createSubResolver(scopes, _proxy, prop, item);
|
|
value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors));
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
function resolveFallback(fallback, prop, value) {
|
|
return isFunction(fallback) ? fallback(prop, value) : fallback;
|
|
}
|
|
const getScope = (key, parent)=>key === true ? parent : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined;
|
|
function addScopes(set, parentScopes, key, parentFallback, value) {
|
|
for (const parent of parentScopes){
|
|
const scope = getScope(key, parent);
|
|
if (scope) {
|
|
set.add(scope);
|
|
const fallback = resolveFallback(scope._fallback, key, value);
|
|
if (typeof fallback !== 'undefined' && fallback !== key && fallback !== parentFallback) {
|
|
// When we reach the descriptor that defines a new _fallback, return that.
|
|
// The fallback will resume to that new scope.
|
|
return fallback;
|
|
}
|
|
} else if (scope === false && typeof parentFallback !== 'undefined' && key !== parentFallback) {
|
|
// Fallback to `false` results to `false`, when falling back to different key.
|
|
// For example `interaction` from `hover` or `plugins.tooltip` and `animation` from `animations`
|
|
return null;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function createSubResolver(parentScopes, resolver, prop, value) {
|
|
const rootScopes = resolver._rootScopes;
|
|
const fallback = resolveFallback(resolver._fallback, prop, value);
|
|
const allScopes = [
|
|
...parentScopes,
|
|
...rootScopes
|
|
];
|
|
const set = new Set();
|
|
set.add(value);
|
|
let key = addScopesFromKey(set, allScopes, prop, fallback || prop, value);
|
|
if (key === null) {
|
|
return false;
|
|
}
|
|
if (typeof fallback !== 'undefined' && fallback !== prop) {
|
|
key = addScopesFromKey(set, allScopes, fallback, key, value);
|
|
if (key === null) {
|
|
return false;
|
|
}
|
|
}
|
|
return _createResolver(Array.from(set), [
|
|
''
|
|
], rootScopes, fallback, ()=>subGetTarget(resolver, prop, value));
|
|
}
|
|
function addScopesFromKey(set, allScopes, key, fallback, item) {
|
|
while(key){
|
|
key = addScopes(set, allScopes, key, fallback, item);
|
|
}
|
|
return key;
|
|
}
|
|
function subGetTarget(resolver, prop, value) {
|
|
const parent = resolver._getTarget();
|
|
if (!(prop in parent)) {
|
|
parent[prop] = {};
|
|
}
|
|
const target = parent[prop];
|
|
if (isArray(target) && isObject(value)) {
|
|
// For array of objects, the object is used to store updated values
|
|
return value;
|
|
}
|
|
return target || {};
|
|
}
|
|
function _resolveWithPrefixes(prop, prefixes, scopes, proxy) {
|
|
let value;
|
|
for (const prefix of prefixes){
|
|
value = _resolve(readKey(prefix, prop), scopes);
|
|
if (typeof value !== 'undefined') {
|
|
return needsSubResolver(prop, value) ? createSubResolver(scopes, proxy, prop, value) : value;
|
|
}
|
|
}
|
|
}
|
|
function _resolve(key, scopes) {
|
|
for (const scope of scopes){
|
|
if (!scope) {
|
|
continue;
|
|
}
|
|
const value = scope[key];
|
|
if (typeof value !== 'undefined') {
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
function getKeysFromAllScopes(target) {
|
|
let keys = target._keys;
|
|
if (!keys) {
|
|
keys = target._keys = resolveKeysFromAllScopes(target._scopes);
|
|
}
|
|
return keys;
|
|
}
|
|
function resolveKeysFromAllScopes(scopes) {
|
|
const set = new Set();
|
|
for (const scope of scopes){
|
|
for (const key of Object.keys(scope).filter((k)=>!k.startsWith('_'))){
|
|
set.add(key);
|
|
}
|
|
}
|
|
return Array.from(set);
|
|
}
|
|
function _parseObjectDataRadialScale(meta, data, start, count) {
|
|
const { iScale } = meta;
|
|
const { key ='r' } = this._parsing;
|
|
const parsed = new Array(count);
|
|
let i, ilen, index, item;
|
|
for(i = 0, ilen = count; i < ilen; ++i){
|
|
index = i + start;
|
|
item = data[index];
|
|
parsed[i] = {
|
|
r: iScale.parse(resolveObjectKey(item, key), index)
|
|
};
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
const EPSILON = Number.EPSILON || 1e-14;
|
|
const getPoint = (points, i)=>i < points.length && !points[i].skip && points[i];
|
|
const getValueAxis = (indexAxis)=>indexAxis === 'x' ? 'y' : 'x';
|
|
function splineCurve(firstPoint, middlePoint, afterPoint, t) {
|
|
// Props to Rob Spencer at scaled innovation for his post on splining between points
|
|
// http://scaledinnovation.com/analytics/splines/aboutSplines.html
|
|
// This function must also respect "skipped" points
|
|
const previous = firstPoint.skip ? middlePoint : firstPoint;
|
|
const current = middlePoint;
|
|
const next = afterPoint.skip ? middlePoint : afterPoint;
|
|
const d01 = distanceBetweenPoints(current, previous);
|
|
const d12 = distanceBetweenPoints(next, current);
|
|
let s01 = d01 / (d01 + d12);
|
|
let s12 = d12 / (d01 + d12);
|
|
// If all points are the same, s01 & s02 will be inf
|
|
s01 = isNaN(s01) ? 0 : s01;
|
|
s12 = isNaN(s12) ? 0 : s12;
|
|
const fa = t * s01; // scaling factor for triangle Ta
|
|
const fb = t * s12;
|
|
return {
|
|
previous: {
|
|
x: current.x - fa * (next.x - previous.x),
|
|
y: current.y - fa * (next.y - previous.y)
|
|
},
|
|
next: {
|
|
x: current.x + fb * (next.x - previous.x),
|
|
y: current.y + fb * (next.y - previous.y)
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Adjust tangents to ensure monotonic properties
|
|
*/ function monotoneAdjust(points, deltaK, mK) {
|
|
const pointsLen = points.length;
|
|
let alphaK, betaK, tauK, squaredMagnitude, pointCurrent;
|
|
let pointAfter = getPoint(points, 0);
|
|
for(let i = 0; i < pointsLen - 1; ++i){
|
|
pointCurrent = pointAfter;
|
|
pointAfter = getPoint(points, i + 1);
|
|
if (!pointCurrent || !pointAfter) {
|
|
continue;
|
|
}
|
|
if (almostEquals(deltaK[i], 0, EPSILON)) {
|
|
mK[i] = mK[i + 1] = 0;
|
|
continue;
|
|
}
|
|
alphaK = mK[i] / deltaK[i];
|
|
betaK = mK[i + 1] / deltaK[i];
|
|
squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);
|
|
if (squaredMagnitude <= 9) {
|
|
continue;
|
|
}
|
|
tauK = 3 / Math.sqrt(squaredMagnitude);
|
|
mK[i] = alphaK * tauK * deltaK[i];
|
|
mK[i + 1] = betaK * tauK * deltaK[i];
|
|
}
|
|
}
|
|
function monotoneCompute(points, mK, indexAxis = 'x') {
|
|
const valueAxis = getValueAxis(indexAxis);
|
|
const pointsLen = points.length;
|
|
let delta, pointBefore, pointCurrent;
|
|
let pointAfter = getPoint(points, 0);
|
|
for(let i = 0; i < pointsLen; ++i){
|
|
pointBefore = pointCurrent;
|
|
pointCurrent = pointAfter;
|
|
pointAfter = getPoint(points, i + 1);
|
|
if (!pointCurrent) {
|
|
continue;
|
|
}
|
|
const iPixel = pointCurrent[indexAxis];
|
|
const vPixel = pointCurrent[valueAxis];
|
|
if (pointBefore) {
|
|
delta = (iPixel - pointBefore[indexAxis]) / 3;
|
|
pointCurrent[`cp1${indexAxis}`] = iPixel - delta;
|
|
pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i];
|
|
}
|
|
if (pointAfter) {
|
|
delta = (pointAfter[indexAxis] - iPixel) / 3;
|
|
pointCurrent[`cp2${indexAxis}`] = iPixel + delta;
|
|
pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i];
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* This function calculates Bézier control points in a similar way than |splineCurve|,
|
|
* but preserves monotonicity of the provided data and ensures no local extremums are added
|
|
* between the dataset discrete points due to the interpolation.
|
|
* See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation
|
|
*/ function splineCurveMonotone(points, indexAxis = 'x') {
|
|
const valueAxis = getValueAxis(indexAxis);
|
|
const pointsLen = points.length;
|
|
const deltaK = Array(pointsLen).fill(0);
|
|
const mK = Array(pointsLen);
|
|
// Calculate slopes (deltaK) and initialize tangents (mK)
|
|
let i, pointBefore, pointCurrent;
|
|
let pointAfter = getPoint(points, 0);
|
|
for(i = 0; i < pointsLen; ++i){
|
|
pointBefore = pointCurrent;
|
|
pointCurrent = pointAfter;
|
|
pointAfter = getPoint(points, i + 1);
|
|
if (!pointCurrent) {
|
|
continue;
|
|
}
|
|
if (pointAfter) {
|
|
const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis];
|
|
// In the case of two points that appear at the same x pixel, slopeDeltaX is 0
|
|
deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0;
|
|
}
|
|
mK[i] = !pointBefore ? deltaK[i] : !pointAfter ? deltaK[i - 1] : sign(deltaK[i - 1]) !== sign(deltaK[i]) ? 0 : (deltaK[i - 1] + deltaK[i]) / 2;
|
|
}
|
|
monotoneAdjust(points, deltaK, mK);
|
|
monotoneCompute(points, mK, indexAxis);
|
|
}
|
|
function capControlPoint(pt, min, max) {
|
|
return Math.max(Math.min(pt, max), min);
|
|
}
|
|
function capBezierPoints(points, area) {
|
|
let i, ilen, point, inArea, inAreaPrev;
|
|
let inAreaNext = _isPointInArea(points[0], area);
|
|
for(i = 0, ilen = points.length; i < ilen; ++i){
|
|
inAreaPrev = inArea;
|
|
inArea = inAreaNext;
|
|
inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area);
|
|
if (!inArea) {
|
|
continue;
|
|
}
|
|
point = points[i];
|
|
if (inAreaPrev) {
|
|
point.cp1x = capControlPoint(point.cp1x, area.left, area.right);
|
|
point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom);
|
|
}
|
|
if (inAreaNext) {
|
|
point.cp2x = capControlPoint(point.cp2x, area.left, area.right);
|
|
point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _updateBezierControlPoints(points, options, area, loop, indexAxis) {
|
|
let i, ilen, point, controlPoints;
|
|
// Only consider points that are drawn in case the spanGaps option is used
|
|
if (options.spanGaps) {
|
|
points = points.filter((pt)=>!pt.skip);
|
|
}
|
|
if (options.cubicInterpolationMode === 'monotone') {
|
|
splineCurveMonotone(points, indexAxis);
|
|
} else {
|
|
let prev = loop ? points[points.length - 1] : points[0];
|
|
for(i = 0, ilen = points.length; i < ilen; ++i){
|
|
point = points[i];
|
|
controlPoints = splineCurve(prev, point, points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], options.tension);
|
|
point.cp1x = controlPoints.previous.x;
|
|
point.cp1y = controlPoints.previous.y;
|
|
point.cp2x = controlPoints.next.x;
|
|
point.cp2y = controlPoints.next.y;
|
|
prev = point;
|
|
}
|
|
}
|
|
if (options.capBezierPoints) {
|
|
capBezierPoints(points, area);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/ function _isDomSupported() {
|
|
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _getParentNode(domNode) {
|
|
let parent = domNode.parentNode;
|
|
if (parent && parent.toString() === '[object ShadowRoot]') {
|
|
parent = parent.host;
|
|
}
|
|
return parent;
|
|
}
|
|
/**
|
|
* convert max-width/max-height values that may be percentages into a number
|
|
* @private
|
|
*/ function parseMaxStyle(styleValue, node, parentProperty) {
|
|
let valueInPixels;
|
|
if (typeof styleValue === 'string') {
|
|
valueInPixels = parseInt(styleValue, 10);
|
|
if (styleValue.indexOf('%') !== -1) {
|
|
// percentage * size in dimension
|
|
valueInPixels = valueInPixels / 100 * node.parentNode[parentProperty];
|
|
}
|
|
} else {
|
|
valueInPixels = styleValue;
|
|
}
|
|
return valueInPixels;
|
|
}
|
|
const helpers_dataset_getComputedStyle = (element)=>element.ownerDocument.defaultView.getComputedStyle(element, null);
|
|
function getStyle(el, property) {
|
|
return helpers_dataset_getComputedStyle(el).getPropertyValue(property);
|
|
}
|
|
const positions = [
|
|
'top',
|
|
'right',
|
|
'bottom',
|
|
'left'
|
|
];
|
|
function getPositionedStyle(styles, style, suffix) {
|
|
const result = {};
|
|
suffix = suffix ? '-' + suffix : '';
|
|
for(let i = 0; i < 4; i++){
|
|
const pos = positions[i];
|
|
result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0;
|
|
}
|
|
result.width = result.left + result.right;
|
|
result.height = result.top + result.bottom;
|
|
return result;
|
|
}
|
|
const useOffsetPos = (x, y, target)=>(x > 0 || y > 0) && (!target || !target.shadowRoot);
|
|
/**
|
|
* @param e
|
|
* @param canvas
|
|
* @returns Canvas position
|
|
*/ function getCanvasPosition(e, canvas) {
|
|
const touches = e.touches;
|
|
const source = touches && touches.length ? touches[0] : e;
|
|
const { offsetX , offsetY } = source;
|
|
let box = false;
|
|
let x, y;
|
|
if (useOffsetPos(offsetX, offsetY, e.target)) {
|
|
x = offsetX;
|
|
y = offsetY;
|
|
} else {
|
|
const rect = canvas.getBoundingClientRect();
|
|
x = source.clientX - rect.left;
|
|
y = source.clientY - rect.top;
|
|
box = true;
|
|
}
|
|
return {
|
|
x,
|
|
y,
|
|
box
|
|
};
|
|
}
|
|
/**
|
|
* Gets an event's x, y coordinates, relative to the chart area
|
|
* @param event
|
|
* @param chart
|
|
* @returns x and y coordinates of the event
|
|
*/ function getRelativePosition(event, chart) {
|
|
if ('native' in event) {
|
|
return event;
|
|
}
|
|
const { canvas , currentDevicePixelRatio } = chart;
|
|
const style = helpers_dataset_getComputedStyle(canvas);
|
|
const borderBox = style.boxSizing === 'border-box';
|
|
const paddings = getPositionedStyle(style, 'padding');
|
|
const borders = getPositionedStyle(style, 'border', 'width');
|
|
const { x , y , box } = getCanvasPosition(event, canvas);
|
|
const xOffset = paddings.left + (box && borders.left);
|
|
const yOffset = paddings.top + (box && borders.top);
|
|
let { width , height } = chart;
|
|
if (borderBox) {
|
|
width -= paddings.width + borders.width;
|
|
height -= paddings.height + borders.height;
|
|
}
|
|
return {
|
|
x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio),
|
|
y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio)
|
|
};
|
|
}
|
|
function getContainerSize(canvas, width, height) {
|
|
let maxWidth, maxHeight;
|
|
if (width === undefined || height === undefined) {
|
|
const container = canvas && _getParentNode(canvas);
|
|
if (!container) {
|
|
width = canvas.clientWidth;
|
|
height = canvas.clientHeight;
|
|
} else {
|
|
const rect = container.getBoundingClientRect(); // this is the border box of the container
|
|
const containerStyle = helpers_dataset_getComputedStyle(container);
|
|
const containerBorder = getPositionedStyle(containerStyle, 'border', 'width');
|
|
const containerPadding = getPositionedStyle(containerStyle, 'padding');
|
|
width = rect.width - containerPadding.width - containerBorder.width;
|
|
height = rect.height - containerPadding.height - containerBorder.height;
|
|
maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth');
|
|
maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight');
|
|
}
|
|
}
|
|
return {
|
|
width,
|
|
height,
|
|
maxWidth: maxWidth || INFINITY,
|
|
maxHeight: maxHeight || INFINITY
|
|
};
|
|
}
|
|
const round1 = (v)=>Math.round(v * 10) / 10;
|
|
// eslint-disable-next-line complexity
|
|
function getMaximumSize(canvas, bbWidth, bbHeight, aspectRatio) {
|
|
const style = helpers_dataset_getComputedStyle(canvas);
|
|
const margins = getPositionedStyle(style, 'margin');
|
|
const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY;
|
|
const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY;
|
|
const containerSize = getContainerSize(canvas, bbWidth, bbHeight);
|
|
let { width , height } = containerSize;
|
|
if (style.boxSizing === 'content-box') {
|
|
const borders = getPositionedStyle(style, 'border', 'width');
|
|
const paddings = getPositionedStyle(style, 'padding');
|
|
width -= paddings.width + borders.width;
|
|
height -= paddings.height + borders.height;
|
|
}
|
|
width = Math.max(0, width - margins.width);
|
|
height = Math.max(0, aspectRatio ? width / aspectRatio : height - margins.height);
|
|
width = round1(Math.min(width, maxWidth, containerSize.maxWidth));
|
|
height = round1(Math.min(height, maxHeight, containerSize.maxHeight));
|
|
if (width && !height) {
|
|
// https://github.com/chartjs/Chart.js/issues/4659
|
|
// If the canvas has width, but no height, default to aspectRatio of 2 (canvas default)
|
|
height = round1(width / 2);
|
|
}
|
|
const maintainHeight = bbWidth !== undefined || bbHeight !== undefined;
|
|
if (maintainHeight && aspectRatio && containerSize.height && height > containerSize.height) {
|
|
height = containerSize.height;
|
|
width = round1(Math.floor(height * aspectRatio));
|
|
}
|
|
return {
|
|
width,
|
|
height
|
|
};
|
|
}
|
|
/**
|
|
* @param chart
|
|
* @param forceRatio
|
|
* @param forceStyle
|
|
* @returns True if the canvas context size or transformation has changed.
|
|
*/ function retinaScale(chart, forceRatio, forceStyle) {
|
|
const pixelRatio = forceRatio || 1;
|
|
const deviceHeight = round1(chart.height * pixelRatio);
|
|
const deviceWidth = round1(chart.width * pixelRatio);
|
|
chart.height = round1(chart.height);
|
|
chart.width = round1(chart.width);
|
|
const canvas = chart.canvas;
|
|
// If no style has been set on the canvas, the render size is used as display size,
|
|
// making the chart visually bigger, so let's enforce it to the "correct" values.
|
|
// See https://github.com/chartjs/Chart.js/issues/3575
|
|
if (canvas.style && (forceStyle || !canvas.style.height && !canvas.style.width)) {
|
|
canvas.style.height = `${chart.height}px`;
|
|
canvas.style.width = `${chart.width}px`;
|
|
}
|
|
if (chart.currentDevicePixelRatio !== pixelRatio || canvas.height !== deviceHeight || canvas.width !== deviceWidth) {
|
|
chart.currentDevicePixelRatio = pixelRatio;
|
|
canvas.height = deviceHeight;
|
|
canvas.width = deviceWidth;
|
|
chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Detects support for options object argument in addEventListener.
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
|
|
* @private
|
|
*/ const supportsEventListenerOptions = function() {
|
|
let passiveSupported = false;
|
|
try {
|
|
const options = {
|
|
get passive () {
|
|
passiveSupported = true;
|
|
return false;
|
|
}
|
|
};
|
|
if (_isDomSupported()) {
|
|
window.addEventListener('test', null, options);
|
|
window.removeEventListener('test', null, options);
|
|
}
|
|
} catch (e) {
|
|
// continue regardless of error
|
|
}
|
|
return passiveSupported;
|
|
}();
|
|
/**
|
|
* The "used" size is the final value of a dimension property after all calculations have
|
|
* been performed. This method uses the computed style of `element` but returns undefined
|
|
* if the computed style is not expressed in pixels. That can happen in some cases where
|
|
* `element` has a size relative to its parent and this last one is not yet displayed,
|
|
* for example because of `display: none` on a parent node.
|
|
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value
|
|
* @returns Size in pixels or undefined if unknown.
|
|
*/ function readUsedSize(element, property) {
|
|
const value = getStyle(element, property);
|
|
const matches = value && value.match(/^(\d+)(\.\d+)?px$/);
|
|
return matches ? +matches[1] : undefined;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/ function _pointInLine(p1, p2, t, mode) {
|
|
return {
|
|
x: p1.x + t * (p2.x - p1.x),
|
|
y: p1.y + t * (p2.y - p1.y)
|
|
};
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _steppedInterpolation(p1, p2, t, mode) {
|
|
return {
|
|
x: p1.x + t * (p2.x - p1.x),
|
|
y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y : mode === 'after' ? t < 1 ? p1.y : p2.y : t > 0 ? p2.y : p1.y
|
|
};
|
|
}
|
|
/**
|
|
* @private
|
|
*/ function _bezierInterpolation(p1, p2, t, mode) {
|
|
const cp1 = {
|
|
x: p1.cp2x,
|
|
y: p1.cp2y
|
|
};
|
|
const cp2 = {
|
|
x: p2.cp1x,
|
|
y: p2.cp1y
|
|
};
|
|
const a = _pointInLine(p1, cp1, t);
|
|
const b = _pointInLine(cp1, cp2, t);
|
|
const c = _pointInLine(cp2, p2, t);
|
|
const d = _pointInLine(a, b, t);
|
|
const e = _pointInLine(b, c, t);
|
|
return _pointInLine(d, e, t);
|
|
}
|
|
|
|
const getRightToLeftAdapter = function(rectX, width) {
|
|
return {
|
|
x (x) {
|
|
return rectX + rectX + width - x;
|
|
},
|
|
setWidth (w) {
|
|
width = w;
|
|
},
|
|
textAlign (align) {
|
|
if (align === 'center') {
|
|
return align;
|
|
}
|
|
return align === 'right' ? 'left' : 'right';
|
|
},
|
|
xPlus (x, value) {
|
|
return x - value;
|
|
},
|
|
leftForLtr (x, itemWidth) {
|
|
return x - itemWidth;
|
|
}
|
|
};
|
|
};
|
|
const getLeftToRightAdapter = function() {
|
|
return {
|
|
x (x) {
|
|
return x;
|
|
},
|
|
setWidth (w) {},
|
|
textAlign (align) {
|
|
return align;
|
|
},
|
|
xPlus (x, value) {
|
|
return x + value;
|
|
},
|
|
leftForLtr (x, _itemWidth) {
|
|
return x;
|
|
}
|
|
};
|
|
};
|
|
function getRtlAdapter(rtl, rectX, width) {
|
|
return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter();
|
|
}
|
|
function overrideTextDirection(ctx, direction) {
|
|
let style, original;
|
|
if (direction === 'ltr' || direction === 'rtl') {
|
|
style = ctx.canvas.style;
|
|
original = [
|
|
style.getPropertyValue('direction'),
|
|
style.getPropertyPriority('direction')
|
|
];
|
|
style.setProperty('direction', direction, 'important');
|
|
ctx.prevTextDirection = original;
|
|
}
|
|
}
|
|
function restoreTextDirection(ctx, original) {
|
|
if (original !== undefined) {
|
|
delete ctx.prevTextDirection;
|
|
ctx.canvas.style.setProperty('direction', original[0], original[1]);
|
|
}
|
|
}
|
|
|
|
function propertyFn(property) {
|
|
if (property === 'angle') {
|
|
return {
|
|
between: _angleBetween,
|
|
compare: _angleDiff,
|
|
normalize: _normalizeAngle
|
|
};
|
|
}
|
|
return {
|
|
between: _isBetween,
|
|
compare: (a, b)=>a - b,
|
|
normalize: (x)=>x
|
|
};
|
|
}
|
|
function normalizeSegment({ start , end , count , loop , style }) {
|
|
return {
|
|
start: start % count,
|
|
end: end % count,
|
|
loop: loop && (end - start + 1) % count === 0,
|
|
style
|
|
};
|
|
}
|
|
function getSegment(segment, points, bounds) {
|
|
const { property , start: startBound , end: endBound } = bounds;
|
|
const { between , normalize } = propertyFn(property);
|
|
const count = points.length;
|
|
let { start , end , loop } = segment;
|
|
let i, ilen;
|
|
if (loop) {
|
|
start += count;
|
|
end += count;
|
|
for(i = 0, ilen = count; i < ilen; ++i){
|
|
if (!between(normalize(points[start % count][property]), startBound, endBound)) {
|
|
break;
|
|
}
|
|
start--;
|
|
end--;
|
|
}
|
|
start %= count;
|
|
end %= count;
|
|
}
|
|
if (end < start) {
|
|
end += count;
|
|
}
|
|
return {
|
|
start,
|
|
end,
|
|
loop,
|
|
style: segment.style
|
|
};
|
|
}
|
|
function _boundSegment(segment, points, bounds) {
|
|
if (!bounds) {
|
|
return [
|
|
segment
|
|
];
|
|
}
|
|
const { property , start: startBound , end: endBound } = bounds;
|
|
const count = points.length;
|
|
const { compare , between , normalize } = propertyFn(property);
|
|
const { start , end , loop , style } = getSegment(segment, points, bounds);
|
|
const result = [];
|
|
let inside = false;
|
|
let subStart = null;
|
|
let value, point, prevValue;
|
|
const startIsBefore = ()=>between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0;
|
|
const endIsBefore = ()=>compare(endBound, value) === 0 || between(endBound, prevValue, value);
|
|
const shouldStart = ()=>inside || startIsBefore();
|
|
const shouldStop = ()=>!inside || endIsBefore();
|
|
for(let i = start, prev = start; i <= end; ++i){
|
|
point = points[i % count];
|
|
if (point.skip) {
|
|
continue;
|
|
}
|
|
value = normalize(point[property]);
|
|
if (value === prevValue) {
|
|
continue;
|
|
}
|
|
inside = between(value, startBound, endBound);
|
|
if (subStart === null && shouldStart()) {
|
|
subStart = compare(value, startBound) === 0 ? i : prev;
|
|
}
|
|
if (subStart !== null && shouldStop()) {
|
|
result.push(normalizeSegment({
|
|
start: subStart,
|
|
end: i,
|
|
loop,
|
|
count,
|
|
style
|
|
}));
|
|
subStart = null;
|
|
}
|
|
prev = i;
|
|
prevValue = value;
|
|
}
|
|
if (subStart !== null) {
|
|
result.push(normalizeSegment({
|
|
start: subStart,
|
|
end,
|
|
loop,
|
|
count,
|
|
style
|
|
}));
|
|
}
|
|
return result;
|
|
}
|
|
function _boundSegments(line, bounds) {
|
|
const result = [];
|
|
const segments = line.segments;
|
|
for(let i = 0; i < segments.length; i++){
|
|
const sub = _boundSegment(segments[i], line.points, bounds);
|
|
if (sub.length) {
|
|
result.push(...sub);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function findStartAndEnd(points, count, loop, spanGaps) {
|
|
let start = 0;
|
|
let end = count - 1;
|
|
if (loop && !spanGaps) {
|
|
while(start < count && !points[start].skip){
|
|
start++;
|
|
}
|
|
}
|
|
while(start < count && points[start].skip){
|
|
start++;
|
|
}
|
|
start %= count;
|
|
if (loop) {
|
|
end += start;
|
|
}
|
|
while(end > start && points[end % count].skip){
|
|
end--;
|
|
}
|
|
end %= count;
|
|
return {
|
|
start,
|
|
end
|
|
};
|
|
}
|
|
function solidSegments(points, start, max, loop) {
|
|
const count = points.length;
|
|
const result = [];
|
|
let last = start;
|
|
let prev = points[start];
|
|
let end;
|
|
for(end = start + 1; end <= max; ++end){
|
|
const cur = points[end % count];
|
|
if (cur.skip || cur.stop) {
|
|
if (!prev.skip) {
|
|
loop = false;
|
|
result.push({
|
|
start: start % count,
|
|
end: (end - 1) % count,
|
|
loop
|
|
});
|
|
start = last = cur.stop ? end : null;
|
|
}
|
|
} else {
|
|
last = end;
|
|
if (prev.skip) {
|
|
start = end;
|
|
}
|
|
}
|
|
prev = cur;
|
|
}
|
|
if (last !== null) {
|
|
result.push({
|
|
start: start % count,
|
|
end: last % count,
|
|
loop
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
function _computeSegments(line, segmentOptions) {
|
|
const points = line.points;
|
|
const spanGaps = line.options.spanGaps;
|
|
const count = points.length;
|
|
if (!count) {
|
|
return [];
|
|
}
|
|
const loop = !!line._loop;
|
|
const { start , end } = findStartAndEnd(points, count, loop, spanGaps);
|
|
if (spanGaps === true) {
|
|
return splitByStyles(line, [
|
|
{
|
|
start,
|
|
end,
|
|
loop
|
|
}
|
|
], points, segmentOptions);
|
|
}
|
|
const max = end < start ? end + count : end;
|
|
const completeLoop = !!line._fullLoop && start === 0 && end === count - 1;
|
|
return splitByStyles(line, solidSegments(points, start, max, completeLoop), points, segmentOptions);
|
|
}
|
|
function splitByStyles(line, segments, points, segmentOptions) {
|
|
if (!segmentOptions || !segmentOptions.setContext || !points) {
|
|
return segments;
|
|
}
|
|
return doSplitByStyles(line, segments, points, segmentOptions);
|
|
}
|
|
function doSplitByStyles(line, segments, points, segmentOptions) {
|
|
const chartContext = line._chart.getContext();
|
|
const baseStyle = readStyle(line.options);
|
|
const { _datasetIndex: datasetIndex , options: { spanGaps } } = line;
|
|
const count = points.length;
|
|
const result = [];
|
|
let prevStyle = baseStyle;
|
|
let start = segments[0].start;
|
|
let i = start;
|
|
function addStyle(s, e, l, st) {
|
|
const dir = spanGaps ? -1 : 1;
|
|
if (s === e) {
|
|
return;
|
|
}
|
|
s += count;
|
|
while(points[s % count].skip){
|
|
s -= dir;
|
|
}
|
|
while(points[e % count].skip){
|
|
e += dir;
|
|
}
|
|
if (s % count !== e % count) {
|
|
result.push({
|
|
start: s % count,
|
|
end: e % count,
|
|
loop: l,
|
|
style: st
|
|
});
|
|
prevStyle = st;
|
|
start = e % count;
|
|
}
|
|
}
|
|
for (const segment of segments){
|
|
start = spanGaps ? start : segment.start;
|
|
let prev = points[start % count];
|
|
let style;
|
|
for(i = start + 1; i <= segment.end; i++){
|
|
const pt = points[i % count];
|
|
style = readStyle(segmentOptions.setContext(createContext(chartContext, {
|
|
type: 'segment',
|
|
p0: prev,
|
|
p1: pt,
|
|
p0DataIndex: (i - 1) % count,
|
|
p1DataIndex: i % count,
|
|
datasetIndex
|
|
})));
|
|
if (styleChanged(style, prevStyle)) {
|
|
addStyle(start, i - 1, segment.loop, prevStyle);
|
|
}
|
|
prev = pt;
|
|
prevStyle = style;
|
|
}
|
|
if (start < i - 1) {
|
|
addStyle(start, i - 1, segment.loop, prevStyle);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function readStyle(options) {
|
|
return {
|
|
backgroundColor: options.backgroundColor,
|
|
borderCapStyle: options.borderCapStyle,
|
|
borderDash: options.borderDash,
|
|
borderDashOffset: options.borderDashOffset,
|
|
borderJoinStyle: options.borderJoinStyle,
|
|
borderWidth: options.borderWidth,
|
|
borderColor: options.borderColor
|
|
};
|
|
}
|
|
function styleChanged(style, prevStyle) {
|
|
if (!prevStyle) {
|
|
return false;
|
|
}
|
|
const cache = [];
|
|
const replacer = function(key, value) {
|
|
if (!isPatternOrGradient(value)) {
|
|
return value;
|
|
}
|
|
if (!cache.includes(value)) {
|
|
cache.push(value);
|
|
}
|
|
return cache.indexOf(value);
|
|
};
|
|
return JSON.stringify(style, replacer) !== JSON.stringify(prevStyle, replacer);
|
|
}
|
|
|
|
function getSizeForArea(scale, chartArea, field) {
|
|
return scale.options.clip ? scale[field] : chartArea[field];
|
|
}
|
|
function getDatasetArea(meta, chartArea) {
|
|
const { xScale , yScale } = meta;
|
|
if (xScale && yScale) {
|
|
return {
|
|
left: getSizeForArea(xScale, chartArea, 'left'),
|
|
right: getSizeForArea(xScale, chartArea, 'right'),
|
|
top: getSizeForArea(yScale, chartArea, 'top'),
|
|
bottom: getSizeForArea(yScale, chartArea, 'bottom')
|
|
};
|
|
}
|
|
return chartArea;
|
|
}
|
|
function getDatasetClipArea(chart, meta) {
|
|
const clip = meta._clip;
|
|
if (clip.disabled) {
|
|
return false;
|
|
}
|
|
const area = getDatasetArea(meta, chart.chartArea);
|
|
return {
|
|
left: clip.left === false ? 0 : area.left - (clip.left === true ? 0 : clip.left),
|
|
right: clip.right === false ? chart.width : area.right + (clip.right === true ? 0 : clip.right),
|
|
top: clip.top === false ? 0 : area.top - (clip.top === true ? 0 : clip.top),
|
|
bottom: clip.bottom === false ? chart.height : area.bottom + (clip.bottom === true ? 0 : clip.bottom)
|
|
};
|
|
}
|
|
|
|
|
|
//# sourceMappingURL=helpers.dataset.js.map
|
|
|
|
;// ../../node_modules/chart.js/dist/chart.js
|
|
/*!
|
|
* Chart.js v4.5.1
|
|
* https://www.chartjs.org
|
|
* (c) 2025 Chart.js Contributors
|
|
* Released under the MIT License
|
|
*/
|
|
|
|
|
|
|
|
class Animator {
|
|
constructor(){
|
|
this._request = null;
|
|
this._charts = new Map();
|
|
this._running = false;
|
|
this._lastDate = undefined;
|
|
}
|
|
_notify(chart, anims, date, type) {
|
|
const callbacks = anims.listeners[type];
|
|
const numSteps = anims.duration;
|
|
callbacks.forEach((fn)=>fn({
|
|
chart,
|
|
initial: anims.initial,
|
|
numSteps,
|
|
currentStep: Math.min(date - anims.start, numSteps)
|
|
}));
|
|
}
|
|
_refresh() {
|
|
if (this._request) {
|
|
return;
|
|
}
|
|
this._running = true;
|
|
this._request = requestAnimFrame.call(window, ()=>{
|
|
this._update();
|
|
this._request = null;
|
|
if (this._running) {
|
|
this._refresh();
|
|
}
|
|
});
|
|
}
|
|
_update(date = Date.now()) {
|
|
let remaining = 0;
|
|
this._charts.forEach((anims, chart)=>{
|
|
if (!anims.running || !anims.items.length) {
|
|
return;
|
|
}
|
|
const items = anims.items;
|
|
let i = items.length - 1;
|
|
let draw = false;
|
|
let item;
|
|
for(; i >= 0; --i){
|
|
item = items[i];
|
|
if (item._active) {
|
|
if (item._total > anims.duration) {
|
|
anims.duration = item._total;
|
|
}
|
|
item.tick(date);
|
|
draw = true;
|
|
} else {
|
|
items[i] = items[items.length - 1];
|
|
items.pop();
|
|
}
|
|
}
|
|
if (draw) {
|
|
chart.draw();
|
|
this._notify(chart, anims, date, 'progress');
|
|
}
|
|
if (!items.length) {
|
|
anims.running = false;
|
|
this._notify(chart, anims, date, 'complete');
|
|
anims.initial = false;
|
|
}
|
|
remaining += items.length;
|
|
});
|
|
this._lastDate = date;
|
|
if (remaining === 0) {
|
|
this._running = false;
|
|
}
|
|
}
|
|
_getAnims(chart) {
|
|
const charts = this._charts;
|
|
let anims = charts.get(chart);
|
|
if (!anims) {
|
|
anims = {
|
|
running: false,
|
|
initial: true,
|
|
items: [],
|
|
listeners: {
|
|
complete: [],
|
|
progress: []
|
|
}
|
|
};
|
|
charts.set(chart, anims);
|
|
}
|
|
return anims;
|
|
}
|
|
listen(chart, event, cb) {
|
|
this._getAnims(chart).listeners[event].push(cb);
|
|
}
|
|
add(chart, items) {
|
|
if (!items || !items.length) {
|
|
return;
|
|
}
|
|
this._getAnims(chart).items.push(...items);
|
|
}
|
|
has(chart) {
|
|
return this._getAnims(chart).items.length > 0;
|
|
}
|
|
start(chart) {
|
|
const anims = this._charts.get(chart);
|
|
if (!anims) {
|
|
return;
|
|
}
|
|
anims.running = true;
|
|
anims.start = Date.now();
|
|
anims.duration = anims.items.reduce((acc, cur)=>Math.max(acc, cur._duration), 0);
|
|
this._refresh();
|
|
}
|
|
running(chart) {
|
|
if (!this._running) {
|
|
return false;
|
|
}
|
|
const anims = this._charts.get(chart);
|
|
if (!anims || !anims.running || !anims.items.length) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
stop(chart) {
|
|
const anims = this._charts.get(chart);
|
|
if (!anims || !anims.items.length) {
|
|
return;
|
|
}
|
|
const items = anims.items;
|
|
let i = items.length - 1;
|
|
for(; i >= 0; --i){
|
|
items[i].cancel();
|
|
}
|
|
anims.items = [];
|
|
this._notify(chart, anims, Date.now(), 'complete');
|
|
}
|
|
remove(chart) {
|
|
return this._charts.delete(chart);
|
|
}
|
|
}
|
|
var animator = /* #__PURE__ */ new Animator();
|
|
|
|
const transparent = 'transparent';
|
|
const interpolators = {
|
|
boolean (from, to, factor) {
|
|
return factor > 0.5 ? to : from;
|
|
},
|
|
color (from, to, factor) {
|
|
const c0 = color(from || transparent);
|
|
const c1 = c0.valid && color(to || transparent);
|
|
return c1 && c1.valid ? c1.mix(c0, factor).hexString() : to;
|
|
},
|
|
number (from, to, factor) {
|
|
return from + (to - from) * factor;
|
|
}
|
|
};
|
|
class Animation {
|
|
constructor(cfg, target, prop, to){
|
|
const currentValue = target[prop];
|
|
to = resolve([
|
|
cfg.to,
|
|
to,
|
|
currentValue,
|
|
cfg.from
|
|
]);
|
|
const from = resolve([
|
|
cfg.from,
|
|
currentValue,
|
|
to
|
|
]);
|
|
this._active = true;
|
|
this._fn = cfg.fn || interpolators[cfg.type || typeof from];
|
|
this._easing = effects[cfg.easing] || effects.linear;
|
|
this._start = Math.floor(Date.now() + (cfg.delay || 0));
|
|
this._duration = this._total = Math.floor(cfg.duration);
|
|
this._loop = !!cfg.loop;
|
|
this._target = target;
|
|
this._prop = prop;
|
|
this._from = from;
|
|
this._to = to;
|
|
this._promises = undefined;
|
|
}
|
|
active() {
|
|
return this._active;
|
|
}
|
|
update(cfg, to, date) {
|
|
if (this._active) {
|
|
this._notify(false);
|
|
const currentValue = this._target[this._prop];
|
|
const elapsed = date - this._start;
|
|
const remain = this._duration - elapsed;
|
|
this._start = date;
|
|
this._duration = Math.floor(Math.max(remain, cfg.duration));
|
|
this._total += elapsed;
|
|
this._loop = !!cfg.loop;
|
|
this._to = resolve([
|
|
cfg.to,
|
|
to,
|
|
currentValue,
|
|
cfg.from
|
|
]);
|
|
this._from = resolve([
|
|
cfg.from,
|
|
currentValue,
|
|
to
|
|
]);
|
|
}
|
|
}
|
|
cancel() {
|
|
if (this._active) {
|
|
this.tick(Date.now());
|
|
this._active = false;
|
|
this._notify(false);
|
|
}
|
|
}
|
|
tick(date) {
|
|
const elapsed = date - this._start;
|
|
const duration = this._duration;
|
|
const prop = this._prop;
|
|
const from = this._from;
|
|
const loop = this._loop;
|
|
const to = this._to;
|
|
let factor;
|
|
this._active = from !== to && (loop || elapsed < duration);
|
|
if (!this._active) {
|
|
this._target[prop] = to;
|
|
this._notify(true);
|
|
return;
|
|
}
|
|
if (elapsed < 0) {
|
|
this._target[prop] = from;
|
|
return;
|
|
}
|
|
factor = elapsed / duration % 2;
|
|
factor = loop && factor > 1 ? 2 - factor : factor;
|
|
factor = this._easing(Math.min(1, Math.max(0, factor)));
|
|
this._target[prop] = this._fn(from, to, factor);
|
|
}
|
|
wait() {
|
|
const promises = this._promises || (this._promises = []);
|
|
return new Promise((res, rej)=>{
|
|
promises.push({
|
|
res,
|
|
rej
|
|
});
|
|
});
|
|
}
|
|
_notify(resolved) {
|
|
const method = resolved ? 'res' : 'rej';
|
|
const promises = this._promises || [];
|
|
for(let i = 0; i < promises.length; i++){
|
|
promises[i][method]();
|
|
}
|
|
}
|
|
}
|
|
|
|
class Animations {
|
|
constructor(chart, config){
|
|
this._chart = chart;
|
|
this._properties = new Map();
|
|
this.configure(config);
|
|
}
|
|
configure(config) {
|
|
if (!isObject(config)) {
|
|
return;
|
|
}
|
|
const animationOptions = Object.keys(defaults.animation);
|
|
const animatedProps = this._properties;
|
|
Object.getOwnPropertyNames(config).forEach((key)=>{
|
|
const cfg = config[key];
|
|
if (!isObject(cfg)) {
|
|
return;
|
|
}
|
|
const resolved = {};
|
|
for (const option of animationOptions){
|
|
resolved[option] = cfg[option];
|
|
}
|
|
(isArray(cfg.properties) && cfg.properties || [
|
|
key
|
|
]).forEach((prop)=>{
|
|
if (prop === key || !animatedProps.has(prop)) {
|
|
animatedProps.set(prop, resolved);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
_animateOptions(target, values) {
|
|
const newOptions = values.options;
|
|
const options = resolveTargetOptions(target, newOptions);
|
|
if (!options) {
|
|
return [];
|
|
}
|
|
const animations = this._createAnimations(options, newOptions);
|
|
if (newOptions.$shared) {
|
|
awaitAll(target.options.$animations, newOptions).then(()=>{
|
|
target.options = newOptions;
|
|
}, ()=>{
|
|
});
|
|
}
|
|
return animations;
|
|
}
|
|
_createAnimations(target, values) {
|
|
const animatedProps = this._properties;
|
|
const animations = [];
|
|
const running = target.$animations || (target.$animations = {});
|
|
const props = Object.keys(values);
|
|
const date = Date.now();
|
|
let i;
|
|
for(i = props.length - 1; i >= 0; --i){
|
|
const prop = props[i];
|
|
if (prop.charAt(0) === '$') {
|
|
continue;
|
|
}
|
|
if (prop === 'options') {
|
|
animations.push(...this._animateOptions(target, values));
|
|
continue;
|
|
}
|
|
const value = values[prop];
|
|
let animation = running[prop];
|
|
const cfg = animatedProps.get(prop);
|
|
if (animation) {
|
|
if (cfg && animation.active()) {
|
|
animation.update(cfg, value, date);
|
|
continue;
|
|
} else {
|
|
animation.cancel();
|
|
}
|
|
}
|
|
if (!cfg || !cfg.duration) {
|
|
target[prop] = value;
|
|
continue;
|
|
}
|
|
running[prop] = animation = new Animation(cfg, target, prop, value);
|
|
animations.push(animation);
|
|
}
|
|
return animations;
|
|
}
|
|
update(target, values) {
|
|
if (this._properties.size === 0) {
|
|
Object.assign(target, values);
|
|
return;
|
|
}
|
|
const animations = this._createAnimations(target, values);
|
|
if (animations.length) {
|
|
animator.add(this._chart, animations);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
function awaitAll(animations, properties) {
|
|
const running = [];
|
|
const keys = Object.keys(properties);
|
|
for(let i = 0; i < keys.length; i++){
|
|
const anim = animations[keys[i]];
|
|
if (anim && anim.active()) {
|
|
running.push(anim.wait());
|
|
}
|
|
}
|
|
return Promise.all(running);
|
|
}
|
|
function resolveTargetOptions(target, newOptions) {
|
|
if (!newOptions) {
|
|
return;
|
|
}
|
|
let options = target.options;
|
|
if (!options) {
|
|
target.options = newOptions;
|
|
return;
|
|
}
|
|
if (options.$shared) {
|
|
target.options = options = Object.assign({}, options, {
|
|
$shared: false,
|
|
$animations: {}
|
|
});
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function scaleClip(scale, allowedOverflow) {
|
|
const opts = scale && scale.options || {};
|
|
const reverse = opts.reverse;
|
|
const min = opts.min === undefined ? allowedOverflow : 0;
|
|
const max = opts.max === undefined ? allowedOverflow : 0;
|
|
return {
|
|
start: reverse ? max : min,
|
|
end: reverse ? min : max
|
|
};
|
|
}
|
|
function defaultClip(xScale, yScale, allowedOverflow) {
|
|
if (allowedOverflow === false) {
|
|
return false;
|
|
}
|
|
const x = scaleClip(xScale, allowedOverflow);
|
|
const y = scaleClip(yScale, allowedOverflow);
|
|
return {
|
|
top: y.end,
|
|
right: x.end,
|
|
bottom: y.start,
|
|
left: x.start
|
|
};
|
|
}
|
|
function toClip(value) {
|
|
let t, r, b, l;
|
|
if (isObject(value)) {
|
|
t = value.top;
|
|
r = value.right;
|
|
b = value.bottom;
|
|
l = value.left;
|
|
} else {
|
|
t = r = b = l = value;
|
|
}
|
|
return {
|
|
top: t,
|
|
right: r,
|
|
bottom: b,
|
|
left: l,
|
|
disabled: value === false
|
|
};
|
|
}
|
|
function getSortedDatasetIndices(chart, filterVisible) {
|
|
const keys = [];
|
|
const metasets = chart._getSortedDatasetMetas(filterVisible);
|
|
let i, ilen;
|
|
for(i = 0, ilen = metasets.length; i < ilen; ++i){
|
|
keys.push(metasets[i].index);
|
|
}
|
|
return keys;
|
|
}
|
|
function applyStack(stack, value, dsIndex, options = {}) {
|
|
const keys = stack.keys;
|
|
const singleMode = options.mode === 'single';
|
|
let i, ilen, datasetIndex, otherValue;
|
|
if (value === null) {
|
|
return;
|
|
}
|
|
let found = false;
|
|
for(i = 0, ilen = keys.length; i < ilen; ++i){
|
|
datasetIndex = +keys[i];
|
|
if (datasetIndex === dsIndex) {
|
|
found = true;
|
|
if (options.all) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
otherValue = stack.values[datasetIndex];
|
|
if (isNumberFinite(otherValue) && (singleMode || value === 0 || sign(value) === sign(otherValue))) {
|
|
value += otherValue;
|
|
}
|
|
}
|
|
if (!found && !options.all) {
|
|
return 0;
|
|
}
|
|
return value;
|
|
}
|
|
function convertObjectDataToArray(data, meta) {
|
|
const { iScale , vScale } = meta;
|
|
const iAxisKey = iScale.axis === 'x' ? 'x' : 'y';
|
|
const vAxisKey = vScale.axis === 'x' ? 'x' : 'y';
|
|
const keys = Object.keys(data);
|
|
const adata = new Array(keys.length);
|
|
let i, ilen, key;
|
|
for(i = 0, ilen = keys.length; i < ilen; ++i){
|
|
key = keys[i];
|
|
adata[i] = {
|
|
[iAxisKey]: key,
|
|
[vAxisKey]: data[key]
|
|
};
|
|
}
|
|
return adata;
|
|
}
|
|
function isStacked(scale, meta) {
|
|
const stacked = scale && scale.options.stacked;
|
|
return stacked || stacked === undefined && meta.stack !== undefined;
|
|
}
|
|
function getStackKey(indexScale, valueScale, meta) {
|
|
return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`;
|
|
}
|
|
function getUserBounds(scale) {
|
|
const { min , max , minDefined , maxDefined } = scale.getUserBounds();
|
|
return {
|
|
min: minDefined ? min : Number.NEGATIVE_INFINITY,
|
|
max: maxDefined ? max : Number.POSITIVE_INFINITY
|
|
};
|
|
}
|
|
function getOrCreateStack(stacks, stackKey, indexValue) {
|
|
const subStack = stacks[stackKey] || (stacks[stackKey] = {});
|
|
return subStack[indexValue] || (subStack[indexValue] = {});
|
|
}
|
|
function getLastIndexInStack(stack, vScale, positive, type) {
|
|
for (const meta of vScale.getMatchingVisibleMetas(type).reverse()){
|
|
const value = stack[meta.index];
|
|
if (positive && value > 0 || !positive && value < 0) {
|
|
return meta.index;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function updateStacks(controller, parsed) {
|
|
const { chart , _cachedMeta: meta } = controller;
|
|
const stacks = chart._stacks || (chart._stacks = {});
|
|
const { iScale , vScale , index: datasetIndex } = meta;
|
|
const iAxis = iScale.axis;
|
|
const vAxis = vScale.axis;
|
|
const key = getStackKey(iScale, vScale, meta);
|
|
const ilen = parsed.length;
|
|
let stack;
|
|
for(let i = 0; i < ilen; ++i){
|
|
const item = parsed[i];
|
|
const { [iAxis]: index , [vAxis]: value } = item;
|
|
const itemStacks = item._stacks || (item._stacks = {});
|
|
stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index);
|
|
stack[datasetIndex] = value;
|
|
stack._top = getLastIndexInStack(stack, vScale, true, meta.type);
|
|
stack._bottom = getLastIndexInStack(stack, vScale, false, meta.type);
|
|
const visualValues = stack._visualValues || (stack._visualValues = {});
|
|
visualValues[datasetIndex] = value;
|
|
}
|
|
}
|
|
function getFirstScaleId(chart, axis) {
|
|
const scales = chart.scales;
|
|
return Object.keys(scales).filter((key)=>scales[key].axis === axis).shift();
|
|
}
|
|
function createDatasetContext(parent, index) {
|
|
return createContext(parent, {
|
|
active: false,
|
|
dataset: undefined,
|
|
datasetIndex: index,
|
|
index,
|
|
mode: 'default',
|
|
type: 'dataset'
|
|
});
|
|
}
|
|
function createDataContext(parent, index, element) {
|
|
return createContext(parent, {
|
|
active: false,
|
|
dataIndex: index,
|
|
parsed: undefined,
|
|
raw: undefined,
|
|
element,
|
|
index,
|
|
mode: 'default',
|
|
type: 'data'
|
|
});
|
|
}
|
|
function clearStacks(meta, items) {
|
|
const datasetIndex = meta.controller.index;
|
|
const axis = meta.vScale && meta.vScale.axis;
|
|
if (!axis) {
|
|
return;
|
|
}
|
|
items = items || meta._parsed;
|
|
for (const parsed of items){
|
|
const stacks = parsed._stacks;
|
|
if (!stacks || stacks[axis] === undefined || stacks[axis][datasetIndex] === undefined) {
|
|
return;
|
|
}
|
|
delete stacks[axis][datasetIndex];
|
|
if (stacks[axis]._visualValues !== undefined && stacks[axis]._visualValues[datasetIndex] !== undefined) {
|
|
delete stacks[axis]._visualValues[datasetIndex];
|
|
}
|
|
}
|
|
}
|
|
const isDirectUpdateMode = (mode)=>mode === 'reset' || mode === 'none';
|
|
const cloneIfNotShared = (cached, shared)=>shared ? cached : Object.assign({}, cached);
|
|
const createStack = (canStack, meta, chart)=>canStack && !meta.hidden && meta._stacked && {
|
|
keys: getSortedDatasetIndices(chart, true),
|
|
values: null
|
|
};
|
|
class DatasetController {
|
|
static defaults = {};
|
|
static datasetElementType = null;
|
|
static dataElementType = null;
|
|
constructor(chart, datasetIndex){
|
|
this.chart = chart;
|
|
this._ctx = chart.ctx;
|
|
this.index = datasetIndex;
|
|
this._cachedDataOpts = {};
|
|
this._cachedMeta = this.getMeta();
|
|
this._type = this._cachedMeta.type;
|
|
this.options = undefined;
|
|
this._parsing = false;
|
|
this._data = undefined;
|
|
this._objectData = undefined;
|
|
this._sharedOptions = undefined;
|
|
this._drawStart = undefined;
|
|
this._drawCount = undefined;
|
|
this.enableOptionSharing = false;
|
|
this.supportsDecimation = false;
|
|
this.$context = undefined;
|
|
this._syncList = [];
|
|
this.datasetElementType = new.target.datasetElementType;
|
|
this.dataElementType = new.target.dataElementType;
|
|
this.initialize();
|
|
}
|
|
initialize() {
|
|
const meta = this._cachedMeta;
|
|
this.configure();
|
|
this.linkScales();
|
|
meta._stacked = isStacked(meta.vScale, meta);
|
|
this.addElements();
|
|
if (this.options.fill && !this.chart.isPluginEnabled('filler')) {
|
|
console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options");
|
|
}
|
|
}
|
|
updateIndex(datasetIndex) {
|
|
if (this.index !== datasetIndex) {
|
|
clearStacks(this._cachedMeta);
|
|
}
|
|
this.index = datasetIndex;
|
|
}
|
|
linkScales() {
|
|
const chart = this.chart;
|
|
const meta = this._cachedMeta;
|
|
const dataset = this.getDataset();
|
|
const chooseId = (axis, x, y, r)=>axis === 'x' ? x : axis === 'r' ? r : y;
|
|
const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x'));
|
|
const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y'));
|
|
const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r'));
|
|
const indexAxis = meta.indexAxis;
|
|
const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid);
|
|
const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid);
|
|
meta.xScale = this.getScaleForId(xid);
|
|
meta.yScale = this.getScaleForId(yid);
|
|
meta.rScale = this.getScaleForId(rid);
|
|
meta.iScale = this.getScaleForId(iid);
|
|
meta.vScale = this.getScaleForId(vid);
|
|
}
|
|
getDataset() {
|
|
return this.chart.data.datasets[this.index];
|
|
}
|
|
getMeta() {
|
|
return this.chart.getDatasetMeta(this.index);
|
|
}
|
|
getScaleForId(scaleID) {
|
|
return this.chart.scales[scaleID];
|
|
}
|
|
_getOtherScale(scale) {
|
|
const meta = this._cachedMeta;
|
|
return scale === meta.iScale ? meta.vScale : meta.iScale;
|
|
}
|
|
reset() {
|
|
this._update('reset');
|
|
}
|
|
_destroy() {
|
|
const meta = this._cachedMeta;
|
|
if (this._data) {
|
|
unlistenArrayEvents(this._data, this);
|
|
}
|
|
if (meta._stacked) {
|
|
clearStacks(meta);
|
|
}
|
|
}
|
|
_dataCheck() {
|
|
const dataset = this.getDataset();
|
|
const data = dataset.data || (dataset.data = []);
|
|
const _data = this._data;
|
|
if (isObject(data)) {
|
|
const meta = this._cachedMeta;
|
|
this._data = convertObjectDataToArray(data, meta);
|
|
} else if (_data !== data) {
|
|
if (_data) {
|
|
unlistenArrayEvents(_data, this);
|
|
const meta = this._cachedMeta;
|
|
clearStacks(meta);
|
|
meta._parsed = [];
|
|
}
|
|
if (data && Object.isExtensible(data)) {
|
|
listenArrayEvents(data, this);
|
|
}
|
|
this._syncList = [];
|
|
this._data = data;
|
|
}
|
|
}
|
|
addElements() {
|
|
const meta = this._cachedMeta;
|
|
this._dataCheck();
|
|
if (this.datasetElementType) {
|
|
meta.dataset = new this.datasetElementType();
|
|
}
|
|
}
|
|
buildOrUpdateElements(resetNewElements) {
|
|
const meta = this._cachedMeta;
|
|
const dataset = this.getDataset();
|
|
let stackChanged = false;
|
|
this._dataCheck();
|
|
const oldStacked = meta._stacked;
|
|
meta._stacked = isStacked(meta.vScale, meta);
|
|
if (meta.stack !== dataset.stack) {
|
|
stackChanged = true;
|
|
clearStacks(meta);
|
|
meta.stack = dataset.stack;
|
|
}
|
|
this._resyncElements(resetNewElements);
|
|
if (stackChanged || oldStacked !== meta._stacked) {
|
|
updateStacks(this, meta._parsed);
|
|
meta._stacked = isStacked(meta.vScale, meta);
|
|
}
|
|
}
|
|
configure() {
|
|
const config = this.chart.config;
|
|
const scopeKeys = config.datasetScopeKeys(this._type);
|
|
const scopes = config.getOptionScopes(this.getDataset(), scopeKeys, true);
|
|
this.options = config.createResolver(scopes, this.getContext());
|
|
this._parsing = this.options.parsing;
|
|
this._cachedDataOpts = {};
|
|
}
|
|
parse(start, count) {
|
|
const { _cachedMeta: meta , _data: data } = this;
|
|
const { iScale , _stacked } = meta;
|
|
const iAxis = iScale.axis;
|
|
let sorted = start === 0 && count === data.length ? true : meta._sorted;
|
|
let prev = start > 0 && meta._parsed[start - 1];
|
|
let i, cur, parsed;
|
|
if (this._parsing === false) {
|
|
meta._parsed = data;
|
|
meta._sorted = true;
|
|
parsed = data;
|
|
} else {
|
|
if (isArray(data[start])) {
|
|
parsed = this.parseArrayData(meta, data, start, count);
|
|
} else if (isObject(data[start])) {
|
|
parsed = this.parseObjectData(meta, data, start, count);
|
|
} else {
|
|
parsed = this.parsePrimitiveData(meta, data, start, count);
|
|
}
|
|
const isNotInOrderComparedToPrev = ()=>cur[iAxis] === null || prev && cur[iAxis] < prev[iAxis];
|
|
for(i = 0; i < count; ++i){
|
|
meta._parsed[i + start] = cur = parsed[i];
|
|
if (sorted) {
|
|
if (isNotInOrderComparedToPrev()) {
|
|
sorted = false;
|
|
}
|
|
prev = cur;
|
|
}
|
|
}
|
|
meta._sorted = sorted;
|
|
}
|
|
if (_stacked) {
|
|
updateStacks(this, parsed);
|
|
}
|
|
}
|
|
parsePrimitiveData(meta, data, start, count) {
|
|
const { iScale , vScale } = meta;
|
|
const iAxis = iScale.axis;
|
|
const vAxis = vScale.axis;
|
|
const labels = iScale.getLabels();
|
|
const singleScale = iScale === vScale;
|
|
const parsed = new Array(count);
|
|
let i, ilen, index;
|
|
for(i = 0, ilen = count; i < ilen; ++i){
|
|
index = i + start;
|
|
parsed[i] = {
|
|
[iAxis]: singleScale || iScale.parse(labels[index], index),
|
|
[vAxis]: vScale.parse(data[index], index)
|
|
};
|
|
}
|
|
return parsed;
|
|
}
|
|
parseArrayData(meta, data, start, count) {
|
|
const { xScale , yScale } = meta;
|
|
const parsed = new Array(count);
|
|
let i, ilen, index, item;
|
|
for(i = 0, ilen = count; i < ilen; ++i){
|
|
index = i + start;
|
|
item = data[index];
|
|
parsed[i] = {
|
|
x: xScale.parse(item[0], index),
|
|
y: yScale.parse(item[1], index)
|
|
};
|
|
}
|
|
return parsed;
|
|
}
|
|
parseObjectData(meta, data, start, count) {
|
|
const { xScale , yScale } = meta;
|
|
const { xAxisKey ='x' , yAxisKey ='y' } = this._parsing;
|
|
const parsed = new Array(count);
|
|
let i, ilen, index, item;
|
|
for(i = 0, ilen = count; i < ilen; ++i){
|
|
index = i + start;
|
|
item = data[index];
|
|
parsed[i] = {
|
|
x: xScale.parse(resolveObjectKey(item, xAxisKey), index),
|
|
y: yScale.parse(resolveObjectKey(item, yAxisKey), index)
|
|
};
|
|
}
|
|
return parsed;
|
|
}
|
|
getParsed(index) {
|
|
return this._cachedMeta._parsed[index];
|
|
}
|
|
getDataElement(index) {
|
|
return this._cachedMeta.data[index];
|
|
}
|
|
applyStack(scale, parsed, mode) {
|
|
const chart = this.chart;
|
|
const meta = this._cachedMeta;
|
|
const value = parsed[scale.axis];
|
|
const stack = {
|
|
keys: getSortedDatasetIndices(chart, true),
|
|
values: parsed._stacks[scale.axis]._visualValues
|
|
};
|
|
return applyStack(stack, value, meta.index, {
|
|
mode
|
|
});
|
|
}
|
|
updateRangeFromParsed(range, scale, parsed, stack) {
|
|
const parsedValue = parsed[scale.axis];
|
|
let value = parsedValue === null ? NaN : parsedValue;
|
|
const values = stack && parsed._stacks[scale.axis];
|
|
if (stack && values) {
|
|
stack.values = values;
|
|
value = applyStack(stack, parsedValue, this._cachedMeta.index);
|
|
}
|
|
range.min = Math.min(range.min, value);
|
|
range.max = Math.max(range.max, value);
|
|
}
|
|
getMinMax(scale, canStack) {
|
|
const meta = this._cachedMeta;
|
|
const _parsed = meta._parsed;
|
|
const sorted = meta._sorted && scale === meta.iScale;
|
|
const ilen = _parsed.length;
|
|
const otherScale = this._getOtherScale(scale);
|
|
const stack = createStack(canStack, meta, this.chart);
|
|
const range = {
|
|
min: Number.POSITIVE_INFINITY,
|
|
max: Number.NEGATIVE_INFINITY
|
|
};
|
|
const { min: otherMin , max: otherMax } = getUserBounds(otherScale);
|
|
let i, parsed;
|
|
function _skip() {
|
|
parsed = _parsed[i];
|
|
const otherValue = parsed[otherScale.axis];
|
|
return !isNumberFinite(parsed[scale.axis]) || otherMin > otherValue || otherMax < otherValue;
|
|
}
|
|
for(i = 0; i < ilen; ++i){
|
|
if (_skip()) {
|
|
continue;
|
|
}
|
|
this.updateRangeFromParsed(range, scale, parsed, stack);
|
|
if (sorted) {
|
|
break;
|
|
}
|
|
}
|
|
if (sorted) {
|
|
for(i = ilen - 1; i >= 0; --i){
|
|
if (_skip()) {
|
|
continue;
|
|
}
|
|
this.updateRangeFromParsed(range, scale, parsed, stack);
|
|
break;
|
|
}
|
|
}
|
|
return range;
|
|
}
|
|
getAllParsedValues(scale) {
|
|
const parsed = this._cachedMeta._parsed;
|
|
const values = [];
|
|
let i, ilen, value;
|
|
for(i = 0, ilen = parsed.length; i < ilen; ++i){
|
|
value = parsed[i][scale.axis];
|
|
if (isNumberFinite(value)) {
|
|
values.push(value);
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
getMaxOverflow() {
|
|
return false;
|
|
}
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const iScale = meta.iScale;
|
|
const vScale = meta.vScale;
|
|
const parsed = this.getParsed(index);
|
|
return {
|
|
label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '',
|
|
value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : ''
|
|
};
|
|
}
|
|
_update(mode) {
|
|
const meta = this._cachedMeta;
|
|
this.update(mode || 'default');
|
|
meta._clip = toClip(valueOrDefault(this.options.clip, defaultClip(meta.xScale, meta.yScale, this.getMaxOverflow())));
|
|
}
|
|
update(mode) {}
|
|
draw() {
|
|
const ctx = this._ctx;
|
|
const chart = this.chart;
|
|
const meta = this._cachedMeta;
|
|
const elements = meta.data || [];
|
|
const area = chart.chartArea;
|
|
const active = [];
|
|
const start = this._drawStart || 0;
|
|
const count = this._drawCount || elements.length - start;
|
|
const drawActiveElementsOnTop = this.options.drawActiveElementsOnTop;
|
|
let i;
|
|
if (meta.dataset) {
|
|
meta.dataset.draw(ctx, area, start, count);
|
|
}
|
|
for(i = start; i < start + count; ++i){
|
|
const element = elements[i];
|
|
if (element.hidden) {
|
|
continue;
|
|
}
|
|
if (element.active && drawActiveElementsOnTop) {
|
|
active.push(element);
|
|
} else {
|
|
element.draw(ctx, area);
|
|
}
|
|
}
|
|
for(i = 0; i < active.length; ++i){
|
|
active[i].draw(ctx, area);
|
|
}
|
|
}
|
|
getStyle(index, active) {
|
|
const mode = active ? 'active' : 'default';
|
|
return index === undefined && this._cachedMeta.dataset ? this.resolveDatasetElementOptions(mode) : this.resolveDataElementOptions(index || 0, mode);
|
|
}
|
|
getContext(index, active, mode) {
|
|
const dataset = this.getDataset();
|
|
let context;
|
|
if (index >= 0 && index < this._cachedMeta.data.length) {
|
|
const element = this._cachedMeta.data[index];
|
|
context = element.$context || (element.$context = createDataContext(this.getContext(), index, element));
|
|
context.parsed = this.getParsed(index);
|
|
context.raw = dataset.data[index];
|
|
context.index = context.dataIndex = index;
|
|
} else {
|
|
context = this.$context || (this.$context = createDatasetContext(this.chart.getContext(), this.index));
|
|
context.dataset = dataset;
|
|
context.index = context.datasetIndex = this.index;
|
|
}
|
|
context.active = !!active;
|
|
context.mode = mode;
|
|
return context;
|
|
}
|
|
resolveDatasetElementOptions(mode) {
|
|
return this._resolveElementOptions(this.datasetElementType.id, mode);
|
|
}
|
|
resolveDataElementOptions(index, mode) {
|
|
return this._resolveElementOptions(this.dataElementType.id, mode, index);
|
|
}
|
|
_resolveElementOptions(elementType, mode = 'default', index) {
|
|
const active = mode === 'active';
|
|
const cache = this._cachedDataOpts;
|
|
const cacheKey = elementType + '-' + mode;
|
|
const cached = cache[cacheKey];
|
|
const sharing = this.enableOptionSharing && defined(index);
|
|
if (cached) {
|
|
return cloneIfNotShared(cached, sharing);
|
|
}
|
|
const config = this.chart.config;
|
|
const scopeKeys = config.datasetElementScopeKeys(this._type, elementType);
|
|
const prefixes = active ? [
|
|
`${elementType}Hover`,
|
|
'hover',
|
|
elementType,
|
|
''
|
|
] : [
|
|
elementType,
|
|
''
|
|
];
|
|
const scopes = config.getOptionScopes(this.getDataset(), scopeKeys);
|
|
const names = Object.keys(defaults.elements[elementType]);
|
|
const context = ()=>this.getContext(index, active, mode);
|
|
const values = config.resolveNamedOptions(scopes, names, context, prefixes);
|
|
if (values.$shared) {
|
|
values.$shared = sharing;
|
|
cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing));
|
|
}
|
|
return values;
|
|
}
|
|
_resolveAnimations(index, transition, active) {
|
|
const chart = this.chart;
|
|
const cache = this._cachedDataOpts;
|
|
const cacheKey = `animation-${transition}`;
|
|
const cached = cache[cacheKey];
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
let options;
|
|
if (chart.options.animation !== false) {
|
|
const config = this.chart.config;
|
|
const scopeKeys = config.datasetAnimationScopeKeys(this._type, transition);
|
|
const scopes = config.getOptionScopes(this.getDataset(), scopeKeys);
|
|
options = config.createResolver(scopes, this.getContext(index, active, transition));
|
|
}
|
|
const animations = new Animations(chart, options && options.animations);
|
|
if (options && options._cacheable) {
|
|
cache[cacheKey] = Object.freeze(animations);
|
|
}
|
|
return animations;
|
|
}
|
|
getSharedOptions(options) {
|
|
if (!options.$shared) {
|
|
return;
|
|
}
|
|
return this._sharedOptions || (this._sharedOptions = Object.assign({}, options));
|
|
}
|
|
includeOptions(mode, sharedOptions) {
|
|
return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled;
|
|
}
|
|
_getSharedOptions(start, mode) {
|
|
const firstOpts = this.resolveDataElementOptions(start, mode);
|
|
const previouslySharedOptions = this._sharedOptions;
|
|
const sharedOptions = this.getSharedOptions(firstOpts);
|
|
const includeOptions = this.includeOptions(mode, sharedOptions) || sharedOptions !== previouslySharedOptions;
|
|
this.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
return {
|
|
sharedOptions,
|
|
includeOptions
|
|
};
|
|
}
|
|
updateElement(element, index, properties, mode) {
|
|
if (isDirectUpdateMode(mode)) {
|
|
Object.assign(element, properties);
|
|
} else {
|
|
this._resolveAnimations(index, mode).update(element, properties);
|
|
}
|
|
}
|
|
updateSharedOptions(sharedOptions, mode, newOptions) {
|
|
if (sharedOptions && !isDirectUpdateMode(mode)) {
|
|
this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions);
|
|
}
|
|
}
|
|
_setStyle(element, index, mode, active) {
|
|
element.active = active;
|
|
const options = this.getStyle(index, active);
|
|
this._resolveAnimations(index, mode, active).update(element, {
|
|
options: !active && this.getSharedOptions(options) || options
|
|
});
|
|
}
|
|
removeHoverStyle(element, datasetIndex, index) {
|
|
this._setStyle(element, index, 'active', false);
|
|
}
|
|
setHoverStyle(element, datasetIndex, index) {
|
|
this._setStyle(element, index, 'active', true);
|
|
}
|
|
_removeDatasetHoverStyle() {
|
|
const element = this._cachedMeta.dataset;
|
|
if (element) {
|
|
this._setStyle(element, undefined, 'active', false);
|
|
}
|
|
}
|
|
_setDatasetHoverStyle() {
|
|
const element = this._cachedMeta.dataset;
|
|
if (element) {
|
|
this._setStyle(element, undefined, 'active', true);
|
|
}
|
|
}
|
|
_resyncElements(resetNewElements) {
|
|
const data = this._data;
|
|
const elements = this._cachedMeta.data;
|
|
for (const [method, arg1, arg2] of this._syncList){
|
|
this[method](arg1, arg2);
|
|
}
|
|
this._syncList = [];
|
|
const numMeta = elements.length;
|
|
const numData = data.length;
|
|
const count = Math.min(numData, numMeta);
|
|
if (count) {
|
|
this.parse(0, count);
|
|
}
|
|
if (numData > numMeta) {
|
|
this._insertElements(numMeta, numData - numMeta, resetNewElements);
|
|
} else if (numData < numMeta) {
|
|
this._removeElements(numData, numMeta - numData);
|
|
}
|
|
}
|
|
_insertElements(start, count, resetNewElements = true) {
|
|
const meta = this._cachedMeta;
|
|
const data = meta.data;
|
|
const end = start + count;
|
|
let i;
|
|
const move = (arr)=>{
|
|
arr.length += count;
|
|
for(i = arr.length - 1; i >= end; i--){
|
|
arr[i] = arr[i - count];
|
|
}
|
|
};
|
|
move(data);
|
|
for(i = start; i < end; ++i){
|
|
data[i] = new this.dataElementType();
|
|
}
|
|
if (this._parsing) {
|
|
move(meta._parsed);
|
|
}
|
|
this.parse(start, count);
|
|
if (resetNewElements) {
|
|
this.updateElements(data, start, count, 'reset');
|
|
}
|
|
}
|
|
updateElements(element, start, count, mode) {}
|
|
_removeElements(start, count) {
|
|
const meta = this._cachedMeta;
|
|
if (this._parsing) {
|
|
const removed = meta._parsed.splice(start, count);
|
|
if (meta._stacked) {
|
|
clearStacks(meta, removed);
|
|
}
|
|
}
|
|
meta.data.splice(start, count);
|
|
}
|
|
_sync(args) {
|
|
if (this._parsing) {
|
|
this._syncList.push(args);
|
|
} else {
|
|
const [method, arg1, arg2] = args;
|
|
this[method](arg1, arg2);
|
|
}
|
|
this.chart._dataChanges.push([
|
|
this.index,
|
|
...args
|
|
]);
|
|
}
|
|
_onDataPush() {
|
|
const count = arguments.length;
|
|
this._sync([
|
|
'_insertElements',
|
|
this.getDataset().data.length - count,
|
|
count
|
|
]);
|
|
}
|
|
_onDataPop() {
|
|
this._sync([
|
|
'_removeElements',
|
|
this._cachedMeta.data.length - 1,
|
|
1
|
|
]);
|
|
}
|
|
_onDataShift() {
|
|
this._sync([
|
|
'_removeElements',
|
|
0,
|
|
1
|
|
]);
|
|
}
|
|
_onDataSplice(start, count) {
|
|
if (count) {
|
|
this._sync([
|
|
'_removeElements',
|
|
start,
|
|
count
|
|
]);
|
|
}
|
|
const newCount = arguments.length - 2;
|
|
if (newCount) {
|
|
this._sync([
|
|
'_insertElements',
|
|
start,
|
|
newCount
|
|
]);
|
|
}
|
|
}
|
|
_onDataUnshift() {
|
|
this._sync([
|
|
'_insertElements',
|
|
0,
|
|
arguments.length
|
|
]);
|
|
}
|
|
}
|
|
|
|
function getAllScaleValues(scale, type) {
|
|
if (!scale._cache.$bar) {
|
|
const visibleMetas = scale.getMatchingVisibleMetas(type);
|
|
let values = [];
|
|
for(let i = 0, ilen = visibleMetas.length; i < ilen; i++){
|
|
values = values.concat(visibleMetas[i].controller.getAllParsedValues(scale));
|
|
}
|
|
scale._cache.$bar = _arrayUnique(values.sort((a, b)=>a - b));
|
|
}
|
|
return scale._cache.$bar;
|
|
}
|
|
function computeMinSampleSize(meta) {
|
|
const scale = meta.iScale;
|
|
const values = getAllScaleValues(scale, meta.type);
|
|
let min = scale._length;
|
|
let i, ilen, curr, prev;
|
|
const updateMinAndPrev = ()=>{
|
|
if (curr === 32767 || curr === -32768) {
|
|
return;
|
|
}
|
|
if (defined(prev)) {
|
|
min = Math.min(min, Math.abs(curr - prev) || min);
|
|
}
|
|
prev = curr;
|
|
};
|
|
for(i = 0, ilen = values.length; i < ilen; ++i){
|
|
curr = scale.getPixelForValue(values[i]);
|
|
updateMinAndPrev();
|
|
}
|
|
prev = undefined;
|
|
for(i = 0, ilen = scale.ticks.length; i < ilen; ++i){
|
|
curr = scale.getPixelForTick(i);
|
|
updateMinAndPrev();
|
|
}
|
|
return min;
|
|
}
|
|
function computeFitCategoryTraits(index, ruler, options, stackCount) {
|
|
const thickness = options.barThickness;
|
|
let size, ratio;
|
|
if (isNullOrUndef(thickness)) {
|
|
size = ruler.min * options.categoryPercentage;
|
|
ratio = options.barPercentage;
|
|
} else {
|
|
size = thickness * stackCount;
|
|
ratio = 1;
|
|
}
|
|
return {
|
|
chunk: size / stackCount,
|
|
ratio,
|
|
start: ruler.pixels[index] - size / 2
|
|
};
|
|
}
|
|
function computeFlexCategoryTraits(index, ruler, options, stackCount) {
|
|
const pixels = ruler.pixels;
|
|
const curr = pixels[index];
|
|
let prev = index > 0 ? pixels[index - 1] : null;
|
|
let next = index < pixels.length - 1 ? pixels[index + 1] : null;
|
|
const percent = options.categoryPercentage;
|
|
if (prev === null) {
|
|
prev = curr - (next === null ? ruler.end - ruler.start : next - curr);
|
|
}
|
|
if (next === null) {
|
|
next = curr + curr - prev;
|
|
}
|
|
const start = curr - (curr - Math.min(prev, next)) / 2 * percent;
|
|
const size = Math.abs(next - prev) / 2 * percent;
|
|
return {
|
|
chunk: size / stackCount,
|
|
ratio: options.barPercentage,
|
|
start
|
|
};
|
|
}
|
|
function parseFloatBar(entry, item, vScale, i) {
|
|
const startValue = vScale.parse(entry[0], i);
|
|
const endValue = vScale.parse(entry[1], i);
|
|
const min = Math.min(startValue, endValue);
|
|
const max = Math.max(startValue, endValue);
|
|
let barStart = min;
|
|
let barEnd = max;
|
|
if (Math.abs(min) > Math.abs(max)) {
|
|
barStart = max;
|
|
barEnd = min;
|
|
}
|
|
item[vScale.axis] = barEnd;
|
|
item._custom = {
|
|
barStart,
|
|
barEnd,
|
|
start: startValue,
|
|
end: endValue,
|
|
min,
|
|
max
|
|
};
|
|
}
|
|
function parseValue(entry, item, vScale, i) {
|
|
if (isArray(entry)) {
|
|
parseFloatBar(entry, item, vScale, i);
|
|
} else {
|
|
item[vScale.axis] = vScale.parse(entry, i);
|
|
}
|
|
return item;
|
|
}
|
|
function parseArrayOrPrimitive(meta, data, start, count) {
|
|
const iScale = meta.iScale;
|
|
const vScale = meta.vScale;
|
|
const labels = iScale.getLabels();
|
|
const singleScale = iScale === vScale;
|
|
const parsed = [];
|
|
let i, ilen, item, entry;
|
|
for(i = start, ilen = start + count; i < ilen; ++i){
|
|
entry = data[i];
|
|
item = {};
|
|
item[iScale.axis] = singleScale || iScale.parse(labels[i], i);
|
|
parsed.push(parseValue(entry, item, vScale, i));
|
|
}
|
|
return parsed;
|
|
}
|
|
function isFloatBar(custom) {
|
|
return custom && custom.barStart !== undefined && custom.barEnd !== undefined;
|
|
}
|
|
function barSign(size, vScale, actualBase) {
|
|
if (size !== 0) {
|
|
return sign(size);
|
|
}
|
|
return (vScale.isHorizontal() ? 1 : -1) * (vScale.min >= actualBase ? 1 : -1);
|
|
}
|
|
function borderProps(properties) {
|
|
let reverse, start, end, top, bottom;
|
|
if (properties.horizontal) {
|
|
reverse = properties.base > properties.x;
|
|
start = 'left';
|
|
end = 'right';
|
|
} else {
|
|
reverse = properties.base < properties.y;
|
|
start = 'bottom';
|
|
end = 'top';
|
|
}
|
|
if (reverse) {
|
|
top = 'end';
|
|
bottom = 'start';
|
|
} else {
|
|
top = 'start';
|
|
bottom = 'end';
|
|
}
|
|
return {
|
|
start,
|
|
end,
|
|
reverse,
|
|
top,
|
|
bottom
|
|
};
|
|
}
|
|
function setBorderSkipped(properties, options, stack, index) {
|
|
let edge = options.borderSkipped;
|
|
const res = {};
|
|
if (!edge) {
|
|
properties.borderSkipped = res;
|
|
return;
|
|
}
|
|
if (edge === true) {
|
|
properties.borderSkipped = {
|
|
top: true,
|
|
right: true,
|
|
bottom: true,
|
|
left: true
|
|
};
|
|
return;
|
|
}
|
|
const { start , end , reverse , top , bottom } = borderProps(properties);
|
|
if (edge === 'middle' && stack) {
|
|
properties.enableBorderRadius = true;
|
|
if ((stack._top || 0) === index) {
|
|
edge = top;
|
|
} else if ((stack._bottom || 0) === index) {
|
|
edge = bottom;
|
|
} else {
|
|
res[parseEdge(bottom, start, end, reverse)] = true;
|
|
edge = top;
|
|
}
|
|
}
|
|
res[parseEdge(edge, start, end, reverse)] = true;
|
|
properties.borderSkipped = res;
|
|
}
|
|
function parseEdge(edge, a, b, reverse) {
|
|
if (reverse) {
|
|
edge = swap(edge, a, b);
|
|
edge = startEnd(edge, b, a);
|
|
} else {
|
|
edge = startEnd(edge, a, b);
|
|
}
|
|
return edge;
|
|
}
|
|
function swap(orig, v1, v2) {
|
|
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
|
|
}
|
|
function startEnd(v, start, end) {
|
|
return v === 'start' ? start : v === 'end' ? end : v;
|
|
}
|
|
function setInflateAmount(properties, { inflateAmount }, ratio) {
|
|
properties.inflateAmount = inflateAmount === 'auto' ? ratio === 1 ? 0.33 : 0 : inflateAmount;
|
|
}
|
|
class BarController extends DatasetController {
|
|
static id = 'bar';
|
|
static defaults = {
|
|
datasetElementType: false,
|
|
dataElementType: 'bar',
|
|
categoryPercentage: 0.8,
|
|
barPercentage: 0.9,
|
|
grouped: true,
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: [
|
|
'x',
|
|
'y',
|
|
'base',
|
|
'width',
|
|
'height'
|
|
]
|
|
}
|
|
}
|
|
};
|
|
static overrides = {
|
|
scales: {
|
|
_index_: {
|
|
type: 'category',
|
|
offset: true,
|
|
grid: {
|
|
offset: true
|
|
}
|
|
},
|
|
_value_: {
|
|
type: 'linear',
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
};
|
|
parsePrimitiveData(meta, data, start, count) {
|
|
return parseArrayOrPrimitive(meta, data, start, count);
|
|
}
|
|
parseArrayData(meta, data, start, count) {
|
|
return parseArrayOrPrimitive(meta, data, start, count);
|
|
}
|
|
parseObjectData(meta, data, start, count) {
|
|
const { iScale , vScale } = meta;
|
|
const { xAxisKey ='x' , yAxisKey ='y' } = this._parsing;
|
|
const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey;
|
|
const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey;
|
|
const parsed = [];
|
|
let i, ilen, item, obj;
|
|
for(i = start, ilen = start + count; i < ilen; ++i){
|
|
obj = data[i];
|
|
item = {};
|
|
item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i);
|
|
parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i));
|
|
}
|
|
return parsed;
|
|
}
|
|
updateRangeFromParsed(range, scale, parsed, stack) {
|
|
super.updateRangeFromParsed(range, scale, parsed, stack);
|
|
const custom = parsed._custom;
|
|
if (custom && scale === this._cachedMeta.vScale) {
|
|
range.min = Math.min(range.min, custom.min);
|
|
range.max = Math.max(range.max, custom.max);
|
|
}
|
|
}
|
|
getMaxOverflow() {
|
|
return 0;
|
|
}
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const { iScale , vScale } = meta;
|
|
const parsed = this.getParsed(index);
|
|
const custom = parsed._custom;
|
|
const value = isFloatBar(custom) ? '[' + custom.start + ', ' + custom.end + ']' : '' + vScale.getLabelForValue(parsed[vScale.axis]);
|
|
return {
|
|
label: '' + iScale.getLabelForValue(parsed[iScale.axis]),
|
|
value
|
|
};
|
|
}
|
|
initialize() {
|
|
this.enableOptionSharing = true;
|
|
super.initialize();
|
|
const meta = this._cachedMeta;
|
|
meta.stack = this.getDataset().stack;
|
|
}
|
|
update(mode) {
|
|
const meta = this._cachedMeta;
|
|
this.updateElements(meta.data, 0, meta.data.length, mode);
|
|
}
|
|
updateElements(bars, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const { index , _cachedMeta: { vScale } } = this;
|
|
const base = vScale.getBasePixel();
|
|
const horizontal = vScale.isHorizontal();
|
|
const ruler = this._getRuler();
|
|
const { sharedOptions , includeOptions } = this._getSharedOptions(start, mode);
|
|
for(let i = start; i < start + count; i++){
|
|
const parsed = this.getParsed(i);
|
|
const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {
|
|
base,
|
|
head: base
|
|
} : this._calculateBarValuePixels(i);
|
|
const ipixels = this._calculateBarIndexPixels(i, ruler);
|
|
const stack = (parsed._stacks || {})[vScale.axis];
|
|
const properties = {
|
|
horizontal,
|
|
base: vpixels.base,
|
|
enableBorderRadius: !stack || isFloatBar(parsed._custom) || index === stack._top || index === stack._bottom,
|
|
x: horizontal ? vpixels.head : ipixels.center,
|
|
y: horizontal ? ipixels.center : vpixels.head,
|
|
height: horizontal ? ipixels.size : Math.abs(vpixels.size),
|
|
width: horizontal ? Math.abs(vpixels.size) : ipixels.size
|
|
};
|
|
if (includeOptions) {
|
|
properties.options = sharedOptions || this.resolveDataElementOptions(i, bars[i].active ? 'active' : mode);
|
|
}
|
|
const options = properties.options || bars[i].options;
|
|
setBorderSkipped(properties, options, stack, index);
|
|
setInflateAmount(properties, options, ruler.ratio);
|
|
this.updateElement(bars[i], i, properties, mode);
|
|
}
|
|
}
|
|
_getStacks(last, dataIndex) {
|
|
const { iScale } = this._cachedMeta;
|
|
const metasets = iScale.getMatchingVisibleMetas(this._type).filter((meta)=>meta.controller.options.grouped);
|
|
const stacked = iScale.options.stacked;
|
|
const stacks = [];
|
|
const currentParsed = this._cachedMeta.controller.getParsed(dataIndex);
|
|
const iScaleValue = currentParsed && currentParsed[iScale.axis];
|
|
const skipNull = (meta)=>{
|
|
const parsed = meta._parsed.find((item)=>item[iScale.axis] === iScaleValue);
|
|
const val = parsed && parsed[meta.vScale.axis];
|
|
if (isNullOrUndef(val) || isNaN(val)) {
|
|
return true;
|
|
}
|
|
};
|
|
for (const meta of metasets){
|
|
if (dataIndex !== undefined && skipNull(meta)) {
|
|
continue;
|
|
}
|
|
if (stacked === false || stacks.indexOf(meta.stack) === -1 || stacked === undefined && meta.stack === undefined) {
|
|
stacks.push(meta.stack);
|
|
}
|
|
if (meta.index === last) {
|
|
break;
|
|
}
|
|
}
|
|
if (!stacks.length) {
|
|
stacks.push(undefined);
|
|
}
|
|
return stacks;
|
|
}
|
|
_getStackCount(index) {
|
|
return this._getStacks(undefined, index).length;
|
|
}
|
|
_getAxisCount() {
|
|
return this._getAxis().length;
|
|
}
|
|
getFirstScaleIdForIndexAxis() {
|
|
const scales = this.chart.scales;
|
|
const indexScaleId = this.chart.options.indexAxis;
|
|
return Object.keys(scales).filter((key)=>scales[key].axis === indexScaleId).shift();
|
|
}
|
|
_getAxis() {
|
|
const axis = {};
|
|
const firstScaleAxisId = this.getFirstScaleIdForIndexAxis();
|
|
for (const dataset of this.chart.data.datasets){
|
|
axis[valueOrDefault(this.chart.options.indexAxis === 'x' ? dataset.xAxisID : dataset.yAxisID, firstScaleAxisId)] = true;
|
|
}
|
|
return Object.keys(axis);
|
|
}
|
|
_getStackIndex(datasetIndex, name, dataIndex) {
|
|
const stacks = this._getStacks(datasetIndex, dataIndex);
|
|
const index = name !== undefined ? stacks.indexOf(name) : -1;
|
|
return index === -1 ? stacks.length - 1 : index;
|
|
}
|
|
_getRuler() {
|
|
const opts = this.options;
|
|
const meta = this._cachedMeta;
|
|
const iScale = meta.iScale;
|
|
const pixels = [];
|
|
let i, ilen;
|
|
for(i = 0, ilen = meta.data.length; i < ilen; ++i){
|
|
pixels.push(iScale.getPixelForValue(this.getParsed(i)[iScale.axis], i));
|
|
}
|
|
const barThickness = opts.barThickness;
|
|
const min = barThickness || computeMinSampleSize(meta);
|
|
return {
|
|
min,
|
|
pixels,
|
|
start: iScale._startPixel,
|
|
end: iScale._endPixel,
|
|
stackCount: this._getStackCount(),
|
|
scale: iScale,
|
|
grouped: opts.grouped,
|
|
ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage
|
|
};
|
|
}
|
|
_calculateBarValuePixels(index) {
|
|
const { _cachedMeta: { vScale , _stacked , index: datasetIndex } , options: { base: baseValue , minBarLength } } = this;
|
|
const actualBase = baseValue || 0;
|
|
const parsed = this.getParsed(index);
|
|
const custom = parsed._custom;
|
|
const floating = isFloatBar(custom);
|
|
let value = parsed[vScale.axis];
|
|
let start = 0;
|
|
let length = _stacked ? this.applyStack(vScale, parsed, _stacked) : value;
|
|
let head, size;
|
|
if (length !== value) {
|
|
start = length - value;
|
|
length = value;
|
|
}
|
|
if (floating) {
|
|
value = custom.barStart;
|
|
length = custom.barEnd - custom.barStart;
|
|
if (value !== 0 && sign(value) !== sign(custom.barEnd)) {
|
|
start = 0;
|
|
}
|
|
start += value;
|
|
}
|
|
const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start;
|
|
let base = vScale.getPixelForValue(startValue);
|
|
if (this.chart.getDataVisibility(index)) {
|
|
head = vScale.getPixelForValue(start + length);
|
|
} else {
|
|
head = base;
|
|
}
|
|
size = head - base;
|
|
if (Math.abs(size) < minBarLength) {
|
|
size = barSign(size, vScale, actualBase) * minBarLength;
|
|
if (value === actualBase) {
|
|
base -= size / 2;
|
|
}
|
|
const startPixel = vScale.getPixelForDecimal(0);
|
|
const endPixel = vScale.getPixelForDecimal(1);
|
|
const min = Math.min(startPixel, endPixel);
|
|
const max = Math.max(startPixel, endPixel);
|
|
base = Math.max(Math.min(base, max), min);
|
|
head = base + size;
|
|
if (_stacked && !floating) {
|
|
parsed._stacks[vScale.axis]._visualValues[datasetIndex] = vScale.getValueForPixel(head) - vScale.getValueForPixel(base);
|
|
}
|
|
}
|
|
if (base === vScale.getPixelForValue(actualBase)) {
|
|
const halfGrid = sign(size) * vScale.getLineWidthForValue(actualBase) / 2;
|
|
base += halfGrid;
|
|
size -= halfGrid;
|
|
}
|
|
return {
|
|
size,
|
|
base,
|
|
head,
|
|
center: head + size / 2
|
|
};
|
|
}
|
|
_calculateBarIndexPixels(index, ruler) {
|
|
const scale = ruler.scale;
|
|
const options = this.options;
|
|
const skipNull = options.skipNull;
|
|
const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity);
|
|
let center, size;
|
|
const axisCount = this._getAxisCount();
|
|
if (ruler.grouped) {
|
|
const stackCount = skipNull ? this._getStackCount(index) : ruler.stackCount;
|
|
const range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options, stackCount * axisCount) : computeFitCategoryTraits(index, ruler, options, stackCount * axisCount);
|
|
const axisID = this.chart.options.indexAxis === 'x' ? this.getDataset().xAxisID : this.getDataset().yAxisID;
|
|
const axisNumber = this._getAxis().indexOf(valueOrDefault(axisID, this.getFirstScaleIdForIndexAxis()));
|
|
const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined) + axisNumber;
|
|
center = range.start + range.chunk * stackIndex + range.chunk / 2;
|
|
size = Math.min(maxBarThickness, range.chunk * range.ratio);
|
|
} else {
|
|
center = scale.getPixelForValue(this.getParsed(index)[scale.axis], index);
|
|
size = Math.min(maxBarThickness, ruler.min * ruler.ratio);
|
|
}
|
|
return {
|
|
base: center - size / 2,
|
|
head: center + size / 2,
|
|
center,
|
|
size
|
|
};
|
|
}
|
|
draw() {
|
|
const meta = this._cachedMeta;
|
|
const vScale = meta.vScale;
|
|
const rects = meta.data;
|
|
const ilen = rects.length;
|
|
let i = 0;
|
|
for(; i < ilen; ++i){
|
|
if (this.getParsed(i)[vScale.axis] !== null && !rects[i].hidden) {
|
|
rects[i].draw(this._ctx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class BubbleController extends DatasetController {
|
|
static id = 'bubble';
|
|
static defaults = {
|
|
datasetElementType: false,
|
|
dataElementType: 'point',
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: [
|
|
'x',
|
|
'y',
|
|
'borderWidth',
|
|
'radius'
|
|
]
|
|
}
|
|
}
|
|
};
|
|
static overrides = {
|
|
scales: {
|
|
x: {
|
|
type: 'linear'
|
|
},
|
|
y: {
|
|
type: 'linear'
|
|
}
|
|
}
|
|
};
|
|
initialize() {
|
|
this.enableOptionSharing = true;
|
|
super.initialize();
|
|
}
|
|
parsePrimitiveData(meta, data, start, count) {
|
|
const parsed = super.parsePrimitiveData(meta, data, start, count);
|
|
for(let i = 0; i < parsed.length; i++){
|
|
parsed[i]._custom = this.resolveDataElementOptions(i + start).radius;
|
|
}
|
|
return parsed;
|
|
}
|
|
parseArrayData(meta, data, start, count) {
|
|
const parsed = super.parseArrayData(meta, data, start, count);
|
|
for(let i = 0; i < parsed.length; i++){
|
|
const item = data[start + i];
|
|
parsed[i]._custom = valueOrDefault(item[2], this.resolveDataElementOptions(i + start).radius);
|
|
}
|
|
return parsed;
|
|
}
|
|
parseObjectData(meta, data, start, count) {
|
|
const parsed = super.parseObjectData(meta, data, start, count);
|
|
for(let i = 0; i < parsed.length; i++){
|
|
const item = data[start + i];
|
|
parsed[i]._custom = valueOrDefault(item && item.r && +item.r, this.resolveDataElementOptions(i + start).radius);
|
|
}
|
|
return parsed;
|
|
}
|
|
getMaxOverflow() {
|
|
const data = this._cachedMeta.data;
|
|
let max = 0;
|
|
for(let i = data.length - 1; i >= 0; --i){
|
|
max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2);
|
|
}
|
|
return max > 0 && max;
|
|
}
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const labels = this.chart.data.labels || [];
|
|
const { xScale , yScale } = meta;
|
|
const parsed = this.getParsed(index);
|
|
const x = xScale.getLabelForValue(parsed.x);
|
|
const y = yScale.getLabelForValue(parsed.y);
|
|
const r = parsed._custom;
|
|
return {
|
|
label: labels[index] || '',
|
|
value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')'
|
|
};
|
|
}
|
|
update(mode) {
|
|
const points = this._cachedMeta.data;
|
|
this.updateElements(points, 0, points.length, mode);
|
|
}
|
|
updateElements(points, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const { iScale , vScale } = this._cachedMeta;
|
|
const { sharedOptions , includeOptions } = this._getSharedOptions(start, mode);
|
|
const iAxis = iScale.axis;
|
|
const vAxis = vScale.axis;
|
|
for(let i = start; i < start + count; i++){
|
|
const point = points[i];
|
|
const parsed = !reset && this.getParsed(i);
|
|
const properties = {};
|
|
const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]);
|
|
const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]);
|
|
properties.skip = isNaN(iPixel) || isNaN(vPixel);
|
|
if (includeOptions) {
|
|
properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);
|
|
if (reset) {
|
|
properties.options.radius = 0;
|
|
}
|
|
}
|
|
this.updateElement(point, i, properties, mode);
|
|
}
|
|
}
|
|
resolveDataElementOptions(index, mode) {
|
|
const parsed = this.getParsed(index);
|
|
let values = super.resolveDataElementOptions(index, mode);
|
|
if (values.$shared) {
|
|
values = Object.assign({}, values, {
|
|
$shared: false
|
|
});
|
|
}
|
|
const radius = values.radius;
|
|
if (mode !== 'active') {
|
|
values.radius = 0;
|
|
}
|
|
values.radius += valueOrDefault(parsed && parsed._custom, radius);
|
|
return values;
|
|
}
|
|
}
|
|
|
|
function getRatioAndOffset(rotation, circumference, cutout) {
|
|
let ratioX = 1;
|
|
let ratioY = 1;
|
|
let offsetX = 0;
|
|
let offsetY = 0;
|
|
if (circumference < TAU) {
|
|
const startAngle = rotation;
|
|
const endAngle = startAngle + circumference;
|
|
const startX = Math.cos(startAngle);
|
|
const startY = Math.sin(startAngle);
|
|
const endX = Math.cos(endAngle);
|
|
const endY = Math.sin(endAngle);
|
|
const calcMax = (angle, a, b)=>_angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout);
|
|
const calcMin = (angle, a, b)=>_angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout);
|
|
const maxX = calcMax(0, startX, endX);
|
|
const maxY = calcMax(HALF_PI, startY, endY);
|
|
const minX = calcMin(PI, startX, endX);
|
|
const minY = calcMin(PI + HALF_PI, startY, endY);
|
|
ratioX = (maxX - minX) / 2;
|
|
ratioY = (maxY - minY) / 2;
|
|
offsetX = -(maxX + minX) / 2;
|
|
offsetY = -(maxY + minY) / 2;
|
|
}
|
|
return {
|
|
ratioX,
|
|
ratioY,
|
|
offsetX,
|
|
offsetY
|
|
};
|
|
}
|
|
class DoughnutController extends DatasetController {
|
|
static id = 'doughnut';
|
|
static defaults = {
|
|
datasetElementType: false,
|
|
dataElementType: 'arc',
|
|
animation: {
|
|
animateRotate: true,
|
|
animateScale: false
|
|
},
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: [
|
|
'circumference',
|
|
'endAngle',
|
|
'innerRadius',
|
|
'outerRadius',
|
|
'startAngle',
|
|
'x',
|
|
'y',
|
|
'offset',
|
|
'borderWidth',
|
|
'spacing'
|
|
]
|
|
}
|
|
},
|
|
cutout: '50%',
|
|
rotation: 0,
|
|
circumference: 360,
|
|
radius: '100%',
|
|
spacing: 0,
|
|
indexAxis: 'r'
|
|
};
|
|
static descriptors = {
|
|
_scriptable: (name)=>name !== 'spacing',
|
|
_indexable: (name)=>name !== 'spacing' && !name.startsWith('borderDash') && !name.startsWith('hoverBorderDash')
|
|
};
|
|
static overrides = {
|
|
aspectRatio: 1,
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
generateLabels (chart) {
|
|
const data = chart.data;
|
|
const { labels: { pointStyle , textAlign , color , useBorderRadius , borderRadius } } = chart.legend.options;
|
|
if (data.labels.length && data.datasets.length) {
|
|
return data.labels.map((label, i)=>{
|
|
const meta = chart.getDatasetMeta(0);
|
|
const style = meta.controller.getStyle(i);
|
|
return {
|
|
text: label,
|
|
fillStyle: style.backgroundColor,
|
|
fontColor: color,
|
|
hidden: !chart.getDataVisibility(i),
|
|
lineDash: style.borderDash,
|
|
lineDashOffset: style.borderDashOffset,
|
|
lineJoin: style.borderJoinStyle,
|
|
lineWidth: style.borderWidth,
|
|
strokeStyle: style.borderColor,
|
|
textAlign: textAlign,
|
|
pointStyle: pointStyle,
|
|
borderRadius: useBorderRadius && (borderRadius || style.borderRadius),
|
|
index: i
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
}
|
|
},
|
|
onClick (e, legendItem, legend) {
|
|
legend.chart.toggleDataVisibility(legendItem.index);
|
|
legend.chart.update();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
constructor(chart, datasetIndex){
|
|
super(chart, datasetIndex);
|
|
this.enableOptionSharing = true;
|
|
this.innerRadius = undefined;
|
|
this.outerRadius = undefined;
|
|
this.offsetX = undefined;
|
|
this.offsetY = undefined;
|
|
}
|
|
linkScales() {}
|
|
parse(start, count) {
|
|
const data = this.getDataset().data;
|
|
const meta = this._cachedMeta;
|
|
if (this._parsing === false) {
|
|
meta._parsed = data;
|
|
} else {
|
|
let getter = (i)=>+data[i];
|
|
if (isObject(data[start])) {
|
|
const { key ='value' } = this._parsing;
|
|
getter = (i)=>+resolveObjectKey(data[i], key);
|
|
}
|
|
let i, ilen;
|
|
for(i = start, ilen = start + count; i < ilen; ++i){
|
|
meta._parsed[i] = getter(i);
|
|
}
|
|
}
|
|
}
|
|
_getRotation() {
|
|
return toRadians(this.options.rotation - 90);
|
|
}
|
|
_getCircumference() {
|
|
return toRadians(this.options.circumference);
|
|
}
|
|
_getRotationExtents() {
|
|
let min = TAU;
|
|
let max = -TAU;
|
|
for(let i = 0; i < this.chart.data.datasets.length; ++i){
|
|
if (this.chart.isDatasetVisible(i) && this.chart.getDatasetMeta(i).type === this._type) {
|
|
const controller = this.chart.getDatasetMeta(i).controller;
|
|
const rotation = controller._getRotation();
|
|
const circumference = controller._getCircumference();
|
|
min = Math.min(min, rotation);
|
|
max = Math.max(max, rotation + circumference);
|
|
}
|
|
}
|
|
return {
|
|
rotation: min,
|
|
circumference: max - min
|
|
};
|
|
}
|
|
update(mode) {
|
|
const chart = this.chart;
|
|
const { chartArea } = chart;
|
|
const meta = this._cachedMeta;
|
|
const arcs = meta.data;
|
|
const spacing = this.getMaxBorderWidth() + this.getMaxOffset(arcs) + this.options.spacing;
|
|
const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0);
|
|
const cutout = Math.min(toPercentage(this.options.cutout, maxSize), 1);
|
|
const chartWeight = this._getRingWeight(this.index);
|
|
const { circumference , rotation } = this._getRotationExtents();
|
|
const { ratioX , ratioY , offsetX , offsetY } = getRatioAndOffset(rotation, circumference, cutout);
|
|
const maxWidth = (chartArea.width - spacing) / ratioX;
|
|
const maxHeight = (chartArea.height - spacing) / ratioY;
|
|
const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
|
|
const outerRadius = toDimension(this.options.radius, maxRadius);
|
|
const innerRadius = Math.max(outerRadius * cutout, 0);
|
|
const radiusLength = (outerRadius - innerRadius) / this._getVisibleDatasetWeightTotal();
|
|
this.offsetX = offsetX * outerRadius;
|
|
this.offsetY = offsetY * outerRadius;
|
|
meta.total = this.calculateTotal();
|
|
this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index);
|
|
this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight, 0);
|
|
this.updateElements(arcs, 0, arcs.length, mode);
|
|
}
|
|
_circumference(i, reset) {
|
|
const opts = this.options;
|
|
const meta = this._cachedMeta;
|
|
const circumference = this._getCircumference();
|
|
if (reset && opts.animation.animateRotate || !this.chart.getDataVisibility(i) || meta._parsed[i] === null || meta.data[i].hidden) {
|
|
return 0;
|
|
}
|
|
return this.calculateCircumference(meta._parsed[i] * circumference / TAU);
|
|
}
|
|
updateElements(arcs, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const chart = this.chart;
|
|
const chartArea = chart.chartArea;
|
|
const opts = chart.options;
|
|
const animationOpts = opts.animation;
|
|
const centerX = (chartArea.left + chartArea.right) / 2;
|
|
const centerY = (chartArea.top + chartArea.bottom) / 2;
|
|
const animateScale = reset && animationOpts.animateScale;
|
|
const innerRadius = animateScale ? 0 : this.innerRadius;
|
|
const outerRadius = animateScale ? 0 : this.outerRadius;
|
|
const { sharedOptions , includeOptions } = this._getSharedOptions(start, mode);
|
|
let startAngle = this._getRotation();
|
|
let i;
|
|
for(i = 0; i < start; ++i){
|
|
startAngle += this._circumference(i, reset);
|
|
}
|
|
for(i = start; i < start + count; ++i){
|
|
const circumference = this._circumference(i, reset);
|
|
const arc = arcs[i];
|
|
const properties = {
|
|
x: centerX + this.offsetX,
|
|
y: centerY + this.offsetY,
|
|
startAngle,
|
|
endAngle: startAngle + circumference,
|
|
circumference,
|
|
outerRadius,
|
|
innerRadius
|
|
};
|
|
if (includeOptions) {
|
|
properties.options = sharedOptions || this.resolveDataElementOptions(i, arc.active ? 'active' : mode);
|
|
}
|
|
startAngle += circumference;
|
|
this.updateElement(arc, i, properties, mode);
|
|
}
|
|
}
|
|
calculateTotal() {
|
|
const meta = this._cachedMeta;
|
|
const metaData = meta.data;
|
|
let total = 0;
|
|
let i;
|
|
for(i = 0; i < metaData.length; i++){
|
|
const value = meta._parsed[i];
|
|
if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i) && !metaData[i].hidden) {
|
|
total += Math.abs(value);
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
calculateCircumference(value) {
|
|
const total = this._cachedMeta.total;
|
|
if (total > 0 && !isNaN(value)) {
|
|
return TAU * (Math.abs(value) / total);
|
|
}
|
|
return 0;
|
|
}
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const chart = this.chart;
|
|
const labels = chart.data.labels || [];
|
|
const value = formatNumber(meta._parsed[index], chart.options.locale);
|
|
return {
|
|
label: labels[index] || '',
|
|
value
|
|
};
|
|
}
|
|
getMaxBorderWidth(arcs) {
|
|
let max = 0;
|
|
const chart = this.chart;
|
|
let i, ilen, meta, controller, options;
|
|
if (!arcs) {
|
|
for(i = 0, ilen = chart.data.datasets.length; i < ilen; ++i){
|
|
if (chart.isDatasetVisible(i)) {
|
|
meta = chart.getDatasetMeta(i);
|
|
arcs = meta.data;
|
|
controller = meta.controller;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!arcs) {
|
|
return 0;
|
|
}
|
|
for(i = 0, ilen = arcs.length; i < ilen; ++i){
|
|
options = controller.resolveDataElementOptions(i);
|
|
if (options.borderAlign !== 'inner') {
|
|
max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0);
|
|
}
|
|
}
|
|
return max;
|
|
}
|
|
getMaxOffset(arcs) {
|
|
let max = 0;
|
|
for(let i = 0, ilen = arcs.length; i < ilen; ++i){
|
|
const options = this.resolveDataElementOptions(i);
|
|
max = Math.max(max, options.offset || 0, options.hoverOffset || 0);
|
|
}
|
|
return max;
|
|
}
|
|
_getRingWeightOffset(datasetIndex) {
|
|
let ringWeightOffset = 0;
|
|
for(let i = 0; i < datasetIndex; ++i){
|
|
if (this.chart.isDatasetVisible(i)) {
|
|
ringWeightOffset += this._getRingWeight(i);
|
|
}
|
|
}
|
|
return ringWeightOffset;
|
|
}
|
|
_getRingWeight(datasetIndex) {
|
|
return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0);
|
|
}
|
|
_getVisibleDatasetWeightTotal() {
|
|
return this._getRingWeightOffset(this.chart.data.datasets.length) || 1;
|
|
}
|
|
}
|
|
|
|
class LineController extends DatasetController {
|
|
static id = 'line';
|
|
static defaults = {
|
|
datasetElementType: 'line',
|
|
dataElementType: 'point',
|
|
showLine: true,
|
|
spanGaps: false
|
|
};
|
|
static overrides = {
|
|
scales: {
|
|
_index_: {
|
|
type: 'category'
|
|
},
|
|
_value_: {
|
|
type: 'linear'
|
|
}
|
|
}
|
|
};
|
|
initialize() {
|
|
this.enableOptionSharing = true;
|
|
this.supportsDecimation = true;
|
|
super.initialize();
|
|
}
|
|
update(mode) {
|
|
const meta = this._cachedMeta;
|
|
const { dataset: line , data: points = [] , _dataset } = meta;
|
|
const animationsDisabled = this.chart._animationsDisabled;
|
|
let { start , count } = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled);
|
|
this._drawStart = start;
|
|
this._drawCount = count;
|
|
if (_scaleRangesChanged(meta)) {
|
|
start = 0;
|
|
count = points.length;
|
|
}
|
|
line._chart = this.chart;
|
|
line._datasetIndex = this.index;
|
|
line._decimated = !!_dataset._decimated;
|
|
line.points = points;
|
|
const options = this.resolveDatasetElementOptions(mode);
|
|
if (!this.options.showLine) {
|
|
options.borderWidth = 0;
|
|
}
|
|
options.segment = this.options.segment;
|
|
this.updateElement(line, undefined, {
|
|
animated: !animationsDisabled,
|
|
options
|
|
}, mode);
|
|
this.updateElements(points, start, count, mode);
|
|
}
|
|
updateElements(points, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const { iScale , vScale , _stacked , _dataset } = this._cachedMeta;
|
|
const { sharedOptions , includeOptions } = this._getSharedOptions(start, mode);
|
|
const iAxis = iScale.axis;
|
|
const vAxis = vScale.axis;
|
|
const { spanGaps , segment } = this.options;
|
|
const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;
|
|
const directUpdate = this.chart._animationsDisabled || reset || mode === 'none';
|
|
const end = start + count;
|
|
const pointsCount = points.length;
|
|
let prevParsed = start > 0 && this.getParsed(start - 1);
|
|
for(let i = 0; i < pointsCount; ++i){
|
|
const point = points[i];
|
|
const properties = directUpdate ? point : {};
|
|
if (i < start || i >= end) {
|
|
properties.skip = true;
|
|
continue;
|
|
}
|
|
const parsed = this.getParsed(i);
|
|
const nullData = isNullOrUndef(parsed[vAxis]);
|
|
const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i);
|
|
const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i);
|
|
properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData;
|
|
properties.stop = i > 0 && Math.abs(parsed[iAxis] - prevParsed[iAxis]) > maxGapLength;
|
|
if (segment) {
|
|
properties.parsed = parsed;
|
|
properties.raw = _dataset.data[i];
|
|
}
|
|
if (includeOptions) {
|
|
properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);
|
|
}
|
|
if (!directUpdate) {
|
|
this.updateElement(point, i, properties, mode);
|
|
}
|
|
prevParsed = parsed;
|
|
}
|
|
}
|
|
getMaxOverflow() {
|
|
const meta = this._cachedMeta;
|
|
const dataset = meta.dataset;
|
|
const border = dataset.options && dataset.options.borderWidth || 0;
|
|
const data = meta.data || [];
|
|
if (!data.length) {
|
|
return border;
|
|
}
|
|
const firstPoint = data[0].size(this.resolveDataElementOptions(0));
|
|
const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1));
|
|
return Math.max(border, firstPoint, lastPoint) / 2;
|
|
}
|
|
draw() {
|
|
const meta = this._cachedMeta;
|
|
meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis);
|
|
super.draw();
|
|
}
|
|
}
|
|
|
|
class PolarAreaController extends DatasetController {
|
|
static id = 'polarArea';
|
|
static defaults = {
|
|
dataElementType: 'arc',
|
|
animation: {
|
|
animateRotate: true,
|
|
animateScale: true
|
|
},
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: [
|
|
'x',
|
|
'y',
|
|
'startAngle',
|
|
'endAngle',
|
|
'innerRadius',
|
|
'outerRadius'
|
|
]
|
|
}
|
|
},
|
|
indexAxis: 'r',
|
|
startAngle: 0
|
|
};
|
|
static overrides = {
|
|
aspectRatio: 1,
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
generateLabels (chart) {
|
|
const data = chart.data;
|
|
if (data.labels.length && data.datasets.length) {
|
|
const { labels: { pointStyle , color } } = chart.legend.options;
|
|
return data.labels.map((label, i)=>{
|
|
const meta = chart.getDatasetMeta(0);
|
|
const style = meta.controller.getStyle(i);
|
|
return {
|
|
text: label,
|
|
fillStyle: style.backgroundColor,
|
|
strokeStyle: style.borderColor,
|
|
fontColor: color,
|
|
lineWidth: style.borderWidth,
|
|
pointStyle: pointStyle,
|
|
hidden: !chart.getDataVisibility(i),
|
|
index: i
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
}
|
|
},
|
|
onClick (e, legendItem, legend) {
|
|
legend.chart.toggleDataVisibility(legendItem.index);
|
|
legend.chart.update();
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
r: {
|
|
type: 'radialLinear',
|
|
angleLines: {
|
|
display: false
|
|
},
|
|
beginAtZero: true,
|
|
grid: {
|
|
circular: true
|
|
},
|
|
pointLabels: {
|
|
display: false
|
|
},
|
|
startAngle: 0
|
|
}
|
|
}
|
|
};
|
|
constructor(chart, datasetIndex){
|
|
super(chart, datasetIndex);
|
|
this.innerRadius = undefined;
|
|
this.outerRadius = undefined;
|
|
}
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const chart = this.chart;
|
|
const labels = chart.data.labels || [];
|
|
const value = formatNumber(meta._parsed[index].r, chart.options.locale);
|
|
return {
|
|
label: labels[index] || '',
|
|
value
|
|
};
|
|
}
|
|
parseObjectData(meta, data, start, count) {
|
|
return _parseObjectDataRadialScale.bind(this)(meta, data, start, count);
|
|
}
|
|
update(mode) {
|
|
const arcs = this._cachedMeta.data;
|
|
this._updateRadius();
|
|
this.updateElements(arcs, 0, arcs.length, mode);
|
|
}
|
|
getMinMax() {
|
|
const meta = this._cachedMeta;
|
|
const range = {
|
|
min: Number.POSITIVE_INFINITY,
|
|
max: Number.NEGATIVE_INFINITY
|
|
};
|
|
meta.data.forEach((element, index)=>{
|
|
const parsed = this.getParsed(index).r;
|
|
if (!isNaN(parsed) && this.chart.getDataVisibility(index)) {
|
|
if (parsed < range.min) {
|
|
range.min = parsed;
|
|
}
|
|
if (parsed > range.max) {
|
|
range.max = parsed;
|
|
}
|
|
}
|
|
});
|
|
return range;
|
|
}
|
|
_updateRadius() {
|
|
const chart = this.chart;
|
|
const chartArea = chart.chartArea;
|
|
const opts = chart.options;
|
|
const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);
|
|
const outerRadius = Math.max(minSize / 2, 0);
|
|
const innerRadius = Math.max(opts.cutoutPercentage ? outerRadius / 100 * opts.cutoutPercentage : 1, 0);
|
|
const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount();
|
|
this.outerRadius = outerRadius - radiusLength * this.index;
|
|
this.innerRadius = this.outerRadius - radiusLength;
|
|
}
|
|
updateElements(arcs, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const chart = this.chart;
|
|
const opts = chart.options;
|
|
const animationOpts = opts.animation;
|
|
const scale = this._cachedMeta.rScale;
|
|
const centerX = scale.xCenter;
|
|
const centerY = scale.yCenter;
|
|
const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI;
|
|
let angle = datasetStartAngle;
|
|
let i;
|
|
const defaultAngle = 360 / this.countVisibleElements();
|
|
for(i = 0; i < start; ++i){
|
|
angle += this._computeAngle(i, mode, defaultAngle);
|
|
}
|
|
for(i = start; i < start + count; i++){
|
|
const arc = arcs[i];
|
|
let startAngle = angle;
|
|
let endAngle = angle + this._computeAngle(i, mode, defaultAngle);
|
|
let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(this.getParsed(i).r) : 0;
|
|
angle = endAngle;
|
|
if (reset) {
|
|
if (animationOpts.animateScale) {
|
|
outerRadius = 0;
|
|
}
|
|
if (animationOpts.animateRotate) {
|
|
startAngle = endAngle = datasetStartAngle;
|
|
}
|
|
}
|
|
const properties = {
|
|
x: centerX,
|
|
y: centerY,
|
|
innerRadius: 0,
|
|
outerRadius,
|
|
startAngle,
|
|
endAngle,
|
|
options: this.resolveDataElementOptions(i, arc.active ? 'active' : mode)
|
|
};
|
|
this.updateElement(arc, i, properties, mode);
|
|
}
|
|
}
|
|
countVisibleElements() {
|
|
const meta = this._cachedMeta;
|
|
let count = 0;
|
|
meta.data.forEach((element, index)=>{
|
|
if (!isNaN(this.getParsed(index).r) && this.chart.getDataVisibility(index)) {
|
|
count++;
|
|
}
|
|
});
|
|
return count;
|
|
}
|
|
_computeAngle(index, mode, defaultAngle) {
|
|
return this.chart.getDataVisibility(index) ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) : 0;
|
|
}
|
|
}
|
|
|
|
class PieController extends DoughnutController {
|
|
static id = 'pie';
|
|
static defaults = {
|
|
cutout: 0,
|
|
rotation: 0,
|
|
circumference: 360,
|
|
radius: '100%'
|
|
};
|
|
}
|
|
|
|
class RadarController extends DatasetController {
|
|
static id = 'radar';
|
|
static defaults = {
|
|
datasetElementType: 'line',
|
|
dataElementType: 'point',
|
|
indexAxis: 'r',
|
|
showLine: true,
|
|
elements: {
|
|
line: {
|
|
fill: 'start'
|
|
}
|
|
}
|
|
};
|
|
static overrides = {
|
|
aspectRatio: 1,
|
|
scales: {
|
|
r: {
|
|
type: 'radialLinear'
|
|
}
|
|
}
|
|
};
|
|
getLabelAndValue(index) {
|
|
const vScale = this._cachedMeta.vScale;
|
|
const parsed = this.getParsed(index);
|
|
return {
|
|
label: vScale.getLabels()[index],
|
|
value: '' + vScale.getLabelForValue(parsed[vScale.axis])
|
|
};
|
|
}
|
|
parseObjectData(meta, data, start, count) {
|
|
return _parseObjectDataRadialScale.bind(this)(meta, data, start, count);
|
|
}
|
|
update(mode) {
|
|
const meta = this._cachedMeta;
|
|
const line = meta.dataset;
|
|
const points = meta.data || [];
|
|
const labels = meta.iScale.getLabels();
|
|
line.points = points;
|
|
if (mode !== 'resize') {
|
|
const options = this.resolveDatasetElementOptions(mode);
|
|
if (!this.options.showLine) {
|
|
options.borderWidth = 0;
|
|
}
|
|
const properties = {
|
|
_loop: true,
|
|
_fullLoop: labels.length === points.length,
|
|
options
|
|
};
|
|
this.updateElement(line, undefined, properties, mode);
|
|
}
|
|
this.updateElements(points, 0, points.length, mode);
|
|
}
|
|
updateElements(points, start, count, mode) {
|
|
const scale = this._cachedMeta.rScale;
|
|
const reset = mode === 'reset';
|
|
for(let i = start; i < start + count; i++){
|
|
const point = points[i];
|
|
const options = this.resolveDataElementOptions(i, point.active ? 'active' : mode);
|
|
const pointPosition = scale.getPointPositionForValue(i, this.getParsed(i).r);
|
|
const x = reset ? scale.xCenter : pointPosition.x;
|
|
const y = reset ? scale.yCenter : pointPosition.y;
|
|
const properties = {
|
|
x,
|
|
y,
|
|
angle: pointPosition.angle,
|
|
skip: isNaN(x) || isNaN(y),
|
|
options
|
|
};
|
|
this.updateElement(point, i, properties, mode);
|
|
}
|
|
}
|
|
}
|
|
|
|
class ScatterController extends DatasetController {
|
|
static id = 'scatter';
|
|
static defaults = {
|
|
datasetElementType: false,
|
|
dataElementType: 'point',
|
|
showLine: false,
|
|
fill: false
|
|
};
|
|
static overrides = {
|
|
interaction: {
|
|
mode: 'point'
|
|
},
|
|
scales: {
|
|
x: {
|
|
type: 'linear'
|
|
},
|
|
y: {
|
|
type: 'linear'
|
|
}
|
|
}
|
|
};
|
|
getLabelAndValue(index) {
|
|
const meta = this._cachedMeta;
|
|
const labels = this.chart.data.labels || [];
|
|
const { xScale , yScale } = meta;
|
|
const parsed = this.getParsed(index);
|
|
const x = xScale.getLabelForValue(parsed.x);
|
|
const y = yScale.getLabelForValue(parsed.y);
|
|
return {
|
|
label: labels[index] || '',
|
|
value: '(' + x + ', ' + y + ')'
|
|
};
|
|
}
|
|
update(mode) {
|
|
const meta = this._cachedMeta;
|
|
const { data: points = [] } = meta;
|
|
const animationsDisabled = this.chart._animationsDisabled;
|
|
let { start , count } = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled);
|
|
this._drawStart = start;
|
|
this._drawCount = count;
|
|
if (_scaleRangesChanged(meta)) {
|
|
start = 0;
|
|
count = points.length;
|
|
}
|
|
if (this.options.showLine) {
|
|
if (!this.datasetElementType) {
|
|
this.addElements();
|
|
}
|
|
const { dataset: line , _dataset } = meta;
|
|
line._chart = this.chart;
|
|
line._datasetIndex = this.index;
|
|
line._decimated = !!_dataset._decimated;
|
|
line.points = points;
|
|
const options = this.resolveDatasetElementOptions(mode);
|
|
options.segment = this.options.segment;
|
|
this.updateElement(line, undefined, {
|
|
animated: !animationsDisabled,
|
|
options
|
|
}, mode);
|
|
} else if (this.datasetElementType) {
|
|
delete meta.dataset;
|
|
this.datasetElementType = false;
|
|
}
|
|
this.updateElements(points, start, count, mode);
|
|
}
|
|
addElements() {
|
|
const { showLine } = this.options;
|
|
if (!this.datasetElementType && showLine) {
|
|
this.datasetElementType = this.chart.registry.getElement('line');
|
|
}
|
|
super.addElements();
|
|
}
|
|
updateElements(points, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const { iScale , vScale , _stacked , _dataset } = this._cachedMeta;
|
|
const firstOpts = this.resolveDataElementOptions(start, mode);
|
|
const sharedOptions = this.getSharedOptions(firstOpts);
|
|
const includeOptions = this.includeOptions(mode, sharedOptions);
|
|
const iAxis = iScale.axis;
|
|
const vAxis = vScale.axis;
|
|
const { spanGaps , segment } = this.options;
|
|
const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;
|
|
const directUpdate = this.chart._animationsDisabled || reset || mode === 'none';
|
|
let prevParsed = start > 0 && this.getParsed(start - 1);
|
|
for(let i = start; i < start + count; ++i){
|
|
const point = points[i];
|
|
const parsed = this.getParsed(i);
|
|
const properties = directUpdate ? point : {};
|
|
const nullData = isNullOrUndef(parsed[vAxis]);
|
|
const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i);
|
|
const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i);
|
|
properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData;
|
|
properties.stop = i > 0 && Math.abs(parsed[iAxis] - prevParsed[iAxis]) > maxGapLength;
|
|
if (segment) {
|
|
properties.parsed = parsed;
|
|
properties.raw = _dataset.data[i];
|
|
}
|
|
if (includeOptions) {
|
|
properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);
|
|
}
|
|
if (!directUpdate) {
|
|
this.updateElement(point, i, properties, mode);
|
|
}
|
|
prevParsed = parsed;
|
|
}
|
|
this.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
}
|
|
getMaxOverflow() {
|
|
const meta = this._cachedMeta;
|
|
const data = meta.data || [];
|
|
if (!this.options.showLine) {
|
|
let max = 0;
|
|
for(let i = data.length - 1; i >= 0; --i){
|
|
max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2);
|
|
}
|
|
return max > 0 && max;
|
|
}
|
|
const dataset = meta.dataset;
|
|
const border = dataset.options && dataset.options.borderWidth || 0;
|
|
if (!data.length) {
|
|
return border;
|
|
}
|
|
const firstPoint = data[0].size(this.resolveDataElementOptions(0));
|
|
const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1));
|
|
return Math.max(border, firstPoint, lastPoint) / 2;
|
|
}
|
|
}
|
|
|
|
var controllers = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
BarController: BarController,
|
|
BubbleController: BubbleController,
|
|
DoughnutController: DoughnutController,
|
|
LineController: LineController,
|
|
PieController: PieController,
|
|
PolarAreaController: PolarAreaController,
|
|
RadarController: RadarController,
|
|
ScatterController: ScatterController
|
|
});
|
|
|
|
/**
|
|
* @namespace Chart._adapters
|
|
* @since 2.8.0
|
|
* @private
|
|
*/ function chart_abstract() {
|
|
throw new Error('This method is not implemented: Check that a complete date adapter is provided.');
|
|
}
|
|
/**
|
|
* Date adapter (current used by the time scale)
|
|
* @namespace Chart._adapters._date
|
|
* @memberof Chart._adapters
|
|
* @private
|
|
*/ class DateAdapterBase {
|
|
/**
|
|
* Override default date adapter methods.
|
|
* Accepts type parameter to define options type.
|
|
* @example
|
|
* Chart._adapters._date.override<{myAdapterOption: string}>({
|
|
* init() {
|
|
* console.log(this.options.myAdapterOption);
|
|
* }
|
|
* })
|
|
*/ static override(members) {
|
|
Object.assign(DateAdapterBase.prototype, members);
|
|
}
|
|
options;
|
|
constructor(options){
|
|
this.options = options || {};
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
init() {}
|
|
formats() {
|
|
return chart_abstract();
|
|
}
|
|
parse() {
|
|
return chart_abstract();
|
|
}
|
|
format() {
|
|
return chart_abstract();
|
|
}
|
|
add() {
|
|
return chart_abstract();
|
|
}
|
|
diff() {
|
|
return chart_abstract();
|
|
}
|
|
startOf() {
|
|
return chart_abstract();
|
|
}
|
|
endOf() {
|
|
return chart_abstract();
|
|
}
|
|
}
|
|
var adapters = {
|
|
_date: DateAdapterBase
|
|
};
|
|
|
|
function binarySearch(metaset, axis, value, intersect) {
|
|
const { controller , data , _sorted } = metaset;
|
|
const iScale = controller._cachedMeta.iScale;
|
|
const spanGaps = metaset.dataset ? metaset.dataset.options ? metaset.dataset.options.spanGaps : null : null;
|
|
if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) {
|
|
const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey;
|
|
if (!intersect) {
|
|
const result = lookupMethod(data, axis, value);
|
|
if (spanGaps) {
|
|
const { vScale } = controller._cachedMeta;
|
|
const { _parsed } = metaset;
|
|
const distanceToDefinedLo = _parsed.slice(0, result.lo + 1).reverse().findIndex((point)=>!isNullOrUndef(point[vScale.axis]));
|
|
result.lo -= Math.max(0, distanceToDefinedLo);
|
|
const distanceToDefinedHi = _parsed.slice(result.hi).findIndex((point)=>!isNullOrUndef(point[vScale.axis]));
|
|
result.hi += Math.max(0, distanceToDefinedHi);
|
|
}
|
|
return result;
|
|
} else if (controller._sharedOptions) {
|
|
const el = data[0];
|
|
const range = typeof el.getRange === 'function' && el.getRange(axis);
|
|
if (range) {
|
|
const start = lookupMethod(data, axis, value - range);
|
|
const end = lookupMethod(data, axis, value + range);
|
|
return {
|
|
lo: start.lo,
|
|
hi: end.hi
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
lo: 0,
|
|
hi: data.length - 1
|
|
};
|
|
}
|
|
function evaluateInteractionItems(chart, axis, position, handler, intersect) {
|
|
const metasets = chart.getSortedVisibleDatasetMetas();
|
|
const value = position[axis];
|
|
for(let i = 0, ilen = metasets.length; i < ilen; ++i){
|
|
const { index , data } = metasets[i];
|
|
const { lo , hi } = binarySearch(metasets[i], axis, value, intersect);
|
|
for(let j = lo; j <= hi; ++j){
|
|
const element = data[j];
|
|
if (!element.skip) {
|
|
handler(element, index, j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function getDistanceMetricForAxis(axis) {
|
|
const useX = axis.indexOf('x') !== -1;
|
|
const useY = axis.indexOf('y') !== -1;
|
|
return function(pt1, pt2) {
|
|
const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0;
|
|
const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0;
|
|
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
|
|
};
|
|
}
|
|
function getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) {
|
|
const items = [];
|
|
if (!includeInvisible && !chart.isPointInArea(position)) {
|
|
return items;
|
|
}
|
|
const evaluationFunc = function(element, datasetIndex, index) {
|
|
if (!includeInvisible && !_isPointInArea(element, chart.chartArea, 0)) {
|
|
return;
|
|
}
|
|
if (element.inRange(position.x, position.y, useFinalPosition)) {
|
|
items.push({
|
|
element,
|
|
datasetIndex,
|
|
index
|
|
});
|
|
}
|
|
};
|
|
evaluateInteractionItems(chart, axis, position, evaluationFunc, true);
|
|
return items;
|
|
}
|
|
function getNearestRadialItems(chart, position, axis, useFinalPosition) {
|
|
let items = [];
|
|
function evaluationFunc(element, datasetIndex, index) {
|
|
const { startAngle , endAngle } = element.getProps([
|
|
'startAngle',
|
|
'endAngle'
|
|
], useFinalPosition);
|
|
const { angle } = getAngleFromPoint(element, {
|
|
x: position.x,
|
|
y: position.y
|
|
});
|
|
if (_angleBetween(angle, startAngle, endAngle)) {
|
|
items.push({
|
|
element,
|
|
datasetIndex,
|
|
index
|
|
});
|
|
}
|
|
}
|
|
evaluateInteractionItems(chart, axis, position, evaluationFunc);
|
|
return items;
|
|
}
|
|
function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {
|
|
let items = [];
|
|
const distanceMetric = getDistanceMetricForAxis(axis);
|
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
function evaluationFunc(element, datasetIndex, index) {
|
|
const inRange = element.inRange(position.x, position.y, useFinalPosition);
|
|
if (intersect && !inRange) {
|
|
return;
|
|
}
|
|
const center = element.getCenterPoint(useFinalPosition);
|
|
const pointInArea = !!includeInvisible || chart.isPointInArea(center);
|
|
if (!pointInArea && !inRange) {
|
|
return;
|
|
}
|
|
const distance = distanceMetric(position, center);
|
|
if (distance < minDistance) {
|
|
items = [
|
|
{
|
|
element,
|
|
datasetIndex,
|
|
index
|
|
}
|
|
];
|
|
minDistance = distance;
|
|
} else if (distance === minDistance) {
|
|
items.push({
|
|
element,
|
|
datasetIndex,
|
|
index
|
|
});
|
|
}
|
|
}
|
|
evaluateInteractionItems(chart, axis, position, evaluationFunc);
|
|
return items;
|
|
}
|
|
function getNearestItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {
|
|
if (!includeInvisible && !chart.isPointInArea(position)) {
|
|
return [];
|
|
}
|
|
return axis === 'r' && !intersect ? getNearestRadialItems(chart, position, axis, useFinalPosition) : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible);
|
|
}
|
|
function getAxisItems(chart, position, axis, intersect, useFinalPosition) {
|
|
const items = [];
|
|
const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange';
|
|
let intersectsItem = false;
|
|
evaluateInteractionItems(chart, axis, position, (element, datasetIndex, index)=>{
|
|
if (element[rangeMethod] && element[rangeMethod](position[axis], useFinalPosition)) {
|
|
items.push({
|
|
element,
|
|
datasetIndex,
|
|
index
|
|
});
|
|
intersectsItem = intersectsItem || element.inRange(position.x, position.y, useFinalPosition);
|
|
}
|
|
});
|
|
if (intersect && !intersectsItem) {
|
|
return [];
|
|
}
|
|
return items;
|
|
}
|
|
var Interaction = {
|
|
evaluateInteractionItems,
|
|
modes: {
|
|
index (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
const axis = options.axis || 'x';
|
|
const includeInvisible = options.includeInvisible || false;
|
|
const items = options.intersect ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);
|
|
const elements = [];
|
|
if (!items.length) {
|
|
return [];
|
|
}
|
|
chart.getSortedVisibleDatasetMetas().forEach((meta)=>{
|
|
const index = items[0].index;
|
|
const element = meta.data[index];
|
|
if (element && !element.skip) {
|
|
elements.push({
|
|
element,
|
|
datasetIndex: meta.index,
|
|
index
|
|
});
|
|
}
|
|
});
|
|
return elements;
|
|
},
|
|
dataset (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
const axis = options.axis || 'xy';
|
|
const includeInvisible = options.includeInvisible || false;
|
|
let items = options.intersect ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);
|
|
if (items.length > 0) {
|
|
const datasetIndex = items[0].datasetIndex;
|
|
const data = chart.getDatasetMeta(datasetIndex).data;
|
|
items = [];
|
|
for(let i = 0; i < data.length; ++i){
|
|
items.push({
|
|
element: data[i],
|
|
datasetIndex,
|
|
index: i
|
|
});
|
|
}
|
|
}
|
|
return items;
|
|
},
|
|
point (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
const axis = options.axis || 'xy';
|
|
const includeInvisible = options.includeInvisible || false;
|
|
return getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible);
|
|
},
|
|
nearest (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
const axis = options.axis || 'xy';
|
|
const includeInvisible = options.includeInvisible || false;
|
|
return getNearestItems(chart, position, axis, options.intersect, useFinalPosition, includeInvisible);
|
|
},
|
|
x (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
return getAxisItems(chart, position, 'x', options.intersect, useFinalPosition);
|
|
},
|
|
y (chart, e, options, useFinalPosition) {
|
|
const position = getRelativePosition(e, chart);
|
|
return getAxisItems(chart, position, 'y', options.intersect, useFinalPosition);
|
|
}
|
|
}
|
|
};
|
|
|
|
const STATIC_POSITIONS = [
|
|
'left',
|
|
'top',
|
|
'right',
|
|
'bottom'
|
|
];
|
|
function filterByPosition(array, position) {
|
|
return array.filter((v)=>v.pos === position);
|
|
}
|
|
function filterDynamicPositionByAxis(array, axis) {
|
|
return array.filter((v)=>STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis);
|
|
}
|
|
function sortByWeight(array, reverse) {
|
|
return array.sort((a, b)=>{
|
|
const v0 = reverse ? b : a;
|
|
const v1 = reverse ? a : b;
|
|
return v0.weight === v1.weight ? v0.index - v1.index : v0.weight - v1.weight;
|
|
});
|
|
}
|
|
function wrapBoxes(boxes) {
|
|
const layoutBoxes = [];
|
|
let i, ilen, box, pos, stack, stackWeight;
|
|
for(i = 0, ilen = (boxes || []).length; i < ilen; ++i){
|
|
box = boxes[i];
|
|
({ position: pos , options: { stack , stackWeight =1 } } = box);
|
|
layoutBoxes.push({
|
|
index: i,
|
|
box,
|
|
pos,
|
|
horizontal: box.isHorizontal(),
|
|
weight: box.weight,
|
|
stack: stack && pos + stack,
|
|
stackWeight
|
|
});
|
|
}
|
|
return layoutBoxes;
|
|
}
|
|
function buildStacks(layouts) {
|
|
const stacks = {};
|
|
for (const wrap of layouts){
|
|
const { stack , pos , stackWeight } = wrap;
|
|
if (!stack || !STATIC_POSITIONS.includes(pos)) {
|
|
continue;
|
|
}
|
|
const _stack = stacks[stack] || (stacks[stack] = {
|
|
count: 0,
|
|
placed: 0,
|
|
weight: 0,
|
|
size: 0
|
|
});
|
|
_stack.count++;
|
|
_stack.weight += stackWeight;
|
|
}
|
|
return stacks;
|
|
}
|
|
function setLayoutDims(layouts, params) {
|
|
const stacks = buildStacks(layouts);
|
|
const { vBoxMaxWidth , hBoxMaxHeight } = params;
|
|
let i, ilen, layout;
|
|
for(i = 0, ilen = layouts.length; i < ilen; ++i){
|
|
layout = layouts[i];
|
|
const { fullSize } = layout.box;
|
|
const stack = stacks[layout.stack];
|
|
const factor = stack && layout.stackWeight / stack.weight;
|
|
if (layout.horizontal) {
|
|
layout.width = factor ? factor * vBoxMaxWidth : fullSize && params.availableWidth;
|
|
layout.height = hBoxMaxHeight;
|
|
} else {
|
|
layout.width = vBoxMaxWidth;
|
|
layout.height = factor ? factor * hBoxMaxHeight : fullSize && params.availableHeight;
|
|
}
|
|
}
|
|
return stacks;
|
|
}
|
|
function buildLayoutBoxes(boxes) {
|
|
const layoutBoxes = wrapBoxes(boxes);
|
|
const fullSize = sortByWeight(layoutBoxes.filter((wrap)=>wrap.box.fullSize), true);
|
|
const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);
|
|
const right = sortByWeight(filterByPosition(layoutBoxes, 'right'));
|
|
const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);
|
|
const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));
|
|
const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x');
|
|
const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y');
|
|
return {
|
|
fullSize,
|
|
leftAndTop: left.concat(top),
|
|
rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal),
|
|
chartArea: filterByPosition(layoutBoxes, 'chartArea'),
|
|
vertical: left.concat(right).concat(centerVertical),
|
|
horizontal: top.concat(bottom).concat(centerHorizontal)
|
|
};
|
|
}
|
|
function getCombinedMax(maxPadding, chartArea, a, b) {
|
|
return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]);
|
|
}
|
|
function updateMaxPadding(maxPadding, boxPadding) {
|
|
maxPadding.top = Math.max(maxPadding.top, boxPadding.top);
|
|
maxPadding.left = Math.max(maxPadding.left, boxPadding.left);
|
|
maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom);
|
|
maxPadding.right = Math.max(maxPadding.right, boxPadding.right);
|
|
}
|
|
function updateDims(chartArea, params, layout, stacks) {
|
|
const { pos , box } = layout;
|
|
const maxPadding = chartArea.maxPadding;
|
|
if (!isObject(pos)) {
|
|
if (layout.size) {
|
|
chartArea[pos] -= layout.size;
|
|
}
|
|
const stack = stacks[layout.stack] || {
|
|
size: 0,
|
|
count: 1
|
|
};
|
|
stack.size = Math.max(stack.size, layout.horizontal ? box.height : box.width);
|
|
layout.size = stack.size / stack.count;
|
|
chartArea[pos] += layout.size;
|
|
}
|
|
if (box.getPadding) {
|
|
updateMaxPadding(maxPadding, box.getPadding());
|
|
}
|
|
const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right'));
|
|
const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom'));
|
|
const widthChanged = newWidth !== chartArea.w;
|
|
const heightChanged = newHeight !== chartArea.h;
|
|
chartArea.w = newWidth;
|
|
chartArea.h = newHeight;
|
|
return layout.horizontal ? {
|
|
same: widthChanged,
|
|
other: heightChanged
|
|
} : {
|
|
same: heightChanged,
|
|
other: widthChanged
|
|
};
|
|
}
|
|
function handleMaxPadding(chartArea) {
|
|
const maxPadding = chartArea.maxPadding;
|
|
function updatePos(pos) {
|
|
const change = Math.max(maxPadding[pos] - chartArea[pos], 0);
|
|
chartArea[pos] += change;
|
|
return change;
|
|
}
|
|
chartArea.y += updatePos('top');
|
|
chartArea.x += updatePos('left');
|
|
updatePos('right');
|
|
updatePos('bottom');
|
|
}
|
|
function getMargins(horizontal, chartArea) {
|
|
const maxPadding = chartArea.maxPadding;
|
|
function marginForPositions(positions) {
|
|
const margin = {
|
|
left: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
};
|
|
positions.forEach((pos)=>{
|
|
margin[pos] = Math.max(chartArea[pos], maxPadding[pos]);
|
|
});
|
|
return margin;
|
|
}
|
|
return horizontal ? marginForPositions([
|
|
'left',
|
|
'right'
|
|
]) : marginForPositions([
|
|
'top',
|
|
'bottom'
|
|
]);
|
|
}
|
|
function fitBoxes(boxes, chartArea, params, stacks) {
|
|
const refitBoxes = [];
|
|
let i, ilen, layout, box, refit, changed;
|
|
for(i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i){
|
|
layout = boxes[i];
|
|
box = layout.box;
|
|
box.update(layout.width || chartArea.w, layout.height || chartArea.h, getMargins(layout.horizontal, chartArea));
|
|
const { same , other } = updateDims(chartArea, params, layout, stacks);
|
|
refit |= same && refitBoxes.length;
|
|
changed = changed || other;
|
|
if (!box.fullSize) {
|
|
refitBoxes.push(layout);
|
|
}
|
|
}
|
|
return refit && fitBoxes(refitBoxes, chartArea, params, stacks) || changed;
|
|
}
|
|
function setBoxDims(box, left, top, width, height) {
|
|
box.top = top;
|
|
box.left = left;
|
|
box.right = left + width;
|
|
box.bottom = top + height;
|
|
box.width = width;
|
|
box.height = height;
|
|
}
|
|
function placeBoxes(boxes, chartArea, params, stacks) {
|
|
const userPadding = params.padding;
|
|
let { x , y } = chartArea;
|
|
for (const layout of boxes){
|
|
const box = layout.box;
|
|
const stack = stacks[layout.stack] || {
|
|
count: 1,
|
|
placed: 0,
|
|
weight: 1
|
|
};
|
|
const weight = layout.stackWeight / stack.weight || 1;
|
|
if (layout.horizontal) {
|
|
const width = chartArea.w * weight;
|
|
const height = stack.size || box.height;
|
|
if (defined(stack.start)) {
|
|
y = stack.start;
|
|
}
|
|
if (box.fullSize) {
|
|
setBoxDims(box, userPadding.left, y, params.outerWidth - userPadding.right - userPadding.left, height);
|
|
} else {
|
|
setBoxDims(box, chartArea.left + stack.placed, y, width, height);
|
|
}
|
|
stack.start = y;
|
|
stack.placed += width;
|
|
y = box.bottom;
|
|
} else {
|
|
const height = chartArea.h * weight;
|
|
const width = stack.size || box.width;
|
|
if (defined(stack.start)) {
|
|
x = stack.start;
|
|
}
|
|
if (box.fullSize) {
|
|
setBoxDims(box, x, userPadding.top, width, params.outerHeight - userPadding.bottom - userPadding.top);
|
|
} else {
|
|
setBoxDims(box, x, chartArea.top + stack.placed, width, height);
|
|
}
|
|
stack.start = x;
|
|
stack.placed += height;
|
|
x = box.right;
|
|
}
|
|
}
|
|
chartArea.x = x;
|
|
chartArea.y = y;
|
|
}
|
|
var layouts = {
|
|
addBox (chart, item) {
|
|
if (!chart.boxes) {
|
|
chart.boxes = [];
|
|
}
|
|
item.fullSize = item.fullSize || false;
|
|
item.position = item.position || 'top';
|
|
item.weight = item.weight || 0;
|
|
item._layers = item._layers || function() {
|
|
return [
|
|
{
|
|
z: 0,
|
|
draw (chartArea) {
|
|
item.draw(chartArea);
|
|
}
|
|
}
|
|
];
|
|
};
|
|
chart.boxes.push(item);
|
|
},
|
|
removeBox (chart, layoutItem) {
|
|
const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1;
|
|
if (index !== -1) {
|
|
chart.boxes.splice(index, 1);
|
|
}
|
|
},
|
|
configure (chart, item, options) {
|
|
item.fullSize = options.fullSize;
|
|
item.position = options.position;
|
|
item.weight = options.weight;
|
|
},
|
|
update (chart, width, height, minPadding) {
|
|
if (!chart) {
|
|
return;
|
|
}
|
|
const padding = toPadding(chart.options.layout.padding);
|
|
const availableWidth = Math.max(width - padding.width, 0);
|
|
const availableHeight = Math.max(height - padding.height, 0);
|
|
const boxes = buildLayoutBoxes(chart.boxes);
|
|
const verticalBoxes = boxes.vertical;
|
|
const horizontalBoxes = boxes.horizontal;
|
|
each(chart.boxes, (box)=>{
|
|
if (typeof box.beforeLayout === 'function') {
|
|
box.beforeLayout();
|
|
}
|
|
});
|
|
const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap)=>wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1;
|
|
const params = Object.freeze({
|
|
outerWidth: width,
|
|
outerHeight: height,
|
|
padding,
|
|
availableWidth,
|
|
availableHeight,
|
|
vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount,
|
|
hBoxMaxHeight: availableHeight / 2
|
|
});
|
|
const maxPadding = Object.assign({}, padding);
|
|
updateMaxPadding(maxPadding, toPadding(minPadding));
|
|
const chartArea = Object.assign({
|
|
maxPadding,
|
|
w: availableWidth,
|
|
h: availableHeight,
|
|
x: padding.left,
|
|
y: padding.top
|
|
}, padding);
|
|
const stacks = setLayoutDims(verticalBoxes.concat(horizontalBoxes), params);
|
|
fitBoxes(boxes.fullSize, chartArea, params, stacks);
|
|
fitBoxes(verticalBoxes, chartArea, params, stacks);
|
|
if (fitBoxes(horizontalBoxes, chartArea, params, stacks)) {
|
|
fitBoxes(verticalBoxes, chartArea, params, stacks);
|
|
}
|
|
handleMaxPadding(chartArea);
|
|
placeBoxes(boxes.leftAndTop, chartArea, params, stacks);
|
|
chartArea.x += chartArea.w;
|
|
chartArea.y += chartArea.h;
|
|
placeBoxes(boxes.rightAndBottom, chartArea, params, stacks);
|
|
chart.chartArea = {
|
|
left: chartArea.left,
|
|
top: chartArea.top,
|
|
right: chartArea.left + chartArea.w,
|
|
bottom: chartArea.top + chartArea.h,
|
|
height: chartArea.h,
|
|
width: chartArea.w
|
|
};
|
|
each(boxes.chartArea, (layout)=>{
|
|
const box = layout.box;
|
|
Object.assign(box, chart.chartArea);
|
|
box.update(chartArea.w, chartArea.h, {
|
|
left: 0,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
class BasePlatform {
|
|
acquireContext(canvas, aspectRatio) {}
|
|
releaseContext(context) {
|
|
return false;
|
|
}
|
|
addEventListener(chart, type, listener) {}
|
|
removeEventListener(chart, type, listener) {}
|
|
getDevicePixelRatio() {
|
|
return 1;
|
|
}
|
|
getMaximumSize(element, width, height, aspectRatio) {
|
|
width = Math.max(0, width || element.width);
|
|
height = height || element.height;
|
|
return {
|
|
width,
|
|
height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height)
|
|
};
|
|
}
|
|
isAttached(canvas) {
|
|
return true;
|
|
}
|
|
updateConfig(config) {
|
|
}
|
|
}
|
|
|
|
class BasicPlatform extends BasePlatform {
|
|
acquireContext(item) {
|
|
return item && item.getContext && item.getContext('2d') || null;
|
|
}
|
|
updateConfig(config) {
|
|
config.options.animation = false;
|
|
}
|
|
}
|
|
|
|
const EXPANDO_KEY = '$chartjs';
|
|
const EVENT_TYPES = {
|
|
touchstart: 'mousedown',
|
|
touchmove: 'mousemove',
|
|
touchend: 'mouseup',
|
|
pointerenter: 'mouseenter',
|
|
pointerdown: 'mousedown',
|
|
pointermove: 'mousemove',
|
|
pointerup: 'mouseup',
|
|
pointerleave: 'mouseout',
|
|
pointerout: 'mouseout'
|
|
};
|
|
const isNullOrEmpty = (value)=>value === null || value === '';
|
|
function initCanvas(canvas, aspectRatio) {
|
|
const style = canvas.style;
|
|
const renderHeight = canvas.getAttribute('height');
|
|
const renderWidth = canvas.getAttribute('width');
|
|
canvas[EXPANDO_KEY] = {
|
|
initial: {
|
|
height: renderHeight,
|
|
width: renderWidth,
|
|
style: {
|
|
display: style.display,
|
|
height: style.height,
|
|
width: style.width
|
|
}
|
|
}
|
|
};
|
|
style.display = style.display || 'block';
|
|
style.boxSizing = style.boxSizing || 'border-box';
|
|
if (isNullOrEmpty(renderWidth)) {
|
|
const displayWidth = readUsedSize(canvas, 'width');
|
|
if (displayWidth !== undefined) {
|
|
canvas.width = displayWidth;
|
|
}
|
|
}
|
|
if (isNullOrEmpty(renderHeight)) {
|
|
if (canvas.style.height === '') {
|
|
canvas.height = canvas.width / (aspectRatio || 2);
|
|
} else {
|
|
const displayHeight = readUsedSize(canvas, 'height');
|
|
if (displayHeight !== undefined) {
|
|
canvas.height = displayHeight;
|
|
}
|
|
}
|
|
}
|
|
return canvas;
|
|
}
|
|
const eventListenerOptions = supportsEventListenerOptions ? {
|
|
passive: true
|
|
} : false;
|
|
function addListener(node, type, listener) {
|
|
if (node) {
|
|
node.addEventListener(type, listener, eventListenerOptions);
|
|
}
|
|
}
|
|
function removeListener(chart, type, listener) {
|
|
if (chart && chart.canvas) {
|
|
chart.canvas.removeEventListener(type, listener, eventListenerOptions);
|
|
}
|
|
}
|
|
function fromNativeEvent(event, chart) {
|
|
const type = EVENT_TYPES[event.type] || event.type;
|
|
const { x , y } = getRelativePosition(event, chart);
|
|
return {
|
|
type,
|
|
chart,
|
|
native: event,
|
|
x: x !== undefined ? x : null,
|
|
y: y !== undefined ? y : null
|
|
};
|
|
}
|
|
function nodeListContains(nodeList, canvas) {
|
|
for (const node of nodeList){
|
|
if (node === canvas || node.contains(canvas)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
function createAttachObserver(chart, type, listener) {
|
|
const canvas = chart.canvas;
|
|
const observer = new MutationObserver((entries)=>{
|
|
let trigger = false;
|
|
for (const entry of entries){
|
|
trigger = trigger || nodeListContains(entry.addedNodes, canvas);
|
|
trigger = trigger && !nodeListContains(entry.removedNodes, canvas);
|
|
}
|
|
if (trigger) {
|
|
listener();
|
|
}
|
|
});
|
|
observer.observe(document, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
return observer;
|
|
}
|
|
function createDetachObserver(chart, type, listener) {
|
|
const canvas = chart.canvas;
|
|
const observer = new MutationObserver((entries)=>{
|
|
let trigger = false;
|
|
for (const entry of entries){
|
|
trigger = trigger || nodeListContains(entry.removedNodes, canvas);
|
|
trigger = trigger && !nodeListContains(entry.addedNodes, canvas);
|
|
}
|
|
if (trigger) {
|
|
listener();
|
|
}
|
|
});
|
|
observer.observe(document, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
return observer;
|
|
}
|
|
const drpListeningCharts = new Map();
|
|
let oldDevicePixelRatio = 0;
|
|
function onWindowResize() {
|
|
const dpr = window.devicePixelRatio;
|
|
if (dpr === oldDevicePixelRatio) {
|
|
return;
|
|
}
|
|
oldDevicePixelRatio = dpr;
|
|
drpListeningCharts.forEach((resize, chart)=>{
|
|
if (chart.currentDevicePixelRatio !== dpr) {
|
|
resize();
|
|
}
|
|
});
|
|
}
|
|
function listenDevicePixelRatioChanges(chart, resize) {
|
|
if (!drpListeningCharts.size) {
|
|
window.addEventListener('resize', onWindowResize);
|
|
}
|
|
drpListeningCharts.set(chart, resize);
|
|
}
|
|
function unlistenDevicePixelRatioChanges(chart) {
|
|
drpListeningCharts.delete(chart);
|
|
if (!drpListeningCharts.size) {
|
|
window.removeEventListener('resize', onWindowResize);
|
|
}
|
|
}
|
|
function createResizeObserver(chart, type, listener) {
|
|
const canvas = chart.canvas;
|
|
const container = canvas && _getParentNode(canvas);
|
|
if (!container) {
|
|
return;
|
|
}
|
|
const resize = throttled((width, height)=>{
|
|
const w = container.clientWidth;
|
|
listener(width, height);
|
|
if (w < container.clientWidth) {
|
|
listener();
|
|
}
|
|
}, window);
|
|
const observer = new ResizeObserver((entries)=>{
|
|
const entry = entries[0];
|
|
const width = entry.contentRect.width;
|
|
const height = entry.contentRect.height;
|
|
if (width === 0 && height === 0) {
|
|
return;
|
|
}
|
|
resize(width, height);
|
|
});
|
|
observer.observe(container);
|
|
listenDevicePixelRatioChanges(chart, resize);
|
|
return observer;
|
|
}
|
|
function releaseObserver(chart, type, observer) {
|
|
if (observer) {
|
|
observer.disconnect();
|
|
}
|
|
if (type === 'resize') {
|
|
unlistenDevicePixelRatioChanges(chart);
|
|
}
|
|
}
|
|
function createProxyAndListen(chart, type, listener) {
|
|
const canvas = chart.canvas;
|
|
const proxy = throttled((event)=>{
|
|
if (chart.ctx !== null) {
|
|
listener(fromNativeEvent(event, chart));
|
|
}
|
|
}, chart);
|
|
addListener(canvas, type, proxy);
|
|
return proxy;
|
|
}
|
|
class DomPlatform extends BasePlatform {
|
|
acquireContext(canvas, aspectRatio) {
|
|
const context = canvas && canvas.getContext && canvas.getContext('2d');
|
|
if (context && context.canvas === canvas) {
|
|
initCanvas(canvas, aspectRatio);
|
|
return context;
|
|
}
|
|
return null;
|
|
}
|
|
releaseContext(context) {
|
|
const canvas = context.canvas;
|
|
if (!canvas[EXPANDO_KEY]) {
|
|
return false;
|
|
}
|
|
const initial = canvas[EXPANDO_KEY].initial;
|
|
[
|
|
'height',
|
|
'width'
|
|
].forEach((prop)=>{
|
|
const value = initial[prop];
|
|
if (isNullOrUndef(value)) {
|
|
canvas.removeAttribute(prop);
|
|
} else {
|
|
canvas.setAttribute(prop, value);
|
|
}
|
|
});
|
|
const style = initial.style || {};
|
|
Object.keys(style).forEach((key)=>{
|
|
canvas.style[key] = style[key];
|
|
});
|
|
canvas.width = canvas.width;
|
|
delete canvas[EXPANDO_KEY];
|
|
return true;
|
|
}
|
|
addEventListener(chart, type, listener) {
|
|
this.removeEventListener(chart, type);
|
|
const proxies = chart.$proxies || (chart.$proxies = {});
|
|
const handlers = {
|
|
attach: createAttachObserver,
|
|
detach: createDetachObserver,
|
|
resize: createResizeObserver
|
|
};
|
|
const handler = handlers[type] || createProxyAndListen;
|
|
proxies[type] = handler(chart, type, listener);
|
|
}
|
|
removeEventListener(chart, type) {
|
|
const proxies = chart.$proxies || (chart.$proxies = {});
|
|
const proxy = proxies[type];
|
|
if (!proxy) {
|
|
return;
|
|
}
|
|
const handlers = {
|
|
attach: releaseObserver,
|
|
detach: releaseObserver,
|
|
resize: releaseObserver
|
|
};
|
|
const handler = handlers[type] || removeListener;
|
|
handler(chart, type, proxy);
|
|
proxies[type] = undefined;
|
|
}
|
|
getDevicePixelRatio() {
|
|
return window.devicePixelRatio;
|
|
}
|
|
getMaximumSize(canvas, width, height, aspectRatio) {
|
|
return getMaximumSize(canvas, width, height, aspectRatio);
|
|
}
|
|
isAttached(canvas) {
|
|
const container = canvas && _getParentNode(canvas);
|
|
return !!(container && container.isConnected);
|
|
}
|
|
}
|
|
|
|
function _detectPlatform(canvas) {
|
|
if (!_isDomSupported() || typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas) {
|
|
return BasicPlatform;
|
|
}
|
|
return DomPlatform;
|
|
}
|
|
|
|
class chart_Element {
|
|
static defaults = {};
|
|
static defaultRoutes = undefined;
|
|
x;
|
|
y;
|
|
active = false;
|
|
options;
|
|
$animations;
|
|
tooltipPosition(useFinalPosition) {
|
|
const { x , y } = this.getProps([
|
|
'x',
|
|
'y'
|
|
], useFinalPosition);
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
hasValue() {
|
|
return isNumber(this.x) && isNumber(this.y);
|
|
}
|
|
getProps(props, final) {
|
|
const anims = this.$animations;
|
|
if (!final || !anims) {
|
|
// let's not create an object, if not needed
|
|
return this;
|
|
}
|
|
const ret = {};
|
|
props.forEach((prop)=>{
|
|
ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : this[prop];
|
|
});
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
function autoSkip(scale, ticks) {
|
|
const tickOpts = scale.options.ticks;
|
|
const determinedMaxTicks = determineMaxTicks(scale);
|
|
const ticksLimit = Math.min(tickOpts.maxTicksLimit || determinedMaxTicks, determinedMaxTicks);
|
|
const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : [];
|
|
const numMajorIndices = majorIndices.length;
|
|
const first = majorIndices[0];
|
|
const last = majorIndices[numMajorIndices - 1];
|
|
const newTicks = [];
|
|
if (numMajorIndices > ticksLimit) {
|
|
skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit);
|
|
return newTicks;
|
|
}
|
|
const spacing = calculateSpacing(majorIndices, ticks, ticksLimit);
|
|
if (numMajorIndices > 0) {
|
|
let i, ilen;
|
|
const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null;
|
|
skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first);
|
|
for(i = 0, ilen = numMajorIndices - 1; i < ilen; i++){
|
|
skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]);
|
|
}
|
|
skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing);
|
|
return newTicks;
|
|
}
|
|
skip(ticks, newTicks, spacing);
|
|
return newTicks;
|
|
}
|
|
function determineMaxTicks(scale) {
|
|
const offset = scale.options.offset;
|
|
const tickLength = scale._tickSize();
|
|
const maxScale = scale._length / tickLength + (offset ? 0 : 1);
|
|
const maxChart = scale._maxLength / tickLength;
|
|
return Math.floor(Math.min(maxScale, maxChart));
|
|
}
|
|
function calculateSpacing(majorIndices, ticks, ticksLimit) {
|
|
const evenMajorSpacing = getEvenSpacing(majorIndices);
|
|
const spacing = ticks.length / ticksLimit;
|
|
if (!evenMajorSpacing) {
|
|
return Math.max(spacing, 1);
|
|
}
|
|
const factors = _factorize(evenMajorSpacing);
|
|
for(let i = 0, ilen = factors.length - 1; i < ilen; i++){
|
|
const factor = factors[i];
|
|
if (factor > spacing) {
|
|
return factor;
|
|
}
|
|
}
|
|
return Math.max(spacing, 1);
|
|
}
|
|
function getMajorIndices(ticks) {
|
|
const result = [];
|
|
let i, ilen;
|
|
for(i = 0, ilen = ticks.length; i < ilen; i++){
|
|
if (ticks[i].major) {
|
|
result.push(i);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function skipMajors(ticks, newTicks, majorIndices, spacing) {
|
|
let count = 0;
|
|
let next = majorIndices[0];
|
|
let i;
|
|
spacing = Math.ceil(spacing);
|
|
for(i = 0; i < ticks.length; i++){
|
|
if (i === next) {
|
|
newTicks.push(ticks[i]);
|
|
count++;
|
|
next = majorIndices[count * spacing];
|
|
}
|
|
}
|
|
}
|
|
function skip(ticks, newTicks, spacing, majorStart, majorEnd) {
|
|
const start = valueOrDefault(majorStart, 0);
|
|
const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length);
|
|
let count = 0;
|
|
let length, i, next;
|
|
spacing = Math.ceil(spacing);
|
|
if (majorEnd) {
|
|
length = majorEnd - majorStart;
|
|
spacing = length / Math.floor(length / spacing);
|
|
}
|
|
next = start;
|
|
while(next < 0){
|
|
count++;
|
|
next = Math.round(start + count * spacing);
|
|
}
|
|
for(i = Math.max(start, 0); i < end; i++){
|
|
if (i === next) {
|
|
newTicks.push(ticks[i]);
|
|
count++;
|
|
next = Math.round(start + count * spacing);
|
|
}
|
|
}
|
|
}
|
|
function getEvenSpacing(arr) {
|
|
const len = arr.length;
|
|
let i, diff;
|
|
if (len < 2) {
|
|
return false;
|
|
}
|
|
for(diff = arr[0], i = 1; i < len; ++i){
|
|
if (arr[i] - arr[i - 1] !== diff) {
|
|
return false;
|
|
}
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
const reverseAlign = (align)=>align === 'left' ? 'right' : align === 'right' ? 'left' : align;
|
|
const offsetFromEdge = (scale, edge, offset)=>edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset;
|
|
const getTicksLimit = (ticksLength, maxTicksLimit)=>Math.min(maxTicksLimit || ticksLength, ticksLength);
|
|
function sample(arr, numItems) {
|
|
const result = [];
|
|
const increment = arr.length / numItems;
|
|
const len = arr.length;
|
|
let i = 0;
|
|
for(; i < len; i += increment){
|
|
result.push(arr[Math.floor(i)]);
|
|
}
|
|
return result;
|
|
}
|
|
function getPixelForGridLine(scale, index, offsetGridLines) {
|
|
const length = scale.ticks.length;
|
|
const validIndex = Math.min(index, length - 1);
|
|
const start = scale._startPixel;
|
|
const end = scale._endPixel;
|
|
const epsilon = 1e-6;
|
|
let lineValue = scale.getPixelForTick(validIndex);
|
|
let offset;
|
|
if (offsetGridLines) {
|
|
if (length === 1) {
|
|
offset = Math.max(lineValue - start, end - lineValue);
|
|
} else if (index === 0) {
|
|
offset = (scale.getPixelForTick(1) - lineValue) / 2;
|
|
} else {
|
|
offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2;
|
|
}
|
|
lineValue += validIndex < index ? offset : -offset;
|
|
if (lineValue < start - epsilon || lineValue > end + epsilon) {
|
|
return;
|
|
}
|
|
}
|
|
return lineValue;
|
|
}
|
|
function garbageCollect(caches, length) {
|
|
each(caches, (cache)=>{
|
|
const gc = cache.gc;
|
|
const gcLen = gc.length / 2;
|
|
let i;
|
|
if (gcLen > length) {
|
|
for(i = 0; i < gcLen; ++i){
|
|
delete cache.data[gc[i]];
|
|
}
|
|
gc.splice(0, gcLen);
|
|
}
|
|
});
|
|
}
|
|
function getTickMarkLength(options) {
|
|
return options.drawTicks ? options.tickLength : 0;
|
|
}
|
|
function getTitleHeight(options, fallback) {
|
|
if (!options.display) {
|
|
return 0;
|
|
}
|
|
const font = toFont(options.font, fallback);
|
|
const padding = toPadding(options.padding);
|
|
const lines = isArray(options.text) ? options.text.length : 1;
|
|
return lines * font.lineHeight + padding.height;
|
|
}
|
|
function createScaleContext(parent, scale) {
|
|
return createContext(parent, {
|
|
scale,
|
|
type: 'scale'
|
|
});
|
|
}
|
|
function createTickContext(parent, index, tick) {
|
|
return createContext(parent, {
|
|
tick,
|
|
index,
|
|
type: 'tick'
|
|
});
|
|
}
|
|
function titleAlign(align, position, reverse) {
|
|
let ret = _toLeftRightCenter(align);
|
|
if (reverse && position !== 'right' || !reverse && position === 'right') {
|
|
ret = reverseAlign(ret);
|
|
}
|
|
return ret;
|
|
}
|
|
function titleArgs(scale, offset, position, align) {
|
|
const { top , left , bottom , right , chart } = scale;
|
|
const { chartArea , scales } = chart;
|
|
let rotation = 0;
|
|
let maxWidth, titleX, titleY;
|
|
const height = bottom - top;
|
|
const width = right - left;
|
|
if (scale.isHorizontal()) {
|
|
titleX = _alignStartEnd(align, left, right);
|
|
if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
titleY = scales[positionAxisID].getPixelForValue(value) + height - offset;
|
|
} else if (position === 'center') {
|
|
titleY = (chartArea.bottom + chartArea.top) / 2 + height - offset;
|
|
} else {
|
|
titleY = offsetFromEdge(scale, position, offset);
|
|
}
|
|
maxWidth = right - left;
|
|
} else {
|
|
if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
titleX = scales[positionAxisID].getPixelForValue(value) - width + offset;
|
|
} else if (position === 'center') {
|
|
titleX = (chartArea.left + chartArea.right) / 2 - width + offset;
|
|
} else {
|
|
titleX = offsetFromEdge(scale, position, offset);
|
|
}
|
|
titleY = _alignStartEnd(align, bottom, top);
|
|
rotation = position === 'left' ? -HALF_PI : HALF_PI;
|
|
}
|
|
return {
|
|
titleX,
|
|
titleY,
|
|
maxWidth,
|
|
rotation
|
|
};
|
|
}
|
|
class Scale extends chart_Element {
|
|
constructor(cfg){
|
|
super();
|
|
this.id = cfg.id;
|
|
this.type = cfg.type;
|
|
this.options = undefined;
|
|
this.ctx = cfg.ctx;
|
|
this.chart = cfg.chart;
|
|
this.top = undefined;
|
|
this.bottom = undefined;
|
|
this.left = undefined;
|
|
this.right = undefined;
|
|
this.width = undefined;
|
|
this.height = undefined;
|
|
this._margins = {
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0
|
|
};
|
|
this.maxWidth = undefined;
|
|
this.maxHeight = undefined;
|
|
this.paddingTop = undefined;
|
|
this.paddingBottom = undefined;
|
|
this.paddingLeft = undefined;
|
|
this.paddingRight = undefined;
|
|
this.axis = undefined;
|
|
this.labelRotation = undefined;
|
|
this.min = undefined;
|
|
this.max = undefined;
|
|
this._range = undefined;
|
|
this.ticks = [];
|
|
this._gridLineItems = null;
|
|
this._labelItems = null;
|
|
this._labelSizes = null;
|
|
this._length = 0;
|
|
this._maxLength = 0;
|
|
this._longestTextCache = {};
|
|
this._startPixel = undefined;
|
|
this._endPixel = undefined;
|
|
this._reversePixels = false;
|
|
this._userMax = undefined;
|
|
this._userMin = undefined;
|
|
this._suggestedMax = undefined;
|
|
this._suggestedMin = undefined;
|
|
this._ticksLength = 0;
|
|
this._borderValue = 0;
|
|
this._cache = {};
|
|
this._dataLimitsCached = false;
|
|
this.$context = undefined;
|
|
}
|
|
init(options) {
|
|
this.options = options.setContext(this.getContext());
|
|
this.axis = options.axis;
|
|
this._userMin = this.parse(options.min);
|
|
this._userMax = this.parse(options.max);
|
|
this._suggestedMin = this.parse(options.suggestedMin);
|
|
this._suggestedMax = this.parse(options.suggestedMax);
|
|
}
|
|
parse(raw, index) {
|
|
return raw;
|
|
}
|
|
getUserBounds() {
|
|
let { _userMin , _userMax , _suggestedMin , _suggestedMax } = this;
|
|
_userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY);
|
|
_userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY);
|
|
_suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY);
|
|
_suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY);
|
|
return {
|
|
min: finiteOrDefault(_userMin, _suggestedMin),
|
|
max: finiteOrDefault(_userMax, _suggestedMax),
|
|
minDefined: isNumberFinite(_userMin),
|
|
maxDefined: isNumberFinite(_userMax)
|
|
};
|
|
}
|
|
getMinMax(canStack) {
|
|
let { min , max , minDefined , maxDefined } = this.getUserBounds();
|
|
let range;
|
|
if (minDefined && maxDefined) {
|
|
return {
|
|
min,
|
|
max
|
|
};
|
|
}
|
|
const metas = this.getMatchingVisibleMetas();
|
|
for(let i = 0, ilen = metas.length; i < ilen; ++i){
|
|
range = metas[i].controller.getMinMax(this, canStack);
|
|
if (!minDefined) {
|
|
min = Math.min(min, range.min);
|
|
}
|
|
if (!maxDefined) {
|
|
max = Math.max(max, range.max);
|
|
}
|
|
}
|
|
min = maxDefined && min > max ? max : min;
|
|
max = minDefined && min > max ? min : max;
|
|
return {
|
|
min: finiteOrDefault(min, finiteOrDefault(max, min)),
|
|
max: finiteOrDefault(max, finiteOrDefault(min, max))
|
|
};
|
|
}
|
|
getPadding() {
|
|
return {
|
|
left: this.paddingLeft || 0,
|
|
top: this.paddingTop || 0,
|
|
right: this.paddingRight || 0,
|
|
bottom: this.paddingBottom || 0
|
|
};
|
|
}
|
|
getTicks() {
|
|
return this.ticks;
|
|
}
|
|
getLabels() {
|
|
const data = this.chart.data;
|
|
return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || [];
|
|
}
|
|
getLabelItems(chartArea = this.chart.chartArea) {
|
|
const items = this._labelItems || (this._labelItems = this._computeLabelItems(chartArea));
|
|
return items;
|
|
}
|
|
beforeLayout() {
|
|
this._cache = {};
|
|
this._dataLimitsCached = false;
|
|
}
|
|
beforeUpdate() {
|
|
callback(this.options.beforeUpdate, [
|
|
this
|
|
]);
|
|
}
|
|
update(maxWidth, maxHeight, margins) {
|
|
const { beginAtZero , grace , ticks: tickOpts } = this.options;
|
|
const sampleSize = tickOpts.sampleSize;
|
|
this.beforeUpdate();
|
|
this.maxWidth = maxWidth;
|
|
this.maxHeight = maxHeight;
|
|
this._margins = margins = Object.assign({
|
|
left: 0,
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0
|
|
}, margins);
|
|
this.ticks = null;
|
|
this._labelSizes = null;
|
|
this._gridLineItems = null;
|
|
this._labelItems = null;
|
|
this.beforeSetDimensions();
|
|
this.setDimensions();
|
|
this.afterSetDimensions();
|
|
this._maxLength = this.isHorizontal() ? this.width + margins.left + margins.right : this.height + margins.top + margins.bottom;
|
|
if (!this._dataLimitsCached) {
|
|
this.beforeDataLimits();
|
|
this.determineDataLimits();
|
|
this.afterDataLimits();
|
|
this._range = _addGrace(this, grace, beginAtZero);
|
|
this._dataLimitsCached = true;
|
|
}
|
|
this.beforeBuildTicks();
|
|
this.ticks = this.buildTicks() || [];
|
|
this.afterBuildTicks();
|
|
const samplingEnabled = sampleSize < this.ticks.length;
|
|
this._convertTicksToLabels(samplingEnabled ? sample(this.ticks, sampleSize) : this.ticks);
|
|
this.configure();
|
|
this.beforeCalculateLabelRotation();
|
|
this.calculateLabelRotation();
|
|
this.afterCalculateLabelRotation();
|
|
if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) {
|
|
this.ticks = autoSkip(this, this.ticks);
|
|
this._labelSizes = null;
|
|
this.afterAutoSkip();
|
|
}
|
|
if (samplingEnabled) {
|
|
this._convertTicksToLabels(this.ticks);
|
|
}
|
|
this.beforeFit();
|
|
this.fit();
|
|
this.afterFit();
|
|
this.afterUpdate();
|
|
}
|
|
configure() {
|
|
let reversePixels = this.options.reverse;
|
|
let startPixel, endPixel;
|
|
if (this.isHorizontal()) {
|
|
startPixel = this.left;
|
|
endPixel = this.right;
|
|
} else {
|
|
startPixel = this.top;
|
|
endPixel = this.bottom;
|
|
reversePixels = !reversePixels;
|
|
}
|
|
this._startPixel = startPixel;
|
|
this._endPixel = endPixel;
|
|
this._reversePixels = reversePixels;
|
|
this._length = endPixel - startPixel;
|
|
this._alignToPixels = this.options.alignToPixels;
|
|
}
|
|
afterUpdate() {
|
|
callback(this.options.afterUpdate, [
|
|
this
|
|
]);
|
|
}
|
|
beforeSetDimensions() {
|
|
callback(this.options.beforeSetDimensions, [
|
|
this
|
|
]);
|
|
}
|
|
setDimensions() {
|
|
if (this.isHorizontal()) {
|
|
this.width = this.maxWidth;
|
|
this.left = 0;
|
|
this.right = this.width;
|
|
} else {
|
|
this.height = this.maxHeight;
|
|
this.top = 0;
|
|
this.bottom = this.height;
|
|
}
|
|
this.paddingLeft = 0;
|
|
this.paddingTop = 0;
|
|
this.paddingRight = 0;
|
|
this.paddingBottom = 0;
|
|
}
|
|
afterSetDimensions() {
|
|
callback(this.options.afterSetDimensions, [
|
|
this
|
|
]);
|
|
}
|
|
_callHooks(name) {
|
|
this.chart.notifyPlugins(name, this.getContext());
|
|
callback(this.options[name], [
|
|
this
|
|
]);
|
|
}
|
|
beforeDataLimits() {
|
|
this._callHooks('beforeDataLimits');
|
|
}
|
|
determineDataLimits() {}
|
|
afterDataLimits() {
|
|
this._callHooks('afterDataLimits');
|
|
}
|
|
beforeBuildTicks() {
|
|
this._callHooks('beforeBuildTicks');
|
|
}
|
|
buildTicks() {
|
|
return [];
|
|
}
|
|
afterBuildTicks() {
|
|
this._callHooks('afterBuildTicks');
|
|
}
|
|
beforeTickToLabelConversion() {
|
|
callback(this.options.beforeTickToLabelConversion, [
|
|
this
|
|
]);
|
|
}
|
|
generateTickLabels(ticks) {
|
|
const tickOpts = this.options.ticks;
|
|
let i, ilen, tick;
|
|
for(i = 0, ilen = ticks.length; i < ilen; i++){
|
|
tick = ticks[i];
|
|
tick.label = callback(tickOpts.callback, [
|
|
tick.value,
|
|
i,
|
|
ticks
|
|
], this);
|
|
}
|
|
}
|
|
afterTickToLabelConversion() {
|
|
callback(this.options.afterTickToLabelConversion, [
|
|
this
|
|
]);
|
|
}
|
|
beforeCalculateLabelRotation() {
|
|
callback(this.options.beforeCalculateLabelRotation, [
|
|
this
|
|
]);
|
|
}
|
|
calculateLabelRotation() {
|
|
const options = this.options;
|
|
const tickOpts = options.ticks;
|
|
const numTicks = getTicksLimit(this.ticks.length, options.ticks.maxTicksLimit);
|
|
const minRotation = tickOpts.minRotation || 0;
|
|
const maxRotation = tickOpts.maxRotation;
|
|
let labelRotation = minRotation;
|
|
let tickWidth, maxHeight, maxLabelDiagonal;
|
|
if (!this._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !this.isHorizontal()) {
|
|
this.labelRotation = minRotation;
|
|
return;
|
|
}
|
|
const labelSizes = this._getLabelSizes();
|
|
const maxLabelWidth = labelSizes.widest.width;
|
|
const maxLabelHeight = labelSizes.highest.height;
|
|
const maxWidth = _limitValue(this.chart.width - maxLabelWidth, 0, this.maxWidth);
|
|
tickWidth = options.offset ? this.maxWidth / numTicks : maxWidth / (numTicks - 1);
|
|
if (maxLabelWidth + 6 > tickWidth) {
|
|
tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1));
|
|
maxHeight = this.maxHeight - getTickMarkLength(options.grid) - tickOpts.padding - getTitleHeight(options.title, this.chart.options.font);
|
|
maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight);
|
|
labelRotation = toDegrees(Math.min(Math.asin(_limitValue((labelSizes.highest.height + 6) / tickWidth, -1, 1)), Math.asin(_limitValue(maxHeight / maxLabelDiagonal, -1, 1)) - Math.asin(_limitValue(maxLabelHeight / maxLabelDiagonal, -1, 1))));
|
|
labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation));
|
|
}
|
|
this.labelRotation = labelRotation;
|
|
}
|
|
afterCalculateLabelRotation() {
|
|
callback(this.options.afterCalculateLabelRotation, [
|
|
this
|
|
]);
|
|
}
|
|
afterAutoSkip() {}
|
|
beforeFit() {
|
|
callback(this.options.beforeFit, [
|
|
this
|
|
]);
|
|
}
|
|
fit() {
|
|
const minSize = {
|
|
width: 0,
|
|
height: 0
|
|
};
|
|
const { chart , options: { ticks: tickOpts , title: titleOpts , grid: gridOpts } } = this;
|
|
const display = this._isVisible();
|
|
const isHorizontal = this.isHorizontal();
|
|
if (display) {
|
|
const titleHeight = getTitleHeight(titleOpts, chart.options.font);
|
|
if (isHorizontal) {
|
|
minSize.width = this.maxWidth;
|
|
minSize.height = getTickMarkLength(gridOpts) + titleHeight;
|
|
} else {
|
|
minSize.height = this.maxHeight;
|
|
minSize.width = getTickMarkLength(gridOpts) + titleHeight;
|
|
}
|
|
if (tickOpts.display && this.ticks.length) {
|
|
const { first , last , widest , highest } = this._getLabelSizes();
|
|
const tickPadding = tickOpts.padding * 2;
|
|
const angleRadians = toRadians(this.labelRotation);
|
|
const cos = Math.cos(angleRadians);
|
|
const sin = Math.sin(angleRadians);
|
|
if (isHorizontal) {
|
|
const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height;
|
|
minSize.height = Math.min(this.maxHeight, minSize.height + labelHeight + tickPadding);
|
|
} else {
|
|
const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height;
|
|
minSize.width = Math.min(this.maxWidth, minSize.width + labelWidth + tickPadding);
|
|
}
|
|
this._calculatePadding(first, last, sin, cos);
|
|
}
|
|
}
|
|
this._handleMargins();
|
|
if (isHorizontal) {
|
|
this.width = this._length = chart.width - this._margins.left - this._margins.right;
|
|
this.height = minSize.height;
|
|
} else {
|
|
this.width = minSize.width;
|
|
this.height = this._length = chart.height - this._margins.top - this._margins.bottom;
|
|
}
|
|
}
|
|
_calculatePadding(first, last, sin, cos) {
|
|
const { ticks: { align , padding } , position } = this.options;
|
|
const isRotated = this.labelRotation !== 0;
|
|
const labelsBelowTicks = position !== 'top' && this.axis === 'x';
|
|
if (this.isHorizontal()) {
|
|
const offsetLeft = this.getPixelForTick(0) - this.left;
|
|
const offsetRight = this.right - this.getPixelForTick(this.ticks.length - 1);
|
|
let paddingLeft = 0;
|
|
let paddingRight = 0;
|
|
if (isRotated) {
|
|
if (labelsBelowTicks) {
|
|
paddingLeft = cos * first.width;
|
|
paddingRight = sin * last.height;
|
|
} else {
|
|
paddingLeft = sin * first.height;
|
|
paddingRight = cos * last.width;
|
|
}
|
|
} else if (align === 'start') {
|
|
paddingRight = last.width;
|
|
} else if (align === 'end') {
|
|
paddingLeft = first.width;
|
|
} else if (align !== 'inner') {
|
|
paddingLeft = first.width / 2;
|
|
paddingRight = last.width / 2;
|
|
}
|
|
this.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * this.width / (this.width - offsetLeft), 0);
|
|
this.paddingRight = Math.max((paddingRight - offsetRight + padding) * this.width / (this.width - offsetRight), 0);
|
|
} else {
|
|
let paddingTop = last.height / 2;
|
|
let paddingBottom = first.height / 2;
|
|
if (align === 'start') {
|
|
paddingTop = 0;
|
|
paddingBottom = first.height;
|
|
} else if (align === 'end') {
|
|
paddingTop = last.height;
|
|
paddingBottom = 0;
|
|
}
|
|
this.paddingTop = paddingTop + padding;
|
|
this.paddingBottom = paddingBottom + padding;
|
|
}
|
|
}
|
|
_handleMargins() {
|
|
if (this._margins) {
|
|
this._margins.left = Math.max(this.paddingLeft, this._margins.left);
|
|
this._margins.top = Math.max(this.paddingTop, this._margins.top);
|
|
this._margins.right = Math.max(this.paddingRight, this._margins.right);
|
|
this._margins.bottom = Math.max(this.paddingBottom, this._margins.bottom);
|
|
}
|
|
}
|
|
afterFit() {
|
|
callback(this.options.afterFit, [
|
|
this
|
|
]);
|
|
}
|
|
isHorizontal() {
|
|
const { axis , position } = this.options;
|
|
return position === 'top' || position === 'bottom' || axis === 'x';
|
|
}
|
|
isFullSize() {
|
|
return this.options.fullSize;
|
|
}
|
|
_convertTicksToLabels(ticks) {
|
|
this.beforeTickToLabelConversion();
|
|
this.generateTickLabels(ticks);
|
|
let i, ilen;
|
|
for(i = 0, ilen = ticks.length; i < ilen; i++){
|
|
if (isNullOrUndef(ticks[i].label)) {
|
|
ticks.splice(i, 1);
|
|
ilen--;
|
|
i--;
|
|
}
|
|
}
|
|
this.afterTickToLabelConversion();
|
|
}
|
|
_getLabelSizes() {
|
|
let labelSizes = this._labelSizes;
|
|
if (!labelSizes) {
|
|
const sampleSize = this.options.ticks.sampleSize;
|
|
let ticks = this.ticks;
|
|
if (sampleSize < ticks.length) {
|
|
ticks = sample(ticks, sampleSize);
|
|
}
|
|
this._labelSizes = labelSizes = this._computeLabelSizes(ticks, ticks.length, this.options.ticks.maxTicksLimit);
|
|
}
|
|
return labelSizes;
|
|
}
|
|
_computeLabelSizes(ticks, length, maxTicksLimit) {
|
|
const { ctx , _longestTextCache: caches } = this;
|
|
const widths = [];
|
|
const heights = [];
|
|
const increment = Math.floor(length / getTicksLimit(length, maxTicksLimit));
|
|
let widestLabelSize = 0;
|
|
let highestLabelSize = 0;
|
|
let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel;
|
|
for(i = 0; i < length; i += increment){
|
|
label = ticks[i].label;
|
|
tickFont = this._resolveTickFontOptions(i);
|
|
ctx.font = fontString = tickFont.string;
|
|
cache = caches[fontString] = caches[fontString] || {
|
|
data: {},
|
|
gc: []
|
|
};
|
|
lineHeight = tickFont.lineHeight;
|
|
width = height = 0;
|
|
if (!isNullOrUndef(label) && !isArray(label)) {
|
|
width = _measureText(ctx, cache.data, cache.gc, width, label);
|
|
height = lineHeight;
|
|
} else if (isArray(label)) {
|
|
for(j = 0, jlen = label.length; j < jlen; ++j){
|
|
nestedLabel = label[j];
|
|
if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) {
|
|
width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel);
|
|
height += lineHeight;
|
|
}
|
|
}
|
|
}
|
|
widths.push(width);
|
|
heights.push(height);
|
|
widestLabelSize = Math.max(width, widestLabelSize);
|
|
highestLabelSize = Math.max(height, highestLabelSize);
|
|
}
|
|
garbageCollect(caches, length);
|
|
const widest = widths.indexOf(widestLabelSize);
|
|
const highest = heights.indexOf(highestLabelSize);
|
|
const valueAt = (idx)=>({
|
|
width: widths[idx] || 0,
|
|
height: heights[idx] || 0
|
|
});
|
|
return {
|
|
first: valueAt(0),
|
|
last: valueAt(length - 1),
|
|
widest: valueAt(widest),
|
|
highest: valueAt(highest),
|
|
widths,
|
|
heights
|
|
};
|
|
}
|
|
getLabelForValue(value) {
|
|
return value;
|
|
}
|
|
getPixelForValue(value, index) {
|
|
return NaN;
|
|
}
|
|
getValueForPixel(pixel) {}
|
|
getPixelForTick(index) {
|
|
const ticks = this.ticks;
|
|
if (index < 0 || index > ticks.length - 1) {
|
|
return null;
|
|
}
|
|
return this.getPixelForValue(ticks[index].value);
|
|
}
|
|
getPixelForDecimal(decimal) {
|
|
if (this._reversePixels) {
|
|
decimal = 1 - decimal;
|
|
}
|
|
const pixel = this._startPixel + decimal * this._length;
|
|
return _int16Range(this._alignToPixels ? _alignPixel(this.chart, pixel, 0) : pixel);
|
|
}
|
|
getDecimalForPixel(pixel) {
|
|
const decimal = (pixel - this._startPixel) / this._length;
|
|
return this._reversePixels ? 1 - decimal : decimal;
|
|
}
|
|
getBasePixel() {
|
|
return this.getPixelForValue(this.getBaseValue());
|
|
}
|
|
getBaseValue() {
|
|
const { min , max } = this;
|
|
return min < 0 && max < 0 ? max : min > 0 && max > 0 ? min : 0;
|
|
}
|
|
getContext(index) {
|
|
const ticks = this.ticks || [];
|
|
if (index >= 0 && index < ticks.length) {
|
|
const tick = ticks[index];
|
|
return tick.$context || (tick.$context = createTickContext(this.getContext(), index, tick));
|
|
}
|
|
return this.$context || (this.$context = createScaleContext(this.chart.getContext(), this));
|
|
}
|
|
_tickSize() {
|
|
const optionTicks = this.options.ticks;
|
|
const rot = toRadians(this.labelRotation);
|
|
const cos = Math.abs(Math.cos(rot));
|
|
const sin = Math.abs(Math.sin(rot));
|
|
const labelSizes = this._getLabelSizes();
|
|
const padding = optionTicks.autoSkipPadding || 0;
|
|
const w = labelSizes ? labelSizes.widest.width + padding : 0;
|
|
const h = labelSizes ? labelSizes.highest.height + padding : 0;
|
|
return this.isHorizontal() ? h * cos > w * sin ? w / cos : h / sin : h * sin < w * cos ? h / cos : w / sin;
|
|
}
|
|
_isVisible() {
|
|
const display = this.options.display;
|
|
if (display !== 'auto') {
|
|
return !!display;
|
|
}
|
|
return this.getMatchingVisibleMetas().length > 0;
|
|
}
|
|
_computeGridLineItems(chartArea) {
|
|
const axis = this.axis;
|
|
const chart = this.chart;
|
|
const options = this.options;
|
|
const { grid , position , border } = options;
|
|
const offset = grid.offset;
|
|
const isHorizontal = this.isHorizontal();
|
|
const ticks = this.ticks;
|
|
const ticksLength = ticks.length + (offset ? 1 : 0);
|
|
const tl = getTickMarkLength(grid);
|
|
const items = [];
|
|
const borderOpts = border.setContext(this.getContext());
|
|
const axisWidth = borderOpts.display ? borderOpts.width : 0;
|
|
const axisHalfWidth = axisWidth / 2;
|
|
const alignBorderValue = function(pixel) {
|
|
return _alignPixel(chart, pixel, axisWidth);
|
|
};
|
|
let borderValue, i, lineValue, alignedLineValue;
|
|
let tx1, ty1, tx2, ty2, x1, y1, x2, y2;
|
|
if (position === 'top') {
|
|
borderValue = alignBorderValue(this.bottom);
|
|
ty1 = this.bottom - tl;
|
|
ty2 = borderValue - axisHalfWidth;
|
|
y1 = alignBorderValue(chartArea.top) + axisHalfWidth;
|
|
y2 = chartArea.bottom;
|
|
} else if (position === 'bottom') {
|
|
borderValue = alignBorderValue(this.top);
|
|
y1 = chartArea.top;
|
|
y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth;
|
|
ty1 = borderValue + axisHalfWidth;
|
|
ty2 = this.top + tl;
|
|
} else if (position === 'left') {
|
|
borderValue = alignBorderValue(this.right);
|
|
tx1 = this.right - tl;
|
|
tx2 = borderValue - axisHalfWidth;
|
|
x1 = alignBorderValue(chartArea.left) + axisHalfWidth;
|
|
x2 = chartArea.right;
|
|
} else if (position === 'right') {
|
|
borderValue = alignBorderValue(this.left);
|
|
x1 = chartArea.left;
|
|
x2 = alignBorderValue(chartArea.right) - axisHalfWidth;
|
|
tx1 = borderValue + axisHalfWidth;
|
|
tx2 = this.left + tl;
|
|
} else if (axis === 'x') {
|
|
if (position === 'center') {
|
|
borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5);
|
|
} else if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));
|
|
}
|
|
y1 = chartArea.top;
|
|
y2 = chartArea.bottom;
|
|
ty1 = borderValue + axisHalfWidth;
|
|
ty2 = ty1 + tl;
|
|
} else if (axis === 'y') {
|
|
if (position === 'center') {
|
|
borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2);
|
|
} else if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));
|
|
}
|
|
tx1 = borderValue - axisHalfWidth;
|
|
tx2 = tx1 - tl;
|
|
x1 = chartArea.left;
|
|
x2 = chartArea.right;
|
|
}
|
|
const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength);
|
|
const step = Math.max(1, Math.ceil(ticksLength / limit));
|
|
for(i = 0; i < ticksLength; i += step){
|
|
const context = this.getContext(i);
|
|
const optsAtIndex = grid.setContext(context);
|
|
const optsAtIndexBorder = border.setContext(context);
|
|
const lineWidth = optsAtIndex.lineWidth;
|
|
const lineColor = optsAtIndex.color;
|
|
const borderDash = optsAtIndexBorder.dash || [];
|
|
const borderDashOffset = optsAtIndexBorder.dashOffset;
|
|
const tickWidth = optsAtIndex.tickWidth;
|
|
const tickColor = optsAtIndex.tickColor;
|
|
const tickBorderDash = optsAtIndex.tickBorderDash || [];
|
|
const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset;
|
|
lineValue = getPixelForGridLine(this, i, offset);
|
|
if (lineValue === undefined) {
|
|
continue;
|
|
}
|
|
alignedLineValue = _alignPixel(chart, lineValue, lineWidth);
|
|
if (isHorizontal) {
|
|
tx1 = tx2 = x1 = x2 = alignedLineValue;
|
|
} else {
|
|
ty1 = ty2 = y1 = y2 = alignedLineValue;
|
|
}
|
|
items.push({
|
|
tx1,
|
|
ty1,
|
|
tx2,
|
|
ty2,
|
|
x1,
|
|
y1,
|
|
x2,
|
|
y2,
|
|
width: lineWidth,
|
|
color: lineColor,
|
|
borderDash,
|
|
borderDashOffset,
|
|
tickWidth,
|
|
tickColor,
|
|
tickBorderDash,
|
|
tickBorderDashOffset
|
|
});
|
|
}
|
|
this._ticksLength = ticksLength;
|
|
this._borderValue = borderValue;
|
|
return items;
|
|
}
|
|
_computeLabelItems(chartArea) {
|
|
const axis = this.axis;
|
|
const options = this.options;
|
|
const { position , ticks: optionTicks } = options;
|
|
const isHorizontal = this.isHorizontal();
|
|
const ticks = this.ticks;
|
|
const { align , crossAlign , padding , mirror } = optionTicks;
|
|
const tl = getTickMarkLength(options.grid);
|
|
const tickAndPadding = tl + padding;
|
|
const hTickAndPadding = mirror ? -padding : tickAndPadding;
|
|
const rotation = -toRadians(this.labelRotation);
|
|
const items = [];
|
|
let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;
|
|
let textBaseline = 'middle';
|
|
if (position === 'top') {
|
|
y = this.bottom - hTickAndPadding;
|
|
textAlign = this._getXAxisLabelAlignment();
|
|
} else if (position === 'bottom') {
|
|
y = this.top + hTickAndPadding;
|
|
textAlign = this._getXAxisLabelAlignment();
|
|
} else if (position === 'left') {
|
|
const ret = this._getYAxisLabelAlignment(tl);
|
|
textAlign = ret.textAlign;
|
|
x = ret.x;
|
|
} else if (position === 'right') {
|
|
const ret = this._getYAxisLabelAlignment(tl);
|
|
textAlign = ret.textAlign;
|
|
x = ret.x;
|
|
} else if (axis === 'x') {
|
|
if (position === 'center') {
|
|
y = (chartArea.top + chartArea.bottom) / 2 + tickAndPadding;
|
|
} else if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
y = this.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding;
|
|
}
|
|
textAlign = this._getXAxisLabelAlignment();
|
|
} else if (axis === 'y') {
|
|
if (position === 'center') {
|
|
x = (chartArea.left + chartArea.right) / 2 - tickAndPadding;
|
|
} else if (isObject(position)) {
|
|
const positionAxisID = Object.keys(position)[0];
|
|
const value = position[positionAxisID];
|
|
x = this.chart.scales[positionAxisID].getPixelForValue(value);
|
|
}
|
|
textAlign = this._getYAxisLabelAlignment(tl).textAlign;
|
|
}
|
|
if (axis === 'y') {
|
|
if (align === 'start') {
|
|
textBaseline = 'top';
|
|
} else if (align === 'end') {
|
|
textBaseline = 'bottom';
|
|
}
|
|
}
|
|
const labelSizes = this._getLabelSizes();
|
|
for(i = 0, ilen = ticks.length; i < ilen; ++i){
|
|
tick = ticks[i];
|
|
label = tick.label;
|
|
const optsAtIndex = optionTicks.setContext(this.getContext(i));
|
|
pixel = this.getPixelForTick(i) + optionTicks.labelOffset;
|
|
font = this._resolveTickFontOptions(i);
|
|
lineHeight = font.lineHeight;
|
|
lineCount = isArray(label) ? label.length : 1;
|
|
const halfCount = lineCount / 2;
|
|
const color = optsAtIndex.color;
|
|
const strokeColor = optsAtIndex.textStrokeColor;
|
|
const strokeWidth = optsAtIndex.textStrokeWidth;
|
|
let tickTextAlign = textAlign;
|
|
if (isHorizontal) {
|
|
x = pixel;
|
|
if (textAlign === 'inner') {
|
|
if (i === ilen - 1) {
|
|
tickTextAlign = !this.options.reverse ? 'right' : 'left';
|
|
} else if (i === 0) {
|
|
tickTextAlign = !this.options.reverse ? 'left' : 'right';
|
|
} else {
|
|
tickTextAlign = 'center';
|
|
}
|
|
}
|
|
if (position === 'top') {
|
|
if (crossAlign === 'near' || rotation !== 0) {
|
|
textOffset = -lineCount * lineHeight + lineHeight / 2;
|
|
} else if (crossAlign === 'center') {
|
|
textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight;
|
|
} else {
|
|
textOffset = -labelSizes.highest.height + lineHeight / 2;
|
|
}
|
|
} else {
|
|
if (crossAlign === 'near' || rotation !== 0) {
|
|
textOffset = lineHeight / 2;
|
|
} else if (crossAlign === 'center') {
|
|
textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight;
|
|
} else {
|
|
textOffset = labelSizes.highest.height - lineCount * lineHeight;
|
|
}
|
|
}
|
|
if (mirror) {
|
|
textOffset *= -1;
|
|
}
|
|
if (rotation !== 0 && !optsAtIndex.showLabelBackdrop) {
|
|
x += lineHeight / 2 * Math.sin(rotation);
|
|
}
|
|
} else {
|
|
y = pixel;
|
|
textOffset = (1 - lineCount) * lineHeight / 2;
|
|
}
|
|
let backdrop;
|
|
if (optsAtIndex.showLabelBackdrop) {
|
|
const labelPadding = toPadding(optsAtIndex.backdropPadding);
|
|
const height = labelSizes.heights[i];
|
|
const width = labelSizes.widths[i];
|
|
let top = textOffset - labelPadding.top;
|
|
let left = 0 - labelPadding.left;
|
|
switch(textBaseline){
|
|
case 'middle':
|
|
top -= height / 2;
|
|
break;
|
|
case 'bottom':
|
|
top -= height;
|
|
break;
|
|
}
|
|
switch(textAlign){
|
|
case 'center':
|
|
left -= width / 2;
|
|
break;
|
|
case 'right':
|
|
left -= width;
|
|
break;
|
|
case 'inner':
|
|
if (i === ilen - 1) {
|
|
left -= width;
|
|
} else if (i > 0) {
|
|
left -= width / 2;
|
|
}
|
|
break;
|
|
}
|
|
backdrop = {
|
|
left,
|
|
top,
|
|
width: width + labelPadding.width,
|
|
height: height + labelPadding.height,
|
|
color: optsAtIndex.backdropColor
|
|
};
|
|
}
|
|
items.push({
|
|
label,
|
|
font,
|
|
textOffset,
|
|
options: {
|
|
rotation,
|
|
color,
|
|
strokeColor,
|
|
strokeWidth,
|
|
textAlign: tickTextAlign,
|
|
textBaseline,
|
|
translation: [
|
|
x,
|
|
y
|
|
],
|
|
backdrop
|
|
}
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
_getXAxisLabelAlignment() {
|
|
const { position , ticks } = this.options;
|
|
const rotation = -toRadians(this.labelRotation);
|
|
if (rotation) {
|
|
return position === 'top' ? 'left' : 'right';
|
|
}
|
|
let align = 'center';
|
|
if (ticks.align === 'start') {
|
|
align = 'left';
|
|
} else if (ticks.align === 'end') {
|
|
align = 'right';
|
|
} else if (ticks.align === 'inner') {
|
|
align = 'inner';
|
|
}
|
|
return align;
|
|
}
|
|
_getYAxisLabelAlignment(tl) {
|
|
const { position , ticks: { crossAlign , mirror , padding } } = this.options;
|
|
const labelSizes = this._getLabelSizes();
|
|
const tickAndPadding = tl + padding;
|
|
const widest = labelSizes.widest.width;
|
|
let textAlign;
|
|
let x;
|
|
if (position === 'left') {
|
|
if (mirror) {
|
|
x = this.right + padding;
|
|
if (crossAlign === 'near') {
|
|
textAlign = 'left';
|
|
} else if (crossAlign === 'center') {
|
|
textAlign = 'center';
|
|
x += widest / 2;
|
|
} else {
|
|
textAlign = 'right';
|
|
x += widest;
|
|
}
|
|
} else {
|
|
x = this.right - tickAndPadding;
|
|
if (crossAlign === 'near') {
|
|
textAlign = 'right';
|
|
} else if (crossAlign === 'center') {
|
|
textAlign = 'center';
|
|
x -= widest / 2;
|
|
} else {
|
|
textAlign = 'left';
|
|
x = this.left;
|
|
}
|
|
}
|
|
} else if (position === 'right') {
|
|
if (mirror) {
|
|
x = this.left + padding;
|
|
if (crossAlign === 'near') {
|
|
textAlign = 'right';
|
|
} else if (crossAlign === 'center') {
|
|
textAlign = 'center';
|
|
x -= widest / 2;
|
|
} else {
|
|
textAlign = 'left';
|
|
x -= widest;
|
|
}
|
|
} else {
|
|
x = this.left + tickAndPadding;
|
|
if (crossAlign === 'near') {
|
|
textAlign = 'left';
|
|
} else if (crossAlign === 'center') {
|
|
textAlign = 'center';
|
|
x += widest / 2;
|
|
} else {
|
|
textAlign = 'right';
|
|
x = this.right;
|
|
}
|
|
}
|
|
} else {
|
|
textAlign = 'right';
|
|
}
|
|
return {
|
|
textAlign,
|
|
x
|
|
};
|
|
}
|
|
_computeLabelArea() {
|
|
if (this.options.ticks.mirror) {
|
|
return;
|
|
}
|
|
const chart = this.chart;
|
|
const position = this.options.position;
|
|
if (position === 'left' || position === 'right') {
|
|
return {
|
|
top: 0,
|
|
left: this.left,
|
|
bottom: chart.height,
|
|
right: this.right
|
|
};
|
|
}
|
|
if (position === 'top' || position === 'bottom') {
|
|
return {
|
|
top: this.top,
|
|
left: 0,
|
|
bottom: this.bottom,
|
|
right: chart.width
|
|
};
|
|
}
|
|
}
|
|
drawBackground() {
|
|
const { ctx , options: { backgroundColor } , left , top , width , height } = this;
|
|
if (backgroundColor) {
|
|
ctx.save();
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fillRect(left, top, width, height);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
getLineWidthForValue(value) {
|
|
const grid = this.options.grid;
|
|
if (!this._isVisible() || !grid.display) {
|
|
return 0;
|
|
}
|
|
const ticks = this.ticks;
|
|
const index = ticks.findIndex((t)=>t.value === value);
|
|
if (index >= 0) {
|
|
const opts = grid.setContext(this.getContext(index));
|
|
return opts.lineWidth;
|
|
}
|
|
return 0;
|
|
}
|
|
drawGrid(chartArea) {
|
|
const grid = this.options.grid;
|
|
const ctx = this.ctx;
|
|
const items = this._gridLineItems || (this._gridLineItems = this._computeGridLineItems(chartArea));
|
|
let i, ilen;
|
|
const drawLine = (p1, p2, style)=>{
|
|
if (!style.width || !style.color) {
|
|
return;
|
|
}
|
|
ctx.save();
|
|
ctx.lineWidth = style.width;
|
|
ctx.strokeStyle = style.color;
|
|
ctx.setLineDash(style.borderDash || []);
|
|
ctx.lineDashOffset = style.borderDashOffset;
|
|
ctx.beginPath();
|
|
ctx.moveTo(p1.x, p1.y);
|
|
ctx.lineTo(p2.x, p2.y);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
};
|
|
if (grid.display) {
|
|
for(i = 0, ilen = items.length; i < ilen; ++i){
|
|
const item = items[i];
|
|
if (grid.drawOnChartArea) {
|
|
drawLine({
|
|
x: item.x1,
|
|
y: item.y1
|
|
}, {
|
|
x: item.x2,
|
|
y: item.y2
|
|
}, item);
|
|
}
|
|
if (grid.drawTicks) {
|
|
drawLine({
|
|
x: item.tx1,
|
|
y: item.ty1
|
|
}, {
|
|
x: item.tx2,
|
|
y: item.ty2
|
|
}, {
|
|
color: item.tickColor,
|
|
width: item.tickWidth,
|
|
borderDash: item.tickBorderDash,
|
|
borderDashOffset: item.tickBorderDashOffset
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
drawBorder() {
|
|
const { chart , ctx , options: { border , grid } } = this;
|
|
const borderOpts = border.setContext(this.getContext());
|
|
const axisWidth = border.display ? borderOpts.width : 0;
|
|
if (!axisWidth) {
|
|
return;
|
|
}
|
|
const lastLineWidth = grid.setContext(this.getContext(0)).lineWidth;
|
|
const borderValue = this._borderValue;
|
|
let x1, x2, y1, y2;
|
|
if (this.isHorizontal()) {
|
|
x1 = _alignPixel(chart, this.left, axisWidth) - axisWidth / 2;
|
|
x2 = _alignPixel(chart, this.right, lastLineWidth) + lastLineWidth / 2;
|
|
y1 = y2 = borderValue;
|
|
} else {
|
|
y1 = _alignPixel(chart, this.top, axisWidth) - axisWidth / 2;
|
|
y2 = _alignPixel(chart, this.bottom, lastLineWidth) + lastLineWidth / 2;
|
|
x1 = x2 = borderValue;
|
|
}
|
|
ctx.save();
|
|
ctx.lineWidth = borderOpts.width;
|
|
ctx.strokeStyle = borderOpts.color;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
drawLabels(chartArea) {
|
|
const optionTicks = this.options.ticks;
|
|
if (!optionTicks.display) {
|
|
return;
|
|
}
|
|
const ctx = this.ctx;
|
|
const area = this._computeLabelArea();
|
|
if (area) {
|
|
clipArea(ctx, area);
|
|
}
|
|
const items = this.getLabelItems(chartArea);
|
|
for (const item of items){
|
|
const renderTextOptions = item.options;
|
|
const tickFont = item.font;
|
|
const label = item.label;
|
|
const y = item.textOffset;
|
|
renderText(ctx, label, 0, y, tickFont, renderTextOptions);
|
|
}
|
|
if (area) {
|
|
unclipArea(ctx);
|
|
}
|
|
}
|
|
drawTitle() {
|
|
const { ctx , options: { position , title , reverse } } = this;
|
|
if (!title.display) {
|
|
return;
|
|
}
|
|
const font = toFont(title.font);
|
|
const padding = toPadding(title.padding);
|
|
const align = title.align;
|
|
let offset = font.lineHeight / 2;
|
|
if (position === 'bottom' || position === 'center' || isObject(position)) {
|
|
offset += padding.bottom;
|
|
if (isArray(title.text)) {
|
|
offset += font.lineHeight * (title.text.length - 1);
|
|
}
|
|
} else {
|
|
offset += padding.top;
|
|
}
|
|
const { titleX , titleY , maxWidth , rotation } = titleArgs(this, offset, position, align);
|
|
renderText(ctx, title.text, 0, 0, font, {
|
|
color: title.color,
|
|
maxWidth,
|
|
rotation,
|
|
textAlign: titleAlign(align, position, reverse),
|
|
textBaseline: 'middle',
|
|
translation: [
|
|
titleX,
|
|
titleY
|
|
]
|
|
});
|
|
}
|
|
draw(chartArea) {
|
|
if (!this._isVisible()) {
|
|
return;
|
|
}
|
|
this.drawBackground();
|
|
this.drawGrid(chartArea);
|
|
this.drawBorder();
|
|
this.drawTitle();
|
|
this.drawLabels(chartArea);
|
|
}
|
|
_layers() {
|
|
const opts = this.options;
|
|
const tz = opts.ticks && opts.ticks.z || 0;
|
|
const gz = valueOrDefault(opts.grid && opts.grid.z, -1);
|
|
const bz = valueOrDefault(opts.border && opts.border.z, 0);
|
|
if (!this._isVisible() || this.draw !== Scale.prototype.draw) {
|
|
return [
|
|
{
|
|
z: tz,
|
|
draw: (chartArea)=>{
|
|
this.draw(chartArea);
|
|
}
|
|
}
|
|
];
|
|
}
|
|
return [
|
|
{
|
|
z: gz,
|
|
draw: (chartArea)=>{
|
|
this.drawBackground();
|
|
this.drawGrid(chartArea);
|
|
this.drawTitle();
|
|
}
|
|
},
|
|
{
|
|
z: bz,
|
|
draw: ()=>{
|
|
this.drawBorder();
|
|
}
|
|
},
|
|
{
|
|
z: tz,
|
|
draw: (chartArea)=>{
|
|
this.drawLabels(chartArea);
|
|
}
|
|
}
|
|
];
|
|
}
|
|
getMatchingVisibleMetas(type) {
|
|
const metas = this.chart.getSortedVisibleDatasetMetas();
|
|
const axisID = this.axis + 'AxisID';
|
|
const result = [];
|
|
let i, ilen;
|
|
for(i = 0, ilen = metas.length; i < ilen; ++i){
|
|
const meta = metas[i];
|
|
if (meta[axisID] === this.id && (!type || meta.type === type)) {
|
|
result.push(meta);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
_resolveTickFontOptions(index) {
|
|
const opts = this.options.ticks.setContext(this.getContext(index));
|
|
return toFont(opts.font);
|
|
}
|
|
_maxDigits() {
|
|
const fontSize = this._resolveTickFontOptions(0).lineHeight;
|
|
return (this.isHorizontal() ? this.width : this.height) / fontSize;
|
|
}
|
|
}
|
|
|
|
class TypedRegistry {
|
|
constructor(type, scope, override){
|
|
this.type = type;
|
|
this.scope = scope;
|
|
this.override = override;
|
|
this.items = Object.create(null);
|
|
}
|
|
isForType(type) {
|
|
return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype);
|
|
}
|
|
register(item) {
|
|
const proto = Object.getPrototypeOf(item);
|
|
let parentScope;
|
|
if (isIChartComponent(proto)) {
|
|
parentScope = this.register(proto);
|
|
}
|
|
const items = this.items;
|
|
const id = item.id;
|
|
const scope = this.scope + '.' + id;
|
|
if (!id) {
|
|
throw new Error('class does not have id: ' + item);
|
|
}
|
|
if (id in items) {
|
|
return scope;
|
|
}
|
|
items[id] = item;
|
|
registerDefaults(item, scope, parentScope);
|
|
if (this.override) {
|
|
defaults.override(item.id, item.overrides);
|
|
}
|
|
return scope;
|
|
}
|
|
get(id) {
|
|
return this.items[id];
|
|
}
|
|
unregister(item) {
|
|
const items = this.items;
|
|
const id = item.id;
|
|
const scope = this.scope;
|
|
if (id in items) {
|
|
delete items[id];
|
|
}
|
|
if (scope && id in defaults[scope]) {
|
|
delete defaults[scope][id];
|
|
if (this.override) {
|
|
delete overrides[id];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function registerDefaults(item, scope, parentScope) {
|
|
const itemDefaults = merge(Object.create(null), [
|
|
parentScope ? defaults.get(parentScope) : {},
|
|
defaults.get(scope),
|
|
item.defaults
|
|
]);
|
|
defaults.set(scope, itemDefaults);
|
|
if (item.defaultRoutes) {
|
|
routeDefaults(scope, item.defaultRoutes);
|
|
}
|
|
if (item.descriptors) {
|
|
defaults.describe(scope, item.descriptors);
|
|
}
|
|
}
|
|
function routeDefaults(scope, routes) {
|
|
Object.keys(routes).forEach((property)=>{
|
|
const propertyParts = property.split('.');
|
|
const sourceName = propertyParts.pop();
|
|
const sourceScope = [
|
|
scope
|
|
].concat(propertyParts).join('.');
|
|
const parts = routes[property].split('.');
|
|
const targetName = parts.pop();
|
|
const targetScope = parts.join('.');
|
|
defaults.route(sourceScope, sourceName, targetScope, targetName);
|
|
});
|
|
}
|
|
function isIChartComponent(proto) {
|
|
return 'id' in proto && 'defaults' in proto;
|
|
}
|
|
|
|
class Registry {
|
|
constructor(){
|
|
this.controllers = new TypedRegistry(DatasetController, 'datasets', true);
|
|
this.elements = new TypedRegistry(chart_Element, 'elements');
|
|
this.plugins = new TypedRegistry(Object, 'plugins');
|
|
this.scales = new TypedRegistry(Scale, 'scales');
|
|
this._typedRegistries = [
|
|
this.controllers,
|
|
this.scales,
|
|
this.elements
|
|
];
|
|
}
|
|
add(...args) {
|
|
this._each('register', args);
|
|
}
|
|
remove(...args) {
|
|
this._each('unregister', args);
|
|
}
|
|
addControllers(...args) {
|
|
this._each('register', args, this.controllers);
|
|
}
|
|
addElements(...args) {
|
|
this._each('register', args, this.elements);
|
|
}
|
|
addPlugins(...args) {
|
|
this._each('register', args, this.plugins);
|
|
}
|
|
addScales(...args) {
|
|
this._each('register', args, this.scales);
|
|
}
|
|
getController(id) {
|
|
return this._get(id, this.controllers, 'controller');
|
|
}
|
|
getElement(id) {
|
|
return this._get(id, this.elements, 'element');
|
|
}
|
|
getPlugin(id) {
|
|
return this._get(id, this.plugins, 'plugin');
|
|
}
|
|
getScale(id) {
|
|
return this._get(id, this.scales, 'scale');
|
|
}
|
|
removeControllers(...args) {
|
|
this._each('unregister', args, this.controllers);
|
|
}
|
|
removeElements(...args) {
|
|
this._each('unregister', args, this.elements);
|
|
}
|
|
removePlugins(...args) {
|
|
this._each('unregister', args, this.plugins);
|
|
}
|
|
removeScales(...args) {
|
|
this._each('unregister', args, this.scales);
|
|
}
|
|
_each(method, args, typedRegistry) {
|
|
[
|
|
...args
|
|
].forEach((arg)=>{
|
|
const reg = typedRegistry || this._getRegistryForType(arg);
|
|
if (typedRegistry || reg.isForType(arg) || reg === this.plugins && arg.id) {
|
|
this._exec(method, reg, arg);
|
|
} else {
|
|
each(arg, (item)=>{
|
|
const itemReg = typedRegistry || this._getRegistryForType(item);
|
|
this._exec(method, itemReg, item);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
_exec(method, registry, component) {
|
|
const camelMethod = _capitalize(method);
|
|
callback(component['before' + camelMethod], [], component);
|
|
registry[method](component);
|
|
callback(component['after' + camelMethod], [], component);
|
|
}
|
|
_getRegistryForType(type) {
|
|
for(let i = 0; i < this._typedRegistries.length; i++){
|
|
const reg = this._typedRegistries[i];
|
|
if (reg.isForType(type)) {
|
|
return reg;
|
|
}
|
|
}
|
|
return this.plugins;
|
|
}
|
|
_get(id, typedRegistry, type) {
|
|
const item = typedRegistry.get(id);
|
|
if (item === undefined) {
|
|
throw new Error('"' + id + '" is not a registered ' + type + '.');
|
|
}
|
|
return item;
|
|
}
|
|
}
|
|
var registry = /* #__PURE__ */ new Registry();
|
|
|
|
class PluginService {
|
|
constructor(){
|
|
this._init = undefined;
|
|
}
|
|
notify(chart, hook, args, filter) {
|
|
if (hook === 'beforeInit') {
|
|
this._init = this._createDescriptors(chart, true);
|
|
this._notify(this._init, chart, 'install');
|
|
}
|
|
if (this._init === undefined) {
|
|
return;
|
|
}
|
|
const descriptors = filter ? this._descriptors(chart).filter(filter) : this._descriptors(chart);
|
|
const result = this._notify(descriptors, chart, hook, args);
|
|
if (hook === 'afterDestroy') {
|
|
this._notify(descriptors, chart, 'stop');
|
|
this._notify(this._init, chart, 'uninstall');
|
|
this._init = undefined;
|
|
}
|
|
return result;
|
|
}
|
|
_notify(descriptors, chart, hook, args) {
|
|
args = args || {};
|
|
for (const descriptor of descriptors){
|
|
const plugin = descriptor.plugin;
|
|
const method = plugin[hook];
|
|
const params = [
|
|
chart,
|
|
args,
|
|
descriptor.options
|
|
];
|
|
if (callback(method, params, plugin) === false && args.cancelable) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
invalidate() {
|
|
if (!isNullOrUndef(this._cache)) {
|
|
this._oldCache = this._cache;
|
|
this._cache = undefined;
|
|
}
|
|
}
|
|
_descriptors(chart) {
|
|
if (this._cache) {
|
|
return this._cache;
|
|
}
|
|
const descriptors = this._cache = this._createDescriptors(chart);
|
|
this._notifyStateChanges(chart);
|
|
return descriptors;
|
|
}
|
|
_createDescriptors(chart, all) {
|
|
const config = chart && chart.config;
|
|
const options = valueOrDefault(config.options && config.options.plugins, {});
|
|
const plugins = allPlugins(config);
|
|
return options === false && !all ? [] : createDescriptors(chart, plugins, options, all);
|
|
}
|
|
_notifyStateChanges(chart) {
|
|
const previousDescriptors = this._oldCache || [];
|
|
const descriptors = this._cache;
|
|
const diff = (a, b)=>a.filter((x)=>!b.some((y)=>x.plugin.id === y.plugin.id));
|
|
this._notify(diff(previousDescriptors, descriptors), chart, 'stop');
|
|
this._notify(diff(descriptors, previousDescriptors), chart, 'start');
|
|
}
|
|
}
|
|
function allPlugins(config) {
|
|
const localIds = {};
|
|
const plugins = [];
|
|
const keys = Object.keys(registry.plugins.items);
|
|
for(let i = 0; i < keys.length; i++){
|
|
plugins.push(registry.getPlugin(keys[i]));
|
|
}
|
|
const local = config.plugins || [];
|
|
for(let i = 0; i < local.length; i++){
|
|
const plugin = local[i];
|
|
if (plugins.indexOf(plugin) === -1) {
|
|
plugins.push(plugin);
|
|
localIds[plugin.id] = true;
|
|
}
|
|
}
|
|
return {
|
|
plugins,
|
|
localIds
|
|
};
|
|
}
|
|
function getOpts(options, all) {
|
|
if (!all && options === false) {
|
|
return null;
|
|
}
|
|
if (options === true) {
|
|
return {};
|
|
}
|
|
return options;
|
|
}
|
|
function createDescriptors(chart, { plugins , localIds }, options, all) {
|
|
const result = [];
|
|
const context = chart.getContext();
|
|
for (const plugin of plugins){
|
|
const id = plugin.id;
|
|
const opts = getOpts(options[id], all);
|
|
if (opts === null) {
|
|
continue;
|
|
}
|
|
result.push({
|
|
plugin,
|
|
options: pluginOpts(chart.config, {
|
|
plugin,
|
|
local: localIds[id]
|
|
}, opts, context)
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
function pluginOpts(config, { plugin , local }, opts, context) {
|
|
const keys = config.pluginScopeKeys(plugin);
|
|
const scopes = config.getOptionScopes(opts, keys);
|
|
if (local && plugin.defaults) {
|
|
scopes.push(plugin.defaults);
|
|
}
|
|
return config.createResolver(scopes, context, [
|
|
''
|
|
], {
|
|
scriptable: false,
|
|
indexable: false,
|
|
allKeys: true
|
|
});
|
|
}
|
|
|
|
function getIndexAxis(type, options) {
|
|
const datasetDefaults = defaults.datasets[type] || {};
|
|
const datasetOptions = (options.datasets || {})[type] || {};
|
|
return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x';
|
|
}
|
|
function getAxisFromDefaultScaleID(id, indexAxis) {
|
|
let axis = id;
|
|
if (id === '_index_') {
|
|
axis = indexAxis;
|
|
} else if (id === '_value_') {
|
|
axis = indexAxis === 'x' ? 'y' : 'x';
|
|
}
|
|
return axis;
|
|
}
|
|
function getDefaultScaleIDFromAxis(axis, indexAxis) {
|
|
return axis === indexAxis ? '_index_' : '_value_';
|
|
}
|
|
function idMatchesAxis(id) {
|
|
if (id === 'x' || id === 'y' || id === 'r') {
|
|
return id;
|
|
}
|
|
}
|
|
function axisFromPosition(position) {
|
|
if (position === 'top' || position === 'bottom') {
|
|
return 'x';
|
|
}
|
|
if (position === 'left' || position === 'right') {
|
|
return 'y';
|
|
}
|
|
}
|
|
function determineAxis(id, ...scaleOptions) {
|
|
if (idMatchesAxis(id)) {
|
|
return id;
|
|
}
|
|
for (const opts of scaleOptions){
|
|
const axis = opts.axis || axisFromPosition(opts.position) || id.length > 1 && idMatchesAxis(id[0].toLowerCase());
|
|
if (axis) {
|
|
return axis;
|
|
}
|
|
}
|
|
throw new Error(`Cannot determine type of '${id}' axis. Please provide 'axis' or 'position' option.`);
|
|
}
|
|
function getAxisFromDataset(id, axis, dataset) {
|
|
if (dataset[axis + 'AxisID'] === id) {
|
|
return {
|
|
axis
|
|
};
|
|
}
|
|
}
|
|
function retrieveAxisFromDatasets(id, config) {
|
|
if (config.data && config.data.datasets) {
|
|
const boundDs = config.data.datasets.filter((d)=>d.xAxisID === id || d.yAxisID === id);
|
|
if (boundDs.length) {
|
|
return getAxisFromDataset(id, 'x', boundDs[0]) || getAxisFromDataset(id, 'y', boundDs[0]);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
function mergeScaleConfig(config, options) {
|
|
const chartDefaults = overrides[config.type] || {
|
|
scales: {}
|
|
};
|
|
const configScales = options.scales || {};
|
|
const chartIndexAxis = getIndexAxis(config.type, options);
|
|
const scales = Object.create(null);
|
|
Object.keys(configScales).forEach((id)=>{
|
|
const scaleConf = configScales[id];
|
|
if (!isObject(scaleConf)) {
|
|
return console.error(`Invalid scale configuration for scale: ${id}`);
|
|
}
|
|
if (scaleConf._proxy) {
|
|
return console.warn(`Ignoring resolver passed as options for scale: ${id}`);
|
|
}
|
|
const axis = determineAxis(id, scaleConf, retrieveAxisFromDatasets(id, config), defaults.scales[scaleConf.type]);
|
|
const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);
|
|
const defaultScaleOptions = chartDefaults.scales || {};
|
|
scales[id] = mergeIf(Object.create(null), [
|
|
{
|
|
axis
|
|
},
|
|
scaleConf,
|
|
defaultScaleOptions[axis],
|
|
defaultScaleOptions[defaultId]
|
|
]);
|
|
});
|
|
config.data.datasets.forEach((dataset)=>{
|
|
const type = dataset.type || config.type;
|
|
const indexAxis = dataset.indexAxis || getIndexAxis(type, options);
|
|
const datasetDefaults = overrides[type] || {};
|
|
const defaultScaleOptions = datasetDefaults.scales || {};
|
|
Object.keys(defaultScaleOptions).forEach((defaultID)=>{
|
|
const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);
|
|
const id = dataset[axis + 'AxisID'] || axis;
|
|
scales[id] = scales[id] || Object.create(null);
|
|
mergeIf(scales[id], [
|
|
{
|
|
axis
|
|
},
|
|
configScales[id],
|
|
defaultScaleOptions[defaultID]
|
|
]);
|
|
});
|
|
});
|
|
Object.keys(scales).forEach((key)=>{
|
|
const scale = scales[key];
|
|
mergeIf(scale, [
|
|
defaults.scales[scale.type],
|
|
defaults.scale
|
|
]);
|
|
});
|
|
return scales;
|
|
}
|
|
function initOptions(config) {
|
|
const options = config.options || (config.options = {});
|
|
options.plugins = valueOrDefault(options.plugins, {});
|
|
options.scales = mergeScaleConfig(config, options);
|
|
}
|
|
function initData(data) {
|
|
data = data || {};
|
|
data.datasets = data.datasets || [];
|
|
data.labels = data.labels || [];
|
|
return data;
|
|
}
|
|
function initConfig(config) {
|
|
config = config || {};
|
|
config.data = initData(config.data);
|
|
initOptions(config);
|
|
return config;
|
|
}
|
|
const keyCache = new Map();
|
|
const keysCached = new Set();
|
|
function cachedKeys(cacheKey, generate) {
|
|
let keys = keyCache.get(cacheKey);
|
|
if (!keys) {
|
|
keys = generate();
|
|
keyCache.set(cacheKey, keys);
|
|
keysCached.add(keys);
|
|
}
|
|
return keys;
|
|
}
|
|
const addIfFound = (set, obj, key)=>{
|
|
const opts = resolveObjectKey(obj, key);
|
|
if (opts !== undefined) {
|
|
set.add(opts);
|
|
}
|
|
};
|
|
class Config {
|
|
constructor(config){
|
|
this._config = initConfig(config);
|
|
this._scopeCache = new Map();
|
|
this._resolverCache = new Map();
|
|
}
|
|
get platform() {
|
|
return this._config.platform;
|
|
}
|
|
get type() {
|
|
return this._config.type;
|
|
}
|
|
set type(type) {
|
|
this._config.type = type;
|
|
}
|
|
get data() {
|
|
return this._config.data;
|
|
}
|
|
set data(data) {
|
|
this._config.data = initData(data);
|
|
}
|
|
get options() {
|
|
return this._config.options;
|
|
}
|
|
set options(options) {
|
|
this._config.options = options;
|
|
}
|
|
get plugins() {
|
|
return this._config.plugins;
|
|
}
|
|
update() {
|
|
const config = this._config;
|
|
this.clearCache();
|
|
initOptions(config);
|
|
}
|
|
clearCache() {
|
|
this._scopeCache.clear();
|
|
this._resolverCache.clear();
|
|
}
|
|
datasetScopeKeys(datasetType) {
|
|
return cachedKeys(datasetType, ()=>[
|
|
[
|
|
`datasets.${datasetType}`,
|
|
''
|
|
]
|
|
]);
|
|
}
|
|
datasetAnimationScopeKeys(datasetType, transition) {
|
|
return cachedKeys(`${datasetType}.transition.${transition}`, ()=>[
|
|
[
|
|
`datasets.${datasetType}.transitions.${transition}`,
|
|
`transitions.${transition}`
|
|
],
|
|
[
|
|
`datasets.${datasetType}`,
|
|
''
|
|
]
|
|
]);
|
|
}
|
|
datasetElementScopeKeys(datasetType, elementType) {
|
|
return cachedKeys(`${datasetType}-${elementType}`, ()=>[
|
|
[
|
|
`datasets.${datasetType}.elements.${elementType}`,
|
|
`datasets.${datasetType}`,
|
|
`elements.${elementType}`,
|
|
''
|
|
]
|
|
]);
|
|
}
|
|
pluginScopeKeys(plugin) {
|
|
const id = plugin.id;
|
|
const type = this.type;
|
|
return cachedKeys(`${type}-plugin-${id}`, ()=>[
|
|
[
|
|
`plugins.${id}`,
|
|
...plugin.additionalOptionScopes || []
|
|
]
|
|
]);
|
|
}
|
|
_cachedScopes(mainScope, resetCache) {
|
|
const _scopeCache = this._scopeCache;
|
|
let cache = _scopeCache.get(mainScope);
|
|
if (!cache || resetCache) {
|
|
cache = new Map();
|
|
_scopeCache.set(mainScope, cache);
|
|
}
|
|
return cache;
|
|
}
|
|
getOptionScopes(mainScope, keyLists, resetCache) {
|
|
const { options , type } = this;
|
|
const cache = this._cachedScopes(mainScope, resetCache);
|
|
const cached = cache.get(keyLists);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const scopes = new Set();
|
|
keyLists.forEach((keys)=>{
|
|
if (mainScope) {
|
|
scopes.add(mainScope);
|
|
keys.forEach((key)=>addIfFound(scopes, mainScope, key));
|
|
}
|
|
keys.forEach((key)=>addIfFound(scopes, options, key));
|
|
keys.forEach((key)=>addIfFound(scopes, overrides[type] || {}, key));
|
|
keys.forEach((key)=>addIfFound(scopes, defaults, key));
|
|
keys.forEach((key)=>addIfFound(scopes, descriptors, key));
|
|
});
|
|
const array = Array.from(scopes);
|
|
if (array.length === 0) {
|
|
array.push(Object.create(null));
|
|
}
|
|
if (keysCached.has(keyLists)) {
|
|
cache.set(keyLists, array);
|
|
}
|
|
return array;
|
|
}
|
|
chartOptionScopes() {
|
|
const { options , type } = this;
|
|
return [
|
|
options,
|
|
overrides[type] || {},
|
|
defaults.datasets[type] || {},
|
|
{
|
|
type
|
|
},
|
|
defaults,
|
|
descriptors
|
|
];
|
|
}
|
|
resolveNamedOptions(scopes, names, context, prefixes = [
|
|
''
|
|
]) {
|
|
const result = {
|
|
$shared: true
|
|
};
|
|
const { resolver , subPrefixes } = getResolver(this._resolverCache, scopes, prefixes);
|
|
let options = resolver;
|
|
if (needContext(resolver, names)) {
|
|
result.$shared = false;
|
|
context = isFunction(context) ? context() : context;
|
|
const subResolver = this.createResolver(scopes, context, subPrefixes);
|
|
options = _attachContext(resolver, context, subResolver);
|
|
}
|
|
for (const prop of names){
|
|
result[prop] = options[prop];
|
|
}
|
|
return result;
|
|
}
|
|
createResolver(scopes, context, prefixes = [
|
|
''
|
|
], descriptorDefaults) {
|
|
const { resolver } = getResolver(this._resolverCache, scopes, prefixes);
|
|
return isObject(context) ? _attachContext(resolver, context, undefined, descriptorDefaults) : resolver;
|
|
}
|
|
}
|
|
function getResolver(resolverCache, scopes, prefixes) {
|
|
let cache = resolverCache.get(scopes);
|
|
if (!cache) {
|
|
cache = new Map();
|
|
resolverCache.set(scopes, cache);
|
|
}
|
|
const cacheKey = prefixes.join();
|
|
let cached = cache.get(cacheKey);
|
|
if (!cached) {
|
|
const resolver = _createResolver(scopes, prefixes);
|
|
cached = {
|
|
resolver,
|
|
subPrefixes: prefixes.filter((p)=>!p.toLowerCase().includes('hover'))
|
|
};
|
|
cache.set(cacheKey, cached);
|
|
}
|
|
return cached;
|
|
}
|
|
const hasFunction = (value)=>isObject(value) && Object.getOwnPropertyNames(value).some((key)=>isFunction(value[key]));
|
|
function needContext(proxy, names) {
|
|
const { isScriptable , isIndexable } = _descriptors(proxy);
|
|
for (const prop of names){
|
|
const scriptable = isScriptable(prop);
|
|
const indexable = isIndexable(prop);
|
|
const value = (indexable || scriptable) && proxy[prop];
|
|
if (scriptable && (isFunction(value) || hasFunction(value)) || indexable && isArray(value)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var version = "4.5.1";
|
|
|
|
const KNOWN_POSITIONS = [
|
|
'top',
|
|
'bottom',
|
|
'left',
|
|
'right',
|
|
'chartArea'
|
|
];
|
|
function positionIsHorizontal(position, axis) {
|
|
return position === 'top' || position === 'bottom' || KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x';
|
|
}
|
|
function compare2Level(l1, l2) {
|
|
return function(a, b) {
|
|
return a[l1] === b[l1] ? a[l2] - b[l2] : a[l1] - b[l1];
|
|
};
|
|
}
|
|
function onAnimationsComplete(context) {
|
|
const chart = context.chart;
|
|
const animationOptions = chart.options.animation;
|
|
chart.notifyPlugins('afterRender');
|
|
callback(animationOptions && animationOptions.onComplete, [
|
|
context
|
|
], chart);
|
|
}
|
|
function onAnimationProgress(context) {
|
|
const chart = context.chart;
|
|
const animationOptions = chart.options.animation;
|
|
callback(animationOptions && animationOptions.onProgress, [
|
|
context
|
|
], chart);
|
|
}
|
|
function getCanvas(item) {
|
|
if (_isDomSupported() && typeof item === 'string') {
|
|
item = document.getElementById(item);
|
|
} else if (item && item.length) {
|
|
item = item[0];
|
|
}
|
|
if (item && item.canvas) {
|
|
item = item.canvas;
|
|
}
|
|
return item;
|
|
}
|
|
const instances = {};
|
|
const getChart = (key)=>{
|
|
const canvas = getCanvas(key);
|
|
return Object.values(instances).filter((c)=>c.canvas === canvas).pop();
|
|
};
|
|
function moveNumericKeys(obj, start, move) {
|
|
const keys = Object.keys(obj);
|
|
for (const key of keys){
|
|
const intKey = +key;
|
|
if (intKey >= start) {
|
|
const value = obj[key];
|
|
delete obj[key];
|
|
if (move > 0 || intKey > start) {
|
|
obj[intKey + move] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function determineLastEvent(e, lastEvent, inChartArea, isClick) {
|
|
if (!inChartArea || e.type === 'mouseout') {
|
|
return null;
|
|
}
|
|
if (isClick) {
|
|
return lastEvent;
|
|
}
|
|
return e;
|
|
}
|
|
class chart_Chart {
|
|
static defaults = defaults;
|
|
static instances = instances;
|
|
static overrides = overrides;
|
|
static registry = registry;
|
|
static version = version;
|
|
static getChart = getChart;
|
|
static register(...items) {
|
|
registry.add(...items);
|
|
invalidatePlugins();
|
|
}
|
|
static unregister(...items) {
|
|
registry.remove(...items);
|
|
invalidatePlugins();
|
|
}
|
|
constructor(item, userConfig){
|
|
const config = this.config = new Config(userConfig);
|
|
const initialCanvas = getCanvas(item);
|
|
const existingChart = getChart(initialCanvas);
|
|
if (existingChart) {
|
|
throw new Error('Canvas is already in use. Chart with ID \'' + existingChart.id + '\'' + ' must be destroyed before the canvas with ID \'' + existingChart.canvas.id + '\' can be reused.');
|
|
}
|
|
const options = config.createResolver(config.chartOptionScopes(), this.getContext());
|
|
this.platform = new (config.platform || _detectPlatform(initialCanvas))();
|
|
this.platform.updateConfig(config);
|
|
const context = this.platform.acquireContext(initialCanvas, options.aspectRatio);
|
|
const canvas = context && context.canvas;
|
|
const height = canvas && canvas.height;
|
|
const width = canvas && canvas.width;
|
|
this.id = uid();
|
|
this.ctx = context;
|
|
this.canvas = canvas;
|
|
this.width = width;
|
|
this.height = height;
|
|
this._options = options;
|
|
this._aspectRatio = this.aspectRatio;
|
|
this._layers = [];
|
|
this._metasets = [];
|
|
this._stacks = undefined;
|
|
this.boxes = [];
|
|
this.currentDevicePixelRatio = undefined;
|
|
this.chartArea = undefined;
|
|
this._active = [];
|
|
this._lastEvent = undefined;
|
|
this._listeners = {};
|
|
this._responsiveListeners = undefined;
|
|
this._sortedMetasets = [];
|
|
this.scales = {};
|
|
this._plugins = new PluginService();
|
|
this.$proxies = {};
|
|
this._hiddenIndices = {};
|
|
this.attached = false;
|
|
this._animationsDisabled = undefined;
|
|
this.$context = undefined;
|
|
this._doResize = debounce((mode)=>this.update(mode), options.resizeDelay || 0);
|
|
this._dataChanges = [];
|
|
instances[this.id] = this;
|
|
if (!context || !canvas) {
|
|
console.error("Failed to create chart: can't acquire context from the given item");
|
|
return;
|
|
}
|
|
animator.listen(this, 'complete', onAnimationsComplete);
|
|
animator.listen(this, 'progress', onAnimationProgress);
|
|
this._initialize();
|
|
if (this.attached) {
|
|
this.update();
|
|
}
|
|
}
|
|
get aspectRatio() {
|
|
const { options: { aspectRatio , maintainAspectRatio } , width , height , _aspectRatio } = this;
|
|
if (!isNullOrUndef(aspectRatio)) {
|
|
return aspectRatio;
|
|
}
|
|
if (maintainAspectRatio && _aspectRatio) {
|
|
return _aspectRatio;
|
|
}
|
|
return height ? width / height : null;
|
|
}
|
|
get data() {
|
|
return this.config.data;
|
|
}
|
|
set data(data) {
|
|
this.config.data = data;
|
|
}
|
|
get options() {
|
|
return this._options;
|
|
}
|
|
set options(options) {
|
|
this.config.options = options;
|
|
}
|
|
get registry() {
|
|
return registry;
|
|
}
|
|
_initialize() {
|
|
this.notifyPlugins('beforeInit');
|
|
if (this.options.responsive) {
|
|
this.resize();
|
|
} else {
|
|
retinaScale(this, this.options.devicePixelRatio);
|
|
}
|
|
this.bindEvents();
|
|
this.notifyPlugins('afterInit');
|
|
return this;
|
|
}
|
|
clear() {
|
|
clearCanvas(this.canvas, this.ctx);
|
|
return this;
|
|
}
|
|
stop() {
|
|
animator.stop(this);
|
|
return this;
|
|
}
|
|
resize(width, height) {
|
|
if (!animator.running(this)) {
|
|
this._resize(width, height);
|
|
} else {
|
|
this._resizeBeforeDraw = {
|
|
width,
|
|
height
|
|
};
|
|
}
|
|
}
|
|
_resize(width, height) {
|
|
const options = this.options;
|
|
const canvas = this.canvas;
|
|
const aspectRatio = options.maintainAspectRatio && this.aspectRatio;
|
|
const newSize = this.platform.getMaximumSize(canvas, width, height, aspectRatio);
|
|
const newRatio = options.devicePixelRatio || this.platform.getDevicePixelRatio();
|
|
const mode = this.width ? 'resize' : 'attach';
|
|
this.width = newSize.width;
|
|
this.height = newSize.height;
|
|
this._aspectRatio = this.aspectRatio;
|
|
if (!retinaScale(this, newRatio, true)) {
|
|
return;
|
|
}
|
|
this.notifyPlugins('resize', {
|
|
size: newSize
|
|
});
|
|
callback(options.onResize, [
|
|
this,
|
|
newSize
|
|
], this);
|
|
if (this.attached) {
|
|
if (this._doResize(mode)) {
|
|
this.render();
|
|
}
|
|
}
|
|
}
|
|
ensureScalesHaveIDs() {
|
|
const options = this.options;
|
|
const scalesOptions = options.scales || {};
|
|
each(scalesOptions, (axisOptions, axisID)=>{
|
|
axisOptions.id = axisID;
|
|
});
|
|
}
|
|
buildOrUpdateScales() {
|
|
const options = this.options;
|
|
const scaleOpts = options.scales;
|
|
const scales = this.scales;
|
|
const updated = Object.keys(scales).reduce((obj, id)=>{
|
|
obj[id] = false;
|
|
return obj;
|
|
}, {});
|
|
let items = [];
|
|
if (scaleOpts) {
|
|
items = items.concat(Object.keys(scaleOpts).map((id)=>{
|
|
const scaleOptions = scaleOpts[id];
|
|
const axis = determineAxis(id, scaleOptions);
|
|
const isRadial = axis === 'r';
|
|
const isHorizontal = axis === 'x';
|
|
return {
|
|
options: scaleOptions,
|
|
dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left',
|
|
dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear'
|
|
};
|
|
}));
|
|
}
|
|
each(items, (item)=>{
|
|
const scaleOptions = item.options;
|
|
const id = scaleOptions.id;
|
|
const axis = determineAxis(id, scaleOptions);
|
|
const scaleType = valueOrDefault(scaleOptions.type, item.dtype);
|
|
if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) {
|
|
scaleOptions.position = item.dposition;
|
|
}
|
|
updated[id] = true;
|
|
let scale = null;
|
|
if (id in scales && scales[id].type === scaleType) {
|
|
scale = scales[id];
|
|
} else {
|
|
const scaleClass = registry.getScale(scaleType);
|
|
scale = new scaleClass({
|
|
id,
|
|
type: scaleType,
|
|
ctx: this.ctx,
|
|
chart: this
|
|
});
|
|
scales[scale.id] = scale;
|
|
}
|
|
scale.init(scaleOptions, options);
|
|
});
|
|
each(updated, (hasUpdated, id)=>{
|
|
if (!hasUpdated) {
|
|
delete scales[id];
|
|
}
|
|
});
|
|
each(scales, (scale)=>{
|
|
layouts.configure(this, scale, scale.options);
|
|
layouts.addBox(this, scale);
|
|
});
|
|
}
|
|
_updateMetasets() {
|
|
const metasets = this._metasets;
|
|
const numData = this.data.datasets.length;
|
|
const numMeta = metasets.length;
|
|
metasets.sort((a, b)=>a.index - b.index);
|
|
if (numMeta > numData) {
|
|
for(let i = numData; i < numMeta; ++i){
|
|
this._destroyDatasetMeta(i);
|
|
}
|
|
metasets.splice(numData, numMeta - numData);
|
|
}
|
|
this._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index'));
|
|
}
|
|
_removeUnreferencedMetasets() {
|
|
const { _metasets: metasets , data: { datasets } } = this;
|
|
if (metasets.length > datasets.length) {
|
|
delete this._stacks;
|
|
}
|
|
metasets.forEach((meta, index)=>{
|
|
if (datasets.filter((x)=>x === meta._dataset).length === 0) {
|
|
this._destroyDatasetMeta(index);
|
|
}
|
|
});
|
|
}
|
|
buildOrUpdateControllers() {
|
|
const newControllers = [];
|
|
const datasets = this.data.datasets;
|
|
let i, ilen;
|
|
this._removeUnreferencedMetasets();
|
|
for(i = 0, ilen = datasets.length; i < ilen; i++){
|
|
const dataset = datasets[i];
|
|
let meta = this.getDatasetMeta(i);
|
|
const type = dataset.type || this.config.type;
|
|
if (meta.type && meta.type !== type) {
|
|
this._destroyDatasetMeta(i);
|
|
meta = this.getDatasetMeta(i);
|
|
}
|
|
meta.type = type;
|
|
meta.indexAxis = dataset.indexAxis || getIndexAxis(type, this.options);
|
|
meta.order = dataset.order || 0;
|
|
meta.index = i;
|
|
meta.label = '' + dataset.label;
|
|
meta.visible = this.isDatasetVisible(i);
|
|
if (meta.controller) {
|
|
meta.controller.updateIndex(i);
|
|
meta.controller.linkScales();
|
|
} else {
|
|
const ControllerClass = registry.getController(type);
|
|
const { datasetElementType , dataElementType } = defaults.datasets[type];
|
|
Object.assign(ControllerClass, {
|
|
dataElementType: registry.getElement(dataElementType),
|
|
datasetElementType: datasetElementType && registry.getElement(datasetElementType)
|
|
});
|
|
meta.controller = new ControllerClass(this, i);
|
|
newControllers.push(meta.controller);
|
|
}
|
|
}
|
|
this._updateMetasets();
|
|
return newControllers;
|
|
}
|
|
_resetElements() {
|
|
each(this.data.datasets, (dataset, datasetIndex)=>{
|
|
this.getDatasetMeta(datasetIndex).controller.reset();
|
|
}, this);
|
|
}
|
|
reset() {
|
|
this._resetElements();
|
|
this.notifyPlugins('reset');
|
|
}
|
|
update(mode) {
|
|
const config = this.config;
|
|
config.update();
|
|
const options = this._options = config.createResolver(config.chartOptionScopes(), this.getContext());
|
|
const animsDisabled = this._animationsDisabled = !options.animation;
|
|
this._updateScales();
|
|
this._checkEventBindings();
|
|
this._updateHiddenIndices();
|
|
this._plugins.invalidate();
|
|
if (this.notifyPlugins('beforeUpdate', {
|
|
mode,
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
const newControllers = this.buildOrUpdateControllers();
|
|
this.notifyPlugins('beforeElementsUpdate');
|
|
let minPadding = 0;
|
|
for(let i = 0, ilen = this.data.datasets.length; i < ilen; i++){
|
|
const { controller } = this.getDatasetMeta(i);
|
|
const reset = !animsDisabled && newControllers.indexOf(controller) === -1;
|
|
controller.buildOrUpdateElements(reset);
|
|
minPadding = Math.max(+controller.getMaxOverflow(), minPadding);
|
|
}
|
|
minPadding = this._minPadding = options.layout.autoPadding ? minPadding : 0;
|
|
this._updateLayout(minPadding);
|
|
if (!animsDisabled) {
|
|
each(newControllers, (controller)=>{
|
|
controller.reset();
|
|
});
|
|
}
|
|
this._updateDatasets(mode);
|
|
this.notifyPlugins('afterUpdate', {
|
|
mode
|
|
});
|
|
this._layers.sort(compare2Level('z', '_idx'));
|
|
const { _active , _lastEvent } = this;
|
|
if (_lastEvent) {
|
|
this._eventHandler(_lastEvent, true);
|
|
} else if (_active.length) {
|
|
this._updateHoverStyles(_active, _active, true);
|
|
}
|
|
this.render();
|
|
}
|
|
_updateScales() {
|
|
each(this.scales, (scale)=>{
|
|
layouts.removeBox(this, scale);
|
|
});
|
|
this.ensureScalesHaveIDs();
|
|
this.buildOrUpdateScales();
|
|
}
|
|
_checkEventBindings() {
|
|
const options = this.options;
|
|
const existingEvents = new Set(Object.keys(this._listeners));
|
|
const newEvents = new Set(options.events);
|
|
if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== options.responsive) {
|
|
this.unbindEvents();
|
|
this.bindEvents();
|
|
}
|
|
}
|
|
_updateHiddenIndices() {
|
|
const { _hiddenIndices } = this;
|
|
const changes = this._getUniformDataChanges() || [];
|
|
for (const { method , start , count } of changes){
|
|
const move = method === '_removeElements' ? -count : count;
|
|
moveNumericKeys(_hiddenIndices, start, move);
|
|
}
|
|
}
|
|
_getUniformDataChanges() {
|
|
const _dataChanges = this._dataChanges;
|
|
if (!_dataChanges || !_dataChanges.length) {
|
|
return;
|
|
}
|
|
this._dataChanges = [];
|
|
const datasetCount = this.data.datasets.length;
|
|
const makeSet = (idx)=>new Set(_dataChanges.filter((c)=>c[0] === idx).map((c, i)=>i + ',' + c.splice(1).join(',')));
|
|
const changeSet = makeSet(0);
|
|
for(let i = 1; i < datasetCount; i++){
|
|
if (!setsEqual(changeSet, makeSet(i))) {
|
|
return;
|
|
}
|
|
}
|
|
return Array.from(changeSet).map((c)=>c.split(',')).map((a)=>({
|
|
method: a[1],
|
|
start: +a[2],
|
|
count: +a[3]
|
|
}));
|
|
}
|
|
_updateLayout(minPadding) {
|
|
if (this.notifyPlugins('beforeLayout', {
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
layouts.update(this, this.width, this.height, minPadding);
|
|
const area = this.chartArea;
|
|
const noArea = area.width <= 0 || area.height <= 0;
|
|
this._layers = [];
|
|
each(this.boxes, (box)=>{
|
|
if (noArea && box.position === 'chartArea') {
|
|
return;
|
|
}
|
|
if (box.configure) {
|
|
box.configure();
|
|
}
|
|
this._layers.push(...box._layers());
|
|
}, this);
|
|
this._layers.forEach((item, index)=>{
|
|
item._idx = index;
|
|
});
|
|
this.notifyPlugins('afterLayout');
|
|
}
|
|
_updateDatasets(mode) {
|
|
if (this.notifyPlugins('beforeDatasetsUpdate', {
|
|
mode,
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
for(let i = 0, ilen = this.data.datasets.length; i < ilen; ++i){
|
|
this.getDatasetMeta(i).controller.configure();
|
|
}
|
|
for(let i = 0, ilen = this.data.datasets.length; i < ilen; ++i){
|
|
this._updateDataset(i, isFunction(mode) ? mode({
|
|
datasetIndex: i
|
|
}) : mode);
|
|
}
|
|
this.notifyPlugins('afterDatasetsUpdate', {
|
|
mode
|
|
});
|
|
}
|
|
_updateDataset(index, mode) {
|
|
const meta = this.getDatasetMeta(index);
|
|
const args = {
|
|
meta,
|
|
index,
|
|
mode,
|
|
cancelable: true
|
|
};
|
|
if (this.notifyPlugins('beforeDatasetUpdate', args) === false) {
|
|
return;
|
|
}
|
|
meta.controller._update(mode);
|
|
args.cancelable = false;
|
|
this.notifyPlugins('afterDatasetUpdate', args);
|
|
}
|
|
render() {
|
|
if (this.notifyPlugins('beforeRender', {
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
if (animator.has(this)) {
|
|
if (this.attached && !animator.running(this)) {
|
|
animator.start(this);
|
|
}
|
|
} else {
|
|
this.draw();
|
|
onAnimationsComplete({
|
|
chart: this
|
|
});
|
|
}
|
|
}
|
|
draw() {
|
|
let i;
|
|
if (this._resizeBeforeDraw) {
|
|
const { width , height } = this._resizeBeforeDraw;
|
|
this._resizeBeforeDraw = null;
|
|
this._resize(width, height);
|
|
}
|
|
this.clear();
|
|
if (this.width <= 0 || this.height <= 0) {
|
|
return;
|
|
}
|
|
if (this.notifyPlugins('beforeDraw', {
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
const layers = this._layers;
|
|
for(i = 0; i < layers.length && layers[i].z <= 0; ++i){
|
|
layers[i].draw(this.chartArea);
|
|
}
|
|
this._drawDatasets();
|
|
for(; i < layers.length; ++i){
|
|
layers[i].draw(this.chartArea);
|
|
}
|
|
this.notifyPlugins('afterDraw');
|
|
}
|
|
_getSortedDatasetMetas(filterVisible) {
|
|
const metasets = this._sortedMetasets;
|
|
const result = [];
|
|
let i, ilen;
|
|
for(i = 0, ilen = metasets.length; i < ilen; ++i){
|
|
const meta = metasets[i];
|
|
if (!filterVisible || meta.visible) {
|
|
result.push(meta);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
getSortedVisibleDatasetMetas() {
|
|
return this._getSortedDatasetMetas(true);
|
|
}
|
|
_drawDatasets() {
|
|
if (this.notifyPlugins('beforeDatasetsDraw', {
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
const metasets = this.getSortedVisibleDatasetMetas();
|
|
for(let i = metasets.length - 1; i >= 0; --i){
|
|
this._drawDataset(metasets[i]);
|
|
}
|
|
this.notifyPlugins('afterDatasetsDraw');
|
|
}
|
|
_drawDataset(meta) {
|
|
const ctx = this.ctx;
|
|
const args = {
|
|
meta,
|
|
index: meta.index,
|
|
cancelable: true
|
|
};
|
|
const clip = getDatasetClipArea(this, meta);
|
|
if (this.notifyPlugins('beforeDatasetDraw', args) === false) {
|
|
return;
|
|
}
|
|
if (clip) {
|
|
clipArea(ctx, clip);
|
|
}
|
|
meta.controller.draw();
|
|
if (clip) {
|
|
unclipArea(ctx);
|
|
}
|
|
args.cancelable = false;
|
|
this.notifyPlugins('afterDatasetDraw', args);
|
|
}
|
|
isPointInArea(point) {
|
|
return _isPointInArea(point, this.chartArea, this._minPadding);
|
|
}
|
|
getElementsAtEventForMode(e, mode, options, useFinalPosition) {
|
|
const method = Interaction.modes[mode];
|
|
if (typeof method === 'function') {
|
|
return method(this, e, options, useFinalPosition);
|
|
}
|
|
return [];
|
|
}
|
|
getDatasetMeta(datasetIndex) {
|
|
const dataset = this.data.datasets[datasetIndex];
|
|
const metasets = this._metasets;
|
|
let meta = metasets.filter((x)=>x && x._dataset === dataset).pop();
|
|
if (!meta) {
|
|
meta = {
|
|
type: null,
|
|
data: [],
|
|
dataset: null,
|
|
controller: null,
|
|
hidden: null,
|
|
xAxisID: null,
|
|
yAxisID: null,
|
|
order: dataset && dataset.order || 0,
|
|
index: datasetIndex,
|
|
_dataset: dataset,
|
|
_parsed: [],
|
|
_sorted: false
|
|
};
|
|
metasets.push(meta);
|
|
}
|
|
return meta;
|
|
}
|
|
getContext() {
|
|
return this.$context || (this.$context = createContext(null, {
|
|
chart: this,
|
|
type: 'chart'
|
|
}));
|
|
}
|
|
getVisibleDatasetCount() {
|
|
return this.getSortedVisibleDatasetMetas().length;
|
|
}
|
|
isDatasetVisible(datasetIndex) {
|
|
const dataset = this.data.datasets[datasetIndex];
|
|
if (!dataset) {
|
|
return false;
|
|
}
|
|
const meta = this.getDatasetMeta(datasetIndex);
|
|
return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden;
|
|
}
|
|
setDatasetVisibility(datasetIndex, visible) {
|
|
const meta = this.getDatasetMeta(datasetIndex);
|
|
meta.hidden = !visible;
|
|
}
|
|
toggleDataVisibility(index) {
|
|
this._hiddenIndices[index] = !this._hiddenIndices[index];
|
|
}
|
|
getDataVisibility(index) {
|
|
return !this._hiddenIndices[index];
|
|
}
|
|
_updateVisibility(datasetIndex, dataIndex, visible) {
|
|
const mode = visible ? 'show' : 'hide';
|
|
const meta = this.getDatasetMeta(datasetIndex);
|
|
const anims = meta.controller._resolveAnimations(undefined, mode);
|
|
if (defined(dataIndex)) {
|
|
meta.data[dataIndex].hidden = !visible;
|
|
this.update();
|
|
} else {
|
|
this.setDatasetVisibility(datasetIndex, visible);
|
|
anims.update(meta, {
|
|
visible
|
|
});
|
|
this.update((ctx)=>ctx.datasetIndex === datasetIndex ? mode : undefined);
|
|
}
|
|
}
|
|
hide(datasetIndex, dataIndex) {
|
|
this._updateVisibility(datasetIndex, dataIndex, false);
|
|
}
|
|
show(datasetIndex, dataIndex) {
|
|
this._updateVisibility(datasetIndex, dataIndex, true);
|
|
}
|
|
_destroyDatasetMeta(datasetIndex) {
|
|
const meta = this._metasets[datasetIndex];
|
|
if (meta && meta.controller) {
|
|
meta.controller._destroy();
|
|
}
|
|
delete this._metasets[datasetIndex];
|
|
}
|
|
_stop() {
|
|
let i, ilen;
|
|
this.stop();
|
|
animator.remove(this);
|
|
for(i = 0, ilen = this.data.datasets.length; i < ilen; ++i){
|
|
this._destroyDatasetMeta(i);
|
|
}
|
|
}
|
|
destroy() {
|
|
this.notifyPlugins('beforeDestroy');
|
|
const { canvas , ctx } = this;
|
|
this._stop();
|
|
this.config.clearCache();
|
|
if (canvas) {
|
|
this.unbindEvents();
|
|
clearCanvas(canvas, ctx);
|
|
this.platform.releaseContext(ctx);
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
}
|
|
delete instances[this.id];
|
|
this.notifyPlugins('afterDestroy');
|
|
}
|
|
toBase64Image(...args) {
|
|
return this.canvas.toDataURL(...args);
|
|
}
|
|
bindEvents() {
|
|
this.bindUserEvents();
|
|
if (this.options.responsive) {
|
|
this.bindResponsiveEvents();
|
|
} else {
|
|
this.attached = true;
|
|
}
|
|
}
|
|
bindUserEvents() {
|
|
const listeners = this._listeners;
|
|
const platform = this.platform;
|
|
const _add = (type, listener)=>{
|
|
platform.addEventListener(this, type, listener);
|
|
listeners[type] = listener;
|
|
};
|
|
const listener = (e, x, y)=>{
|
|
e.offsetX = x;
|
|
e.offsetY = y;
|
|
this._eventHandler(e);
|
|
};
|
|
each(this.options.events, (type)=>_add(type, listener));
|
|
}
|
|
bindResponsiveEvents() {
|
|
if (!this._responsiveListeners) {
|
|
this._responsiveListeners = {};
|
|
}
|
|
const listeners = this._responsiveListeners;
|
|
const platform = this.platform;
|
|
const _add = (type, listener)=>{
|
|
platform.addEventListener(this, type, listener);
|
|
listeners[type] = listener;
|
|
};
|
|
const _remove = (type, listener)=>{
|
|
if (listeners[type]) {
|
|
platform.removeEventListener(this, type, listener);
|
|
delete listeners[type];
|
|
}
|
|
};
|
|
const listener = (width, height)=>{
|
|
if (this.canvas) {
|
|
this.resize(width, height);
|
|
}
|
|
};
|
|
let detached;
|
|
const attached = ()=>{
|
|
_remove('attach', attached);
|
|
this.attached = true;
|
|
this.resize();
|
|
_add('resize', listener);
|
|
_add('detach', detached);
|
|
};
|
|
detached = ()=>{
|
|
this.attached = false;
|
|
_remove('resize', listener);
|
|
this._stop();
|
|
this._resize(0, 0);
|
|
_add('attach', attached);
|
|
};
|
|
if (platform.isAttached(this.canvas)) {
|
|
attached();
|
|
} else {
|
|
detached();
|
|
}
|
|
}
|
|
unbindEvents() {
|
|
each(this._listeners, (listener, type)=>{
|
|
this.platform.removeEventListener(this, type, listener);
|
|
});
|
|
this._listeners = {};
|
|
each(this._responsiveListeners, (listener, type)=>{
|
|
this.platform.removeEventListener(this, type, listener);
|
|
});
|
|
this._responsiveListeners = undefined;
|
|
}
|
|
updateHoverStyle(items, mode, enabled) {
|
|
const prefix = enabled ? 'set' : 'remove';
|
|
let meta, item, i, ilen;
|
|
if (mode === 'dataset') {
|
|
meta = this.getDatasetMeta(items[0].datasetIndex);
|
|
meta.controller['_' + prefix + 'DatasetHoverStyle']();
|
|
}
|
|
for(i = 0, ilen = items.length; i < ilen; ++i){
|
|
item = items[i];
|
|
const controller = item && this.getDatasetMeta(item.datasetIndex).controller;
|
|
if (controller) {
|
|
controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index);
|
|
}
|
|
}
|
|
}
|
|
getActiveElements() {
|
|
return this._active || [];
|
|
}
|
|
setActiveElements(activeElements) {
|
|
const lastActive = this._active || [];
|
|
const active = activeElements.map(({ datasetIndex , index })=>{
|
|
const meta = this.getDatasetMeta(datasetIndex);
|
|
if (!meta) {
|
|
throw new Error('No dataset found at index ' + datasetIndex);
|
|
}
|
|
return {
|
|
datasetIndex,
|
|
element: meta.data[index],
|
|
index
|
|
};
|
|
});
|
|
const changed = !_elementsEqual(active, lastActive);
|
|
if (changed) {
|
|
this._active = active;
|
|
this._lastEvent = null;
|
|
this._updateHoverStyles(active, lastActive);
|
|
}
|
|
}
|
|
notifyPlugins(hook, args, filter) {
|
|
return this._plugins.notify(this, hook, args, filter);
|
|
}
|
|
isPluginEnabled(pluginId) {
|
|
return this._plugins._cache.filter((p)=>p.plugin.id === pluginId).length === 1;
|
|
}
|
|
_updateHoverStyles(active, lastActive, replay) {
|
|
const hoverOptions = this.options.hover;
|
|
const diff = (a, b)=>a.filter((x)=>!b.some((y)=>x.datasetIndex === y.datasetIndex && x.index === y.index));
|
|
const deactivated = diff(lastActive, active);
|
|
const activated = replay ? active : diff(active, lastActive);
|
|
if (deactivated.length) {
|
|
this.updateHoverStyle(deactivated, hoverOptions.mode, false);
|
|
}
|
|
if (activated.length && hoverOptions.mode) {
|
|
this.updateHoverStyle(activated, hoverOptions.mode, true);
|
|
}
|
|
}
|
|
_eventHandler(e, replay) {
|
|
const args = {
|
|
event: e,
|
|
replay,
|
|
cancelable: true,
|
|
inChartArea: this.isPointInArea(e)
|
|
};
|
|
const eventFilter = (plugin)=>(plugin.options.events || this.options.events).includes(e.native.type);
|
|
if (this.notifyPlugins('beforeEvent', args, eventFilter) === false) {
|
|
return;
|
|
}
|
|
const changed = this._handleEvent(e, replay, args.inChartArea);
|
|
args.cancelable = false;
|
|
this.notifyPlugins('afterEvent', args, eventFilter);
|
|
if (changed || args.changed) {
|
|
this.render();
|
|
}
|
|
return this;
|
|
}
|
|
_handleEvent(e, replay, inChartArea) {
|
|
const { _active: lastActive = [] , options } = this;
|
|
const useFinalPosition = replay;
|
|
const active = this._getActiveElements(e, lastActive, inChartArea, useFinalPosition);
|
|
const isClick = _isClickEvent(e);
|
|
const lastEvent = determineLastEvent(e, this._lastEvent, inChartArea, isClick);
|
|
if (inChartArea) {
|
|
this._lastEvent = null;
|
|
callback(options.onHover, [
|
|
e,
|
|
active,
|
|
this
|
|
], this);
|
|
if (isClick) {
|
|
callback(options.onClick, [
|
|
e,
|
|
active,
|
|
this
|
|
], this);
|
|
}
|
|
}
|
|
const changed = !_elementsEqual(active, lastActive);
|
|
if (changed || replay) {
|
|
this._active = active;
|
|
this._updateHoverStyles(active, lastActive, replay);
|
|
}
|
|
this._lastEvent = lastEvent;
|
|
return changed;
|
|
}
|
|
_getActiveElements(e, lastActive, inChartArea, useFinalPosition) {
|
|
if (e.type === 'mouseout') {
|
|
return [];
|
|
}
|
|
if (!inChartArea) {
|
|
return lastActive;
|
|
}
|
|
const hoverOptions = this.options.hover;
|
|
return this.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition);
|
|
}
|
|
}
|
|
function invalidatePlugins() {
|
|
return each(chart_Chart.instances, (chart)=>chart._plugins.invalidate());
|
|
}
|
|
|
|
function clipSelf(ctx, element, endAngle) {
|
|
const { startAngle , x , y , outerRadius , innerRadius , options } = element;
|
|
const { borderWidth , borderJoinStyle } = options;
|
|
const outerAngleClip = Math.min(borderWidth / outerRadius, _normalizeAngle(startAngle - endAngle));
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, outerRadius - borderWidth / 2, startAngle + outerAngleClip / 2, endAngle - outerAngleClip / 2);
|
|
if (innerRadius > 0) {
|
|
const innerAngleClip = Math.min(borderWidth / innerRadius, _normalizeAngle(startAngle - endAngle));
|
|
ctx.arc(x, y, innerRadius + borderWidth / 2, endAngle - innerAngleClip / 2, startAngle + innerAngleClip / 2, true);
|
|
} else {
|
|
const clipWidth = Math.min(borderWidth / 2, outerRadius * _normalizeAngle(startAngle - endAngle));
|
|
if (borderJoinStyle === 'round') {
|
|
ctx.arc(x, y, clipWidth, endAngle - PI / 2, startAngle + PI / 2, true);
|
|
} else if (borderJoinStyle === 'bevel') {
|
|
const r = 2 * clipWidth * clipWidth;
|
|
const endX = -r * Math.cos(endAngle + PI / 2) + x;
|
|
const endY = -r * Math.sin(endAngle + PI / 2) + y;
|
|
const startX = r * Math.cos(startAngle + PI / 2) + x;
|
|
const startY = r * Math.sin(startAngle + PI / 2) + y;
|
|
ctx.lineTo(endX, endY);
|
|
ctx.lineTo(startX, startY);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
ctx.clip('evenodd');
|
|
}
|
|
function clipArc(ctx, element, endAngle) {
|
|
const { startAngle , pixelMargin , x , y , outerRadius , innerRadius } = element;
|
|
let angleMargin = pixelMargin / outerRadius;
|
|
// Draw an inner border by clipping the arc and drawing a double-width border
|
|
// Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin);
|
|
if (innerRadius > pixelMargin) {
|
|
angleMargin = pixelMargin / innerRadius;
|
|
ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true);
|
|
} else {
|
|
ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI);
|
|
}
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
}
|
|
function toRadiusCorners(value) {
|
|
return _readValueToProps(value, [
|
|
'outerStart',
|
|
'outerEnd',
|
|
'innerStart',
|
|
'innerEnd'
|
|
]);
|
|
}
|
|
/**
|
|
* Parse border radius from the provided options
|
|
*/ function parseBorderRadius$1(arc, innerRadius, outerRadius, angleDelta) {
|
|
const o = toRadiusCorners(arc.options.borderRadius);
|
|
const halfThickness = (outerRadius - innerRadius) / 2;
|
|
const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2);
|
|
// Outer limits are complicated. We want to compute the available angular distance at
|
|
// a radius of outerRadius - borderRadius because for small angular distances, this term limits.
|
|
// We compute at r = outerRadius - borderRadius because this circle defines the center of the border corners.
|
|
//
|
|
// If the borderRadius is large, that value can become negative.
|
|
// This causes the outer borders to lose their radius entirely, which is rather unexpected. To solve that, if borderRadius > outerRadius
|
|
// we know that the thickness term will dominate and compute the limits at that point
|
|
const computeOuterLimit = (val)=>{
|
|
const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2;
|
|
return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit));
|
|
};
|
|
return {
|
|
outerStart: computeOuterLimit(o.outerStart),
|
|
outerEnd: computeOuterLimit(o.outerEnd),
|
|
innerStart: _limitValue(o.innerStart, 0, innerLimit),
|
|
innerEnd: _limitValue(o.innerEnd, 0, innerLimit)
|
|
};
|
|
}
|
|
/**
|
|
* Convert (r, 𝜃) to (x, y)
|
|
*/ function rThetaToXY(r, theta, x, y) {
|
|
return {
|
|
x: x + r * Math.cos(theta),
|
|
y: y + r * Math.sin(theta)
|
|
};
|
|
}
|
|
/**
|
|
* Path the arc, respecting border radius by separating into left and right halves.
|
|
*
|
|
* Start End
|
|
*
|
|
* 1--->a--->2 Outer
|
|
* / \
|
|
* 8 3
|
|
* | |
|
|
* | |
|
|
* 7 4
|
|
* \ /
|
|
* 6<---b<---5 Inner
|
|
*/ function pathArc(ctx, element, offset, spacing, end, circular) {
|
|
const { x , y , startAngle: start , pixelMargin , innerRadius: innerR } = element;
|
|
const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0);
|
|
const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0;
|
|
let spacingOffset = 0;
|
|
const alpha = end - start;
|
|
if (spacing) {
|
|
// When spacing is present, it is the same for all items
|
|
// So we adjust the start and end angle of the arc such that
|
|
// the distance is the same as it would be without the spacing
|
|
const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0;
|
|
const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0;
|
|
const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2;
|
|
const adjustedAngle = avNogSpacingRadius !== 0 ? alpha * avNogSpacingRadius / (avNogSpacingRadius + spacing) : alpha;
|
|
spacingOffset = (alpha - adjustedAngle) / 2;
|
|
}
|
|
const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius;
|
|
const angleOffset = (alpha - beta) / 2;
|
|
const startAngle = start + angleOffset + spacingOffset;
|
|
const endAngle = end - angleOffset - spacingOffset;
|
|
const { outerStart , outerEnd , innerStart , innerEnd } = parseBorderRadius$1(element, innerRadius, outerRadius, endAngle - startAngle);
|
|
const outerStartAdjustedRadius = outerRadius - outerStart;
|
|
const outerEndAdjustedRadius = outerRadius - outerEnd;
|
|
const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius;
|
|
const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius;
|
|
const innerStartAdjustedRadius = innerRadius + innerStart;
|
|
const innerEndAdjustedRadius = innerRadius + innerEnd;
|
|
const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius;
|
|
const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius;
|
|
ctx.beginPath();
|
|
if (circular) {
|
|
// The first arc segments from point 1 to point a to point 2
|
|
const outerMidAdjustedAngle = (outerStartAdjustedAngle + outerEndAdjustedAngle) / 2;
|
|
ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerMidAdjustedAngle);
|
|
ctx.arc(x, y, outerRadius, outerMidAdjustedAngle, outerEndAdjustedAngle);
|
|
// The corner segment from point 2 to point 3
|
|
if (outerEnd > 0) {
|
|
const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y);
|
|
ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI);
|
|
}
|
|
// The line from point 3 to point 4
|
|
const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y);
|
|
ctx.lineTo(p4.x, p4.y);
|
|
// The corner segment from point 4 to point 5
|
|
if (innerEnd > 0) {
|
|
const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y);
|
|
ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI);
|
|
}
|
|
// The inner arc from point 5 to point b to point 6
|
|
const innerMidAdjustedAngle = (endAngle - innerEnd / innerRadius + (startAngle + innerStart / innerRadius)) / 2;
|
|
ctx.arc(x, y, innerRadius, endAngle - innerEnd / innerRadius, innerMidAdjustedAngle, true);
|
|
ctx.arc(x, y, innerRadius, innerMidAdjustedAngle, startAngle + innerStart / innerRadius, true);
|
|
// The corner segment from point 6 to point 7
|
|
if (innerStart > 0) {
|
|
const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y);
|
|
ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI);
|
|
}
|
|
// The line from point 7 to point 8
|
|
const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y);
|
|
ctx.lineTo(p8.x, p8.y);
|
|
// The corner segment from point 8 to point 1
|
|
if (outerStart > 0) {
|
|
const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y);
|
|
ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle);
|
|
}
|
|
} else {
|
|
ctx.moveTo(x, y);
|
|
const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x;
|
|
const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y;
|
|
ctx.lineTo(outerStartX, outerStartY);
|
|
const outerEndX = Math.cos(outerEndAdjustedAngle) * outerRadius + x;
|
|
const outerEndY = Math.sin(outerEndAdjustedAngle) * outerRadius + y;
|
|
ctx.lineTo(outerEndX, outerEndY);
|
|
}
|
|
ctx.closePath();
|
|
}
|
|
function drawArc(ctx, element, offset, spacing, circular) {
|
|
const { fullCircles , startAngle , circumference } = element;
|
|
let endAngle = element.endAngle;
|
|
if (fullCircles) {
|
|
pathArc(ctx, element, offset, spacing, endAngle, circular);
|
|
for(let i = 0; i < fullCircles; ++i){
|
|
ctx.fill();
|
|
}
|
|
if (!isNaN(circumference)) {
|
|
endAngle = startAngle + (circumference % TAU || TAU);
|
|
}
|
|
}
|
|
pathArc(ctx, element, offset, spacing, endAngle, circular);
|
|
ctx.fill();
|
|
return endAngle;
|
|
}
|
|
function drawBorder(ctx, element, offset, spacing, circular) {
|
|
const { fullCircles , startAngle , circumference , options } = element;
|
|
const { borderWidth , borderJoinStyle , borderDash , borderDashOffset , borderRadius } = options;
|
|
const inner = options.borderAlign === 'inner';
|
|
if (!borderWidth) {
|
|
return;
|
|
}
|
|
ctx.setLineDash(borderDash || []);
|
|
ctx.lineDashOffset = borderDashOffset;
|
|
if (inner) {
|
|
ctx.lineWidth = borderWidth * 2;
|
|
ctx.lineJoin = borderJoinStyle || 'round';
|
|
} else {
|
|
ctx.lineWidth = borderWidth;
|
|
ctx.lineJoin = borderJoinStyle || 'bevel';
|
|
}
|
|
let endAngle = element.endAngle;
|
|
if (fullCircles) {
|
|
pathArc(ctx, element, offset, spacing, endAngle, circular);
|
|
for(let i = 0; i < fullCircles; ++i){
|
|
ctx.stroke();
|
|
}
|
|
if (!isNaN(circumference)) {
|
|
endAngle = startAngle + (circumference % TAU || TAU);
|
|
}
|
|
}
|
|
if (inner) {
|
|
clipArc(ctx, element, endAngle);
|
|
}
|
|
if (options.selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter') {
|
|
clipSelf(ctx, element, endAngle);
|
|
}
|
|
if (!fullCircles) {
|
|
pathArc(ctx, element, offset, spacing, endAngle, circular);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
class ArcElement extends chart_Element {
|
|
static id = 'arc';
|
|
static defaults = {
|
|
borderAlign: 'center',
|
|
borderColor: '#fff',
|
|
borderDash: [],
|
|
borderDashOffset: 0,
|
|
borderJoinStyle: undefined,
|
|
borderRadius: 0,
|
|
borderWidth: 2,
|
|
offset: 0,
|
|
spacing: 0,
|
|
angle: undefined,
|
|
circular: true,
|
|
selfJoin: false
|
|
};
|
|
static defaultRoutes = {
|
|
backgroundColor: 'backgroundColor'
|
|
};
|
|
static descriptors = {
|
|
_scriptable: true,
|
|
_indexable: (name)=>name !== 'borderDash'
|
|
};
|
|
circumference;
|
|
endAngle;
|
|
fullCircles;
|
|
innerRadius;
|
|
outerRadius;
|
|
pixelMargin;
|
|
startAngle;
|
|
constructor(cfg){
|
|
super();
|
|
this.options = undefined;
|
|
this.circumference = undefined;
|
|
this.startAngle = undefined;
|
|
this.endAngle = undefined;
|
|
this.innerRadius = undefined;
|
|
this.outerRadius = undefined;
|
|
this.pixelMargin = 0;
|
|
this.fullCircles = 0;
|
|
if (cfg) {
|
|
Object.assign(this, cfg);
|
|
}
|
|
}
|
|
inRange(chartX, chartY, useFinalPosition) {
|
|
const point = this.getProps([
|
|
'x',
|
|
'y'
|
|
], useFinalPosition);
|
|
const { angle , distance } = getAngleFromPoint(point, {
|
|
x: chartX,
|
|
y: chartY
|
|
});
|
|
const { startAngle , endAngle , innerRadius , outerRadius , circumference } = this.getProps([
|
|
'startAngle',
|
|
'endAngle',
|
|
'innerRadius',
|
|
'outerRadius',
|
|
'circumference'
|
|
], useFinalPosition);
|
|
const rAdjust = (this.options.spacing + this.options.borderWidth) / 2;
|
|
const _circumference = valueOrDefault(circumference, endAngle - startAngle);
|
|
const nonZeroBetween = _angleBetween(angle, startAngle, endAngle) && startAngle !== endAngle;
|
|
const betweenAngles = _circumference >= TAU || nonZeroBetween;
|
|
const withinRadius = _isBetween(distance, innerRadius + rAdjust, outerRadius + rAdjust);
|
|
return betweenAngles && withinRadius;
|
|
}
|
|
getCenterPoint(useFinalPosition) {
|
|
const { x , y , startAngle , endAngle , innerRadius , outerRadius } = this.getProps([
|
|
'x',
|
|
'y',
|
|
'startAngle',
|
|
'endAngle',
|
|
'innerRadius',
|
|
'outerRadius'
|
|
], useFinalPosition);
|
|
const { offset , spacing } = this.options;
|
|
const halfAngle = (startAngle + endAngle) / 2;
|
|
const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2;
|
|
return {
|
|
x: x + Math.cos(halfAngle) * halfRadius,
|
|
y: y + Math.sin(halfAngle) * halfRadius
|
|
};
|
|
}
|
|
tooltipPosition(useFinalPosition) {
|
|
return this.getCenterPoint(useFinalPosition);
|
|
}
|
|
draw(ctx) {
|
|
const { options , circumference } = this;
|
|
const offset = (options.offset || 0) / 4;
|
|
const spacing = (options.spacing || 0) / 2;
|
|
const circular = options.circular;
|
|
this.pixelMargin = options.borderAlign === 'inner' ? 0.33 : 0;
|
|
this.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0;
|
|
if (circumference === 0 || this.innerRadius < 0 || this.outerRadius < 0) {
|
|
return;
|
|
}
|
|
ctx.save();
|
|
const halfAngle = (this.startAngle + this.endAngle) / 2;
|
|
ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset);
|
|
const fix = 1 - Math.sin(Math.min(PI, circumference || 0));
|
|
const radiusOffset = offset * fix;
|
|
ctx.fillStyle = options.backgroundColor;
|
|
ctx.strokeStyle = options.borderColor;
|
|
drawArc(ctx, this, radiusOffset, spacing, circular);
|
|
drawBorder(ctx, this, radiusOffset, spacing, circular);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
function setStyle(ctx, options, style = options) {
|
|
ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle);
|
|
ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash));
|
|
ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset);
|
|
ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle);
|
|
ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth);
|
|
ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor);
|
|
}
|
|
function lineTo(ctx, previous, target) {
|
|
ctx.lineTo(target.x, target.y);
|
|
}
|
|
function getLineMethod(options) {
|
|
if (options.stepped) {
|
|
return _steppedLineTo;
|
|
}
|
|
if (options.tension || options.cubicInterpolationMode === 'monotone') {
|
|
return _bezierCurveTo;
|
|
}
|
|
return lineTo;
|
|
}
|
|
function pathVars(points, segment, params = {}) {
|
|
const count = points.length;
|
|
const { start: paramsStart = 0 , end: paramsEnd = count - 1 } = params;
|
|
const { start: segmentStart , end: segmentEnd } = segment;
|
|
const start = Math.max(paramsStart, segmentStart);
|
|
const end = Math.min(paramsEnd, segmentEnd);
|
|
const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd;
|
|
return {
|
|
count,
|
|
start,
|
|
loop: segment.loop,
|
|
ilen: end < start && !outside ? count + end - start : end - start
|
|
};
|
|
}
|
|
function pathSegment(ctx, line, segment, params) {
|
|
const { points , options } = line;
|
|
const { count , start , loop , ilen } = pathVars(points, segment, params);
|
|
const lineMethod = getLineMethod(options);
|
|
let { move =true , reverse } = params || {};
|
|
let i, point, prev;
|
|
for(i = 0; i <= ilen; ++i){
|
|
point = points[(start + (reverse ? ilen - i : i)) % count];
|
|
if (point.skip) {
|
|
continue;
|
|
} else if (move) {
|
|
ctx.moveTo(point.x, point.y);
|
|
move = false;
|
|
} else {
|
|
lineMethod(ctx, prev, point, reverse, options.stepped);
|
|
}
|
|
prev = point;
|
|
}
|
|
if (loop) {
|
|
point = points[(start + (reverse ? ilen : 0)) % count];
|
|
lineMethod(ctx, prev, point, reverse, options.stepped);
|
|
}
|
|
return !!loop;
|
|
}
|
|
function fastPathSegment(ctx, line, segment, params) {
|
|
const points = line.points;
|
|
const { count , start , ilen } = pathVars(points, segment, params);
|
|
const { move =true , reverse } = params || {};
|
|
let avgX = 0;
|
|
let countX = 0;
|
|
let i, point, prevX, minY, maxY, lastY;
|
|
const pointIndex = (index)=>(start + (reverse ? ilen - index : index)) % count;
|
|
const drawX = ()=>{
|
|
if (minY !== maxY) {
|
|
ctx.lineTo(avgX, maxY);
|
|
ctx.lineTo(avgX, minY);
|
|
ctx.lineTo(avgX, lastY);
|
|
}
|
|
};
|
|
if (move) {
|
|
point = points[pointIndex(0)];
|
|
ctx.moveTo(point.x, point.y);
|
|
}
|
|
for(i = 0; i <= ilen; ++i){
|
|
point = points[pointIndex(i)];
|
|
if (point.skip) {
|
|
continue;
|
|
}
|
|
const x = point.x;
|
|
const y = point.y;
|
|
const truncX = x | 0;
|
|
if (truncX === prevX) {
|
|
if (y < minY) {
|
|
minY = y;
|
|
} else if (y > maxY) {
|
|
maxY = y;
|
|
}
|
|
avgX = (countX * avgX + x) / ++countX;
|
|
} else {
|
|
drawX();
|
|
ctx.lineTo(x, y);
|
|
prevX = truncX;
|
|
countX = 0;
|
|
minY = maxY = y;
|
|
}
|
|
lastY = y;
|
|
}
|
|
drawX();
|
|
}
|
|
function _getSegmentMethod(line) {
|
|
const opts = line.options;
|
|
const borderDash = opts.borderDash && opts.borderDash.length;
|
|
const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash;
|
|
return useFastPath ? fastPathSegment : pathSegment;
|
|
}
|
|
function _getInterpolationMethod(options) {
|
|
if (options.stepped) {
|
|
return _steppedInterpolation;
|
|
}
|
|
if (options.tension || options.cubicInterpolationMode === 'monotone') {
|
|
return _bezierInterpolation;
|
|
}
|
|
return _pointInLine;
|
|
}
|
|
function strokePathWithCache(ctx, line, start, count) {
|
|
let path = line._path;
|
|
if (!path) {
|
|
path = line._path = new Path2D();
|
|
if (line.path(path, start, count)) {
|
|
path.closePath();
|
|
}
|
|
}
|
|
setStyle(ctx, line.options);
|
|
ctx.stroke(path);
|
|
}
|
|
function strokePathDirect(ctx, line, start, count) {
|
|
const { segments , options } = line;
|
|
const segmentMethod = _getSegmentMethod(line);
|
|
for (const segment of segments){
|
|
setStyle(ctx, options, segment.style);
|
|
ctx.beginPath();
|
|
if (segmentMethod(ctx, line, segment, {
|
|
start,
|
|
end: start + count - 1
|
|
})) {
|
|
ctx.closePath();
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
const usePath2D = typeof Path2D === 'function';
|
|
function draw(ctx, line, start, count) {
|
|
if (usePath2D && !line.options.segment) {
|
|
strokePathWithCache(ctx, line, start, count);
|
|
} else {
|
|
strokePathDirect(ctx, line, start, count);
|
|
}
|
|
}
|
|
class LineElement extends chart_Element {
|
|
static id = 'line';
|
|
static defaults = {
|
|
borderCapStyle: 'butt',
|
|
borderDash: [],
|
|
borderDashOffset: 0,
|
|
borderJoinStyle: 'miter',
|
|
borderWidth: 3,
|
|
capBezierPoints: true,
|
|
cubicInterpolationMode: 'default',
|
|
fill: false,
|
|
spanGaps: false,
|
|
stepped: false,
|
|
tension: 0
|
|
};
|
|
static defaultRoutes = {
|
|
backgroundColor: 'backgroundColor',
|
|
borderColor: 'borderColor'
|
|
};
|
|
static descriptors = {
|
|
_scriptable: true,
|
|
_indexable: (name)=>name !== 'borderDash' && name !== 'fill'
|
|
};
|
|
constructor(cfg){
|
|
super();
|
|
this.animated = true;
|
|
this.options = undefined;
|
|
this._chart = undefined;
|
|
this._loop = undefined;
|
|
this._fullLoop = undefined;
|
|
this._path = undefined;
|
|
this._points = undefined;
|
|
this._segments = undefined;
|
|
this._decimated = false;
|
|
this._pointsUpdated = false;
|
|
this._datasetIndex = undefined;
|
|
if (cfg) {
|
|
Object.assign(this, cfg);
|
|
}
|
|
}
|
|
updateControlPoints(chartArea, indexAxis) {
|
|
const options = this.options;
|
|
if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !this._pointsUpdated) {
|
|
const loop = options.spanGaps ? this._loop : this._fullLoop;
|
|
_updateBezierControlPoints(this._points, options, chartArea, loop, indexAxis);
|
|
this._pointsUpdated = true;
|
|
}
|
|
}
|
|
set points(points) {
|
|
this._points = points;
|
|
delete this._segments;
|
|
delete this._path;
|
|
this._pointsUpdated = false;
|
|
}
|
|
get points() {
|
|
return this._points;
|
|
}
|
|
get segments() {
|
|
return this._segments || (this._segments = _computeSegments(this, this.options.segment));
|
|
}
|
|
first() {
|
|
const segments = this.segments;
|
|
const points = this.points;
|
|
return segments.length && points[segments[0].start];
|
|
}
|
|
last() {
|
|
const segments = this.segments;
|
|
const points = this.points;
|
|
const count = segments.length;
|
|
return count && points[segments[count - 1].end];
|
|
}
|
|
interpolate(point, property) {
|
|
const options = this.options;
|
|
const value = point[property];
|
|
const points = this.points;
|
|
const segments = _boundSegments(this, {
|
|
property,
|
|
start: value,
|
|
end: value
|
|
});
|
|
if (!segments.length) {
|
|
return;
|
|
}
|
|
const result = [];
|
|
const _interpolate = _getInterpolationMethod(options);
|
|
let i, ilen;
|
|
for(i = 0, ilen = segments.length; i < ilen; ++i){
|
|
const { start , end } = segments[i];
|
|
const p1 = points[start];
|
|
const p2 = points[end];
|
|
if (p1 === p2) {
|
|
result.push(p1);
|
|
continue;
|
|
}
|
|
const t = Math.abs((value - p1[property]) / (p2[property] - p1[property]));
|
|
const interpolated = _interpolate(p1, p2, t, options.stepped);
|
|
interpolated[property] = point[property];
|
|
result.push(interpolated);
|
|
}
|
|
return result.length === 1 ? result[0] : result;
|
|
}
|
|
pathSegment(ctx, segment, params) {
|
|
const segmentMethod = _getSegmentMethod(this);
|
|
return segmentMethod(ctx, this, segment, params);
|
|
}
|
|
path(ctx, start, count) {
|
|
const segments = this.segments;
|
|
const segmentMethod = _getSegmentMethod(this);
|
|
let loop = this._loop;
|
|
start = start || 0;
|
|
count = count || this.points.length - start;
|
|
for (const segment of segments){
|
|
loop &= segmentMethod(ctx, this, segment, {
|
|
start,
|
|
end: start + count - 1
|
|
});
|
|
}
|
|
return !!loop;
|
|
}
|
|
draw(ctx, chartArea, start, count) {
|
|
const options = this.options || {};
|
|
const points = this.points || [];
|
|
if (points.length && options.borderWidth) {
|
|
ctx.save();
|
|
draw(ctx, this, start, count);
|
|
ctx.restore();
|
|
}
|
|
if (this.animated) {
|
|
this._pointsUpdated = false;
|
|
this._path = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
function inRange$1(el, pos, axis, useFinalPosition) {
|
|
const options = el.options;
|
|
const { [axis]: value } = el.getProps([
|
|
axis
|
|
], useFinalPosition);
|
|
return Math.abs(pos - value) < options.radius + options.hitRadius;
|
|
}
|
|
class PointElement extends chart_Element {
|
|
static id = 'point';
|
|
parsed;
|
|
skip;
|
|
stop;
|
|
/**
|
|
* @type {any}
|
|
*/ static defaults = {
|
|
borderWidth: 1,
|
|
hitRadius: 1,
|
|
hoverBorderWidth: 1,
|
|
hoverRadius: 4,
|
|
pointStyle: 'circle',
|
|
radius: 3,
|
|
rotation: 0
|
|
};
|
|
/**
|
|
* @type {any}
|
|
*/ static defaultRoutes = {
|
|
backgroundColor: 'backgroundColor',
|
|
borderColor: 'borderColor'
|
|
};
|
|
constructor(cfg){
|
|
super();
|
|
this.options = undefined;
|
|
this.parsed = undefined;
|
|
this.skip = undefined;
|
|
this.stop = undefined;
|
|
if (cfg) {
|
|
Object.assign(this, cfg);
|
|
}
|
|
}
|
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
const options = this.options;
|
|
const { x , y } = this.getProps([
|
|
'x',
|
|
'y'
|
|
], useFinalPosition);
|
|
return Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2) < Math.pow(options.hitRadius + options.radius, 2);
|
|
}
|
|
inXRange(mouseX, useFinalPosition) {
|
|
return inRange$1(this, mouseX, 'x', useFinalPosition);
|
|
}
|
|
inYRange(mouseY, useFinalPosition) {
|
|
return inRange$1(this, mouseY, 'y', useFinalPosition);
|
|
}
|
|
getCenterPoint(useFinalPosition) {
|
|
const { x , y } = this.getProps([
|
|
'x',
|
|
'y'
|
|
], useFinalPosition);
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
size(options) {
|
|
options = options || this.options || {};
|
|
let radius = options.radius || 0;
|
|
radius = Math.max(radius, radius && options.hoverRadius || 0);
|
|
const borderWidth = radius && options.borderWidth || 0;
|
|
return (radius + borderWidth) * 2;
|
|
}
|
|
draw(ctx, area) {
|
|
const options = this.options;
|
|
if (this.skip || options.radius < 0.1 || !_isPointInArea(this, area, this.size(options) / 2)) {
|
|
return;
|
|
}
|
|
ctx.strokeStyle = options.borderColor;
|
|
ctx.lineWidth = options.borderWidth;
|
|
ctx.fillStyle = options.backgroundColor;
|
|
drawPoint(ctx, options, this.x, this.y);
|
|
}
|
|
getRange() {
|
|
const options = this.options || {};
|
|
// @ts-expect-error Fallbacks should never be hit in practice
|
|
return options.radius + options.hitRadius;
|
|
}
|
|
}
|
|
|
|
function getBarBounds(bar, useFinalPosition) {
|
|
const { x , y , base , width , height } = bar.getProps([
|
|
'x',
|
|
'y',
|
|
'base',
|
|
'width',
|
|
'height'
|
|
], useFinalPosition);
|
|
let left, right, top, bottom, half;
|
|
if (bar.horizontal) {
|
|
half = height / 2;
|
|
left = Math.min(x, base);
|
|
right = Math.max(x, base);
|
|
top = y - half;
|
|
bottom = y + half;
|
|
} else {
|
|
half = width / 2;
|
|
left = x - half;
|
|
right = x + half;
|
|
top = Math.min(y, base);
|
|
bottom = Math.max(y, base);
|
|
}
|
|
return {
|
|
left,
|
|
top,
|
|
right,
|
|
bottom
|
|
};
|
|
}
|
|
function skipOrLimit(skip, value, min, max) {
|
|
return skip ? 0 : _limitValue(value, min, max);
|
|
}
|
|
function parseBorderWidth(bar, maxW, maxH) {
|
|
const value = bar.options.borderWidth;
|
|
const skip = bar.borderSkipped;
|
|
const o = toTRBL(value);
|
|
return {
|
|
t: skipOrLimit(skip.top, o.top, 0, maxH),
|
|
r: skipOrLimit(skip.right, o.right, 0, maxW),
|
|
b: skipOrLimit(skip.bottom, o.bottom, 0, maxH),
|
|
l: skipOrLimit(skip.left, o.left, 0, maxW)
|
|
};
|
|
}
|
|
function parseBorderRadius(bar, maxW, maxH) {
|
|
const { enableBorderRadius } = bar.getProps([
|
|
'enableBorderRadius'
|
|
]);
|
|
const value = bar.options.borderRadius;
|
|
const o = toTRBLCorners(value);
|
|
const maxR = Math.min(maxW, maxH);
|
|
const skip = bar.borderSkipped;
|
|
const enableBorder = enableBorderRadius || isObject(value);
|
|
return {
|
|
topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR),
|
|
topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR),
|
|
bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR),
|
|
bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR)
|
|
};
|
|
}
|
|
function boundingRects(bar) {
|
|
const bounds = getBarBounds(bar);
|
|
const width = bounds.right - bounds.left;
|
|
const height = bounds.bottom - bounds.top;
|
|
const border = parseBorderWidth(bar, width / 2, height / 2);
|
|
const radius = parseBorderRadius(bar, width / 2, height / 2);
|
|
return {
|
|
outer: {
|
|
x: bounds.left,
|
|
y: bounds.top,
|
|
w: width,
|
|
h: height,
|
|
radius
|
|
},
|
|
inner: {
|
|
x: bounds.left + border.l,
|
|
y: bounds.top + border.t,
|
|
w: width - border.l - border.r,
|
|
h: height - border.t - border.b,
|
|
radius: {
|
|
topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
|
|
topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
|
|
bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
|
|
bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r))
|
|
}
|
|
}
|
|
};
|
|
}
|
|
function inRange(bar, x, y, useFinalPosition) {
|
|
const skipX = x === null;
|
|
const skipY = y === null;
|
|
const skipBoth = skipX && skipY;
|
|
const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition);
|
|
return bounds && (skipX || _isBetween(x, bounds.left, bounds.right)) && (skipY || _isBetween(y, bounds.top, bounds.bottom));
|
|
}
|
|
function hasRadius(radius) {
|
|
return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
|
|
}
|
|
function addNormalRectPath(ctx, rect) {
|
|
ctx.rect(rect.x, rect.y, rect.w, rect.h);
|
|
}
|
|
function inflateRect(rect, amount, refRect = {}) {
|
|
const x = rect.x !== refRect.x ? -amount : 0;
|
|
const y = rect.y !== refRect.y ? -amount : 0;
|
|
const w = (rect.x + rect.w !== refRect.x + refRect.w ? amount : 0) - x;
|
|
const h = (rect.y + rect.h !== refRect.y + refRect.h ? amount : 0) - y;
|
|
return {
|
|
x: rect.x + x,
|
|
y: rect.y + y,
|
|
w: rect.w + w,
|
|
h: rect.h + h,
|
|
radius: rect.radius
|
|
};
|
|
}
|
|
class BarElement extends chart_Element {
|
|
static id = 'bar';
|
|
static defaults = {
|
|
borderSkipped: 'start',
|
|
borderWidth: 0,
|
|
borderRadius: 0,
|
|
inflateAmount: 'auto',
|
|
pointStyle: undefined
|
|
};
|
|
static defaultRoutes = {
|
|
backgroundColor: 'backgroundColor',
|
|
borderColor: 'borderColor'
|
|
};
|
|
constructor(cfg){
|
|
super();
|
|
this.options = undefined;
|
|
this.horizontal = undefined;
|
|
this.base = undefined;
|
|
this.width = undefined;
|
|
this.height = undefined;
|
|
this.inflateAmount = undefined;
|
|
if (cfg) {
|
|
Object.assign(this, cfg);
|
|
}
|
|
}
|
|
draw(ctx) {
|
|
const { inflateAmount , options: { borderColor , backgroundColor } } = this;
|
|
const { inner , outer } = boundingRects(this);
|
|
const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
|
|
ctx.save();
|
|
if (outer.w !== inner.w || outer.h !== inner.h) {
|
|
ctx.beginPath();
|
|
addRectPath(ctx, inflateRect(outer, inflateAmount, inner));
|
|
ctx.clip();
|
|
addRectPath(ctx, inflateRect(inner, -inflateAmount, outer));
|
|
ctx.fillStyle = borderColor;
|
|
ctx.fill('evenodd');
|
|
}
|
|
ctx.beginPath();
|
|
addRectPath(ctx, inflateRect(inner, inflateAmount));
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
return inRange(this, mouseX, mouseY, useFinalPosition);
|
|
}
|
|
inXRange(mouseX, useFinalPosition) {
|
|
return inRange(this, mouseX, null, useFinalPosition);
|
|
}
|
|
inYRange(mouseY, useFinalPosition) {
|
|
return inRange(this, null, mouseY, useFinalPosition);
|
|
}
|
|
getCenterPoint(useFinalPosition) {
|
|
const { x , y , base , horizontal } = this.getProps([
|
|
'x',
|
|
'y',
|
|
'base',
|
|
'horizontal'
|
|
], useFinalPosition);
|
|
return {
|
|
x: horizontal ? (x + base) / 2 : x,
|
|
y: horizontal ? y : (y + base) / 2
|
|
};
|
|
}
|
|
getRange(axis) {
|
|
return axis === 'x' ? this.width / 2 : this.height / 2;
|
|
}
|
|
}
|
|
|
|
var chart_elements = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
ArcElement: ArcElement,
|
|
BarElement: BarElement,
|
|
LineElement: LineElement,
|
|
PointElement: PointElement
|
|
});
|
|
|
|
const BORDER_COLORS = [
|
|
'rgb(54, 162, 235)',
|
|
'rgb(255, 99, 132)',
|
|
'rgb(255, 159, 64)',
|
|
'rgb(255, 205, 86)',
|
|
'rgb(75, 192, 192)',
|
|
'rgb(153, 102, 255)',
|
|
'rgb(201, 203, 207)' // grey
|
|
];
|
|
// Border colors with 50% transparency
|
|
const BACKGROUND_COLORS = /* #__PURE__ */ BORDER_COLORS.map((color)=>color.replace('rgb(', 'rgba(').replace(')', ', 0.5)'));
|
|
function getBorderColor(i) {
|
|
return BORDER_COLORS[i % BORDER_COLORS.length];
|
|
}
|
|
function getBackgroundColor(i) {
|
|
return BACKGROUND_COLORS[i % BACKGROUND_COLORS.length];
|
|
}
|
|
function colorizeDefaultDataset(dataset, i) {
|
|
dataset.borderColor = getBorderColor(i);
|
|
dataset.backgroundColor = getBackgroundColor(i);
|
|
return ++i;
|
|
}
|
|
function colorizeDoughnutDataset(dataset, i) {
|
|
dataset.backgroundColor = dataset.data.map(()=>getBorderColor(i++));
|
|
return i;
|
|
}
|
|
function colorizePolarAreaDataset(dataset, i) {
|
|
dataset.backgroundColor = dataset.data.map(()=>getBackgroundColor(i++));
|
|
return i;
|
|
}
|
|
function getColorizer(chart) {
|
|
let i = 0;
|
|
return (dataset, datasetIndex)=>{
|
|
const controller = chart.getDatasetMeta(datasetIndex).controller;
|
|
if (controller instanceof DoughnutController) {
|
|
i = colorizeDoughnutDataset(dataset, i);
|
|
} else if (controller instanceof PolarAreaController) {
|
|
i = colorizePolarAreaDataset(dataset, i);
|
|
} else if (controller) {
|
|
i = colorizeDefaultDataset(dataset, i);
|
|
}
|
|
};
|
|
}
|
|
function containsColorsDefinitions(descriptors) {
|
|
let k;
|
|
for(k in descriptors){
|
|
if (descriptors[k].borderColor || descriptors[k].backgroundColor) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function containsColorsDefinition(descriptor) {
|
|
return descriptor && (descriptor.borderColor || descriptor.backgroundColor);
|
|
}
|
|
function containsDefaultColorsDefenitions() {
|
|
return defaults.borderColor !== 'rgba(0,0,0,0.1)' || defaults.backgroundColor !== 'rgba(0,0,0,0.1)';
|
|
}
|
|
var plugin_colors = {
|
|
id: 'colors',
|
|
defaults: {
|
|
enabled: true,
|
|
forceOverride: false
|
|
},
|
|
beforeLayout (chart, _args, options) {
|
|
if (!options.enabled) {
|
|
return;
|
|
}
|
|
const { data: { datasets } , options: chartOptions } = chart.config;
|
|
const { elements } = chartOptions;
|
|
const containsColorDefenition = containsColorsDefinitions(datasets) || containsColorsDefinition(chartOptions) || elements && containsColorsDefinitions(elements) || containsDefaultColorsDefenitions();
|
|
if (!options.forceOverride && containsColorDefenition) {
|
|
return;
|
|
}
|
|
const colorizer = getColorizer(chart);
|
|
datasets.forEach(colorizer);
|
|
}
|
|
};
|
|
|
|
function lttbDecimation(data, start, count, availableWidth, options) {
|
|
const samples = options.samples || availableWidth;
|
|
if (samples >= count) {
|
|
return data.slice(start, start + count);
|
|
}
|
|
const decimated = [];
|
|
const bucketWidth = (count - 2) / (samples - 2);
|
|
let sampledIndex = 0;
|
|
const endIndex = start + count - 1;
|
|
let a = start;
|
|
let i, maxAreaPoint, maxArea, area, nextA;
|
|
decimated[sampledIndex++] = data[a];
|
|
for(i = 0; i < samples - 2; i++){
|
|
let avgX = 0;
|
|
let avgY = 0;
|
|
let j;
|
|
const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start;
|
|
const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start;
|
|
const avgRangeLength = avgRangeEnd - avgRangeStart;
|
|
for(j = avgRangeStart; j < avgRangeEnd; j++){
|
|
avgX += data[j].x;
|
|
avgY += data[j].y;
|
|
}
|
|
avgX /= avgRangeLength;
|
|
avgY /= avgRangeLength;
|
|
const rangeOffs = Math.floor(i * bucketWidth) + 1 + start;
|
|
const rangeTo = Math.min(Math.floor((i + 1) * bucketWidth) + 1, count) + start;
|
|
const { x: pointAx , y: pointAy } = data[a];
|
|
maxArea = area = -1;
|
|
for(j = rangeOffs; j < rangeTo; j++){
|
|
area = 0.5 * Math.abs((pointAx - avgX) * (data[j].y - pointAy) - (pointAx - data[j].x) * (avgY - pointAy));
|
|
if (area > maxArea) {
|
|
maxArea = area;
|
|
maxAreaPoint = data[j];
|
|
nextA = j;
|
|
}
|
|
}
|
|
decimated[sampledIndex++] = maxAreaPoint;
|
|
a = nextA;
|
|
}
|
|
decimated[sampledIndex++] = data[endIndex];
|
|
return decimated;
|
|
}
|
|
function minMaxDecimation(data, start, count, availableWidth) {
|
|
let avgX = 0;
|
|
let countX = 0;
|
|
let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY;
|
|
const decimated = [];
|
|
const endIndex = start + count - 1;
|
|
const xMin = data[start].x;
|
|
const xMax = data[endIndex].x;
|
|
const dx = xMax - xMin;
|
|
for(i = start; i < start + count; ++i){
|
|
point = data[i];
|
|
x = (point.x - xMin) / dx * availableWidth;
|
|
y = point.y;
|
|
const truncX = x | 0;
|
|
if (truncX === prevX) {
|
|
if (y < minY) {
|
|
minY = y;
|
|
minIndex = i;
|
|
} else if (y > maxY) {
|
|
maxY = y;
|
|
maxIndex = i;
|
|
}
|
|
avgX = (countX * avgX + point.x) / ++countX;
|
|
} else {
|
|
const lastIndex = i - 1;
|
|
if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) {
|
|
const intermediateIndex1 = Math.min(minIndex, maxIndex);
|
|
const intermediateIndex2 = Math.max(minIndex, maxIndex);
|
|
if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) {
|
|
decimated.push({
|
|
...data[intermediateIndex1],
|
|
x: avgX
|
|
});
|
|
}
|
|
if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) {
|
|
decimated.push({
|
|
...data[intermediateIndex2],
|
|
x: avgX
|
|
});
|
|
}
|
|
}
|
|
if (i > 0 && lastIndex !== startIndex) {
|
|
decimated.push(data[lastIndex]);
|
|
}
|
|
decimated.push(point);
|
|
prevX = truncX;
|
|
countX = 0;
|
|
minY = maxY = y;
|
|
minIndex = maxIndex = startIndex = i;
|
|
}
|
|
}
|
|
return decimated;
|
|
}
|
|
function cleanDecimatedDataset(dataset) {
|
|
if (dataset._decimated) {
|
|
const data = dataset._data;
|
|
delete dataset._decimated;
|
|
delete dataset._data;
|
|
Object.defineProperty(dataset, 'data', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: data
|
|
});
|
|
}
|
|
}
|
|
function cleanDecimatedData(chart) {
|
|
chart.data.datasets.forEach((dataset)=>{
|
|
cleanDecimatedDataset(dataset);
|
|
});
|
|
}
|
|
function getStartAndCountOfVisiblePointsSimplified(meta, points) {
|
|
const pointCount = points.length;
|
|
let start = 0;
|
|
let count;
|
|
const { iScale } = meta;
|
|
const { min , max , minDefined , maxDefined } = iScale.getUserBounds();
|
|
if (minDefined) {
|
|
start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1);
|
|
}
|
|
if (maxDefined) {
|
|
count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start;
|
|
} else {
|
|
count = pointCount - start;
|
|
}
|
|
return {
|
|
start,
|
|
count
|
|
};
|
|
}
|
|
var plugin_decimation = {
|
|
id: 'decimation',
|
|
defaults: {
|
|
algorithm: 'min-max',
|
|
enabled: false
|
|
},
|
|
beforeElementsUpdate: (chart, args, options)=>{
|
|
if (!options.enabled) {
|
|
cleanDecimatedData(chart);
|
|
return;
|
|
}
|
|
const availableWidth = chart.width;
|
|
chart.data.datasets.forEach((dataset, datasetIndex)=>{
|
|
const { _data , indexAxis } = dataset;
|
|
const meta = chart.getDatasetMeta(datasetIndex);
|
|
const data = _data || dataset.data;
|
|
if (resolve([
|
|
indexAxis,
|
|
chart.options.indexAxis
|
|
]) === 'y') {
|
|
return;
|
|
}
|
|
if (!meta.controller.supportsDecimation) {
|
|
return;
|
|
}
|
|
const xAxis = chart.scales[meta.xAxisID];
|
|
if (xAxis.type !== 'linear' && xAxis.type !== 'time') {
|
|
return;
|
|
}
|
|
if (chart.options.parsing) {
|
|
return;
|
|
}
|
|
let { start , count } = getStartAndCountOfVisiblePointsSimplified(meta, data);
|
|
const threshold = options.threshold || 4 * availableWidth;
|
|
if (count <= threshold) {
|
|
cleanDecimatedDataset(dataset);
|
|
return;
|
|
}
|
|
if (isNullOrUndef(_data)) {
|
|
dataset._data = data;
|
|
delete dataset.data;
|
|
Object.defineProperty(dataset, 'data', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function() {
|
|
return this._decimated;
|
|
},
|
|
set: function(d) {
|
|
this._data = d;
|
|
}
|
|
});
|
|
}
|
|
let decimated;
|
|
switch(options.algorithm){
|
|
case 'lttb':
|
|
decimated = lttbDecimation(data, start, count, availableWidth, options);
|
|
break;
|
|
case 'min-max':
|
|
decimated = minMaxDecimation(data, start, count, availableWidth);
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`);
|
|
}
|
|
dataset._decimated = decimated;
|
|
});
|
|
},
|
|
destroy (chart) {
|
|
cleanDecimatedData(chart);
|
|
}
|
|
};
|
|
|
|
function _segments(line, target, property) {
|
|
const segments = line.segments;
|
|
const points = line.points;
|
|
const tpoints = target.points;
|
|
const parts = [];
|
|
for (const segment of segments){
|
|
let { start , end } = segment;
|
|
end = _findSegmentEnd(start, end, points);
|
|
const bounds = _getBounds(property, points[start], points[end], segment.loop);
|
|
if (!target.segments) {
|
|
parts.push({
|
|
source: segment,
|
|
target: bounds,
|
|
start: points[start],
|
|
end: points[end]
|
|
});
|
|
continue;
|
|
}
|
|
const targetSegments = _boundSegments(target, bounds);
|
|
for (const tgt of targetSegments){
|
|
const subBounds = _getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop);
|
|
const fillSources = _boundSegment(segment, points, subBounds);
|
|
for (const fillSource of fillSources){
|
|
parts.push({
|
|
source: fillSource,
|
|
target: tgt,
|
|
start: {
|
|
[property]: _getEdge(bounds, subBounds, 'start', Math.max)
|
|
},
|
|
end: {
|
|
[property]: _getEdge(bounds, subBounds, 'end', Math.min)
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return parts;
|
|
}
|
|
function _getBounds(property, first, last, loop) {
|
|
if (loop) {
|
|
return;
|
|
}
|
|
let start = first[property];
|
|
let end = last[property];
|
|
if (property === 'angle') {
|
|
start = _normalizeAngle(start);
|
|
end = _normalizeAngle(end);
|
|
}
|
|
return {
|
|
property,
|
|
start,
|
|
end
|
|
};
|
|
}
|
|
function _pointsFromSegments(boundary, line) {
|
|
const { x =null , y =null } = boundary || {};
|
|
const linePoints = line.points;
|
|
const points = [];
|
|
line.segments.forEach(({ start , end })=>{
|
|
end = _findSegmentEnd(start, end, linePoints);
|
|
const first = linePoints[start];
|
|
const last = linePoints[end];
|
|
if (y !== null) {
|
|
points.push({
|
|
x: first.x,
|
|
y
|
|
});
|
|
points.push({
|
|
x: last.x,
|
|
y
|
|
});
|
|
} else if (x !== null) {
|
|
points.push({
|
|
x,
|
|
y: first.y
|
|
});
|
|
points.push({
|
|
x,
|
|
y: last.y
|
|
});
|
|
}
|
|
});
|
|
return points;
|
|
}
|
|
function _findSegmentEnd(start, end, points) {
|
|
for(; end > start; end--){
|
|
const point = points[end];
|
|
if (!isNaN(point.x) && !isNaN(point.y)) {
|
|
break;
|
|
}
|
|
}
|
|
return end;
|
|
}
|
|
function _getEdge(a, b, prop, fn) {
|
|
if (a && b) {
|
|
return fn(a[prop], b[prop]);
|
|
}
|
|
return a ? a[prop] : b ? b[prop] : 0;
|
|
}
|
|
|
|
function _createBoundaryLine(boundary, line) {
|
|
let points = [];
|
|
let _loop = false;
|
|
if (isArray(boundary)) {
|
|
_loop = true;
|
|
points = boundary;
|
|
} else {
|
|
points = _pointsFromSegments(boundary, line);
|
|
}
|
|
return points.length ? new LineElement({
|
|
points,
|
|
options: {
|
|
tension: 0
|
|
},
|
|
_loop,
|
|
_fullLoop: _loop
|
|
}) : null;
|
|
}
|
|
function _shouldApplyFill(source) {
|
|
return source && source.fill !== false;
|
|
}
|
|
|
|
function _resolveTarget(sources, index, propagate) {
|
|
const source = sources[index];
|
|
let fill = source.fill;
|
|
const visited = [
|
|
index
|
|
];
|
|
let target;
|
|
if (!propagate) {
|
|
return fill;
|
|
}
|
|
while(fill !== false && visited.indexOf(fill) === -1){
|
|
if (!isNumberFinite(fill)) {
|
|
return fill;
|
|
}
|
|
target = sources[fill];
|
|
if (!target) {
|
|
return false;
|
|
}
|
|
if (target.visible) {
|
|
return fill;
|
|
}
|
|
visited.push(fill);
|
|
fill = target.fill;
|
|
}
|
|
return false;
|
|
}
|
|
function _decodeFill(line, index, count) {
|
|
const fill = parseFillOption(line);
|
|
if (isObject(fill)) {
|
|
return isNaN(fill.value) ? false : fill;
|
|
}
|
|
let target = parseFloat(fill);
|
|
if (isNumberFinite(target) && Math.floor(target) === target) {
|
|
return decodeTargetIndex(fill[0], index, target, count);
|
|
}
|
|
return [
|
|
'origin',
|
|
'start',
|
|
'end',
|
|
'stack',
|
|
'shape'
|
|
].indexOf(fill) >= 0 && fill;
|
|
}
|
|
function decodeTargetIndex(firstCh, index, target, count) {
|
|
if (firstCh === '-' || firstCh === '+') {
|
|
target = index + target;
|
|
}
|
|
if (target === index || target < 0 || target >= count) {
|
|
return false;
|
|
}
|
|
return target;
|
|
}
|
|
function _getTargetPixel(fill, scale) {
|
|
let pixel = null;
|
|
if (fill === 'start') {
|
|
pixel = scale.bottom;
|
|
} else if (fill === 'end') {
|
|
pixel = scale.top;
|
|
} else if (isObject(fill)) {
|
|
pixel = scale.getPixelForValue(fill.value);
|
|
} else if (scale.getBasePixel) {
|
|
pixel = scale.getBasePixel();
|
|
}
|
|
return pixel;
|
|
}
|
|
function _getTargetValue(fill, scale, startValue) {
|
|
let value;
|
|
if (fill === 'start') {
|
|
value = startValue;
|
|
} else if (fill === 'end') {
|
|
value = scale.options.reverse ? scale.min : scale.max;
|
|
} else if (isObject(fill)) {
|
|
value = fill.value;
|
|
} else {
|
|
value = scale.getBaseValue();
|
|
}
|
|
return value;
|
|
}
|
|
function parseFillOption(line) {
|
|
const options = line.options;
|
|
const fillOption = options.fill;
|
|
let fill = valueOrDefault(fillOption && fillOption.target, fillOption);
|
|
if (fill === undefined) {
|
|
fill = !!options.backgroundColor;
|
|
}
|
|
if (fill === false || fill === null) {
|
|
return false;
|
|
}
|
|
if (fill === true) {
|
|
return 'origin';
|
|
}
|
|
return fill;
|
|
}
|
|
|
|
function _buildStackLine(source) {
|
|
const { scale , index , line } = source;
|
|
const points = [];
|
|
const segments = line.segments;
|
|
const sourcePoints = line.points;
|
|
const linesBelow = getLinesBelow(scale, index);
|
|
linesBelow.push(_createBoundaryLine({
|
|
x: null,
|
|
y: scale.bottom
|
|
}, line));
|
|
for(let i = 0; i < segments.length; i++){
|
|
const segment = segments[i];
|
|
for(let j = segment.start; j <= segment.end; j++){
|
|
addPointsBelow(points, sourcePoints[j], linesBelow);
|
|
}
|
|
}
|
|
return new LineElement({
|
|
points,
|
|
options: {}
|
|
});
|
|
}
|
|
function getLinesBelow(scale, index) {
|
|
const below = [];
|
|
const metas = scale.getMatchingVisibleMetas('line');
|
|
for(let i = 0; i < metas.length; i++){
|
|
const meta = metas[i];
|
|
if (meta.index === index) {
|
|
break;
|
|
}
|
|
if (!meta.hidden) {
|
|
below.unshift(meta.dataset);
|
|
}
|
|
}
|
|
return below;
|
|
}
|
|
function addPointsBelow(points, sourcePoint, linesBelow) {
|
|
const postponed = [];
|
|
for(let j = 0; j < linesBelow.length; j++){
|
|
const line = linesBelow[j];
|
|
const { first , last , point } = findPoint(line, sourcePoint, 'x');
|
|
if (!point || first && last) {
|
|
continue;
|
|
}
|
|
if (first) {
|
|
postponed.unshift(point);
|
|
} else {
|
|
points.push(point);
|
|
if (!last) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
points.push(...postponed);
|
|
}
|
|
function findPoint(line, sourcePoint, property) {
|
|
const point = line.interpolate(sourcePoint, property);
|
|
if (!point) {
|
|
return {};
|
|
}
|
|
const pointValue = point[property];
|
|
const segments = line.segments;
|
|
const linePoints = line.points;
|
|
let first = false;
|
|
let last = false;
|
|
for(let i = 0; i < segments.length; i++){
|
|
const segment = segments[i];
|
|
const firstValue = linePoints[segment.start][property];
|
|
const lastValue = linePoints[segment.end][property];
|
|
if (_isBetween(pointValue, firstValue, lastValue)) {
|
|
first = pointValue === firstValue;
|
|
last = pointValue === lastValue;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
first,
|
|
last,
|
|
point
|
|
};
|
|
}
|
|
|
|
class simpleArc {
|
|
constructor(opts){
|
|
this.x = opts.x;
|
|
this.y = opts.y;
|
|
this.radius = opts.radius;
|
|
}
|
|
pathSegment(ctx, bounds, opts) {
|
|
const { x , y , radius } = this;
|
|
bounds = bounds || {
|
|
start: 0,
|
|
end: TAU
|
|
};
|
|
ctx.arc(x, y, radius, bounds.end, bounds.start, true);
|
|
return !opts.bounds;
|
|
}
|
|
interpolate(point) {
|
|
const { x , y , radius } = this;
|
|
const angle = point.angle;
|
|
return {
|
|
x: x + Math.cos(angle) * radius,
|
|
y: y + Math.sin(angle) * radius,
|
|
angle
|
|
};
|
|
}
|
|
}
|
|
|
|
function _getTarget(source) {
|
|
const { chart , fill , line } = source;
|
|
if (isNumberFinite(fill)) {
|
|
return getLineByIndex(chart, fill);
|
|
}
|
|
if (fill === 'stack') {
|
|
return _buildStackLine(source);
|
|
}
|
|
if (fill === 'shape') {
|
|
return true;
|
|
}
|
|
const boundary = computeBoundary(source);
|
|
if (boundary instanceof simpleArc) {
|
|
return boundary;
|
|
}
|
|
return _createBoundaryLine(boundary, line);
|
|
}
|
|
function getLineByIndex(chart, index) {
|
|
const meta = chart.getDatasetMeta(index);
|
|
const visible = meta && chart.isDatasetVisible(index);
|
|
return visible ? meta.dataset : null;
|
|
}
|
|
function computeBoundary(source) {
|
|
const scale = source.scale || {};
|
|
if (scale.getPointPositionForValue) {
|
|
return computeCircularBoundary(source);
|
|
}
|
|
return computeLinearBoundary(source);
|
|
}
|
|
function computeLinearBoundary(source) {
|
|
const { scale ={} , fill } = source;
|
|
const pixel = _getTargetPixel(fill, scale);
|
|
if (isNumberFinite(pixel)) {
|
|
const horizontal = scale.isHorizontal();
|
|
return {
|
|
x: horizontal ? pixel : null,
|
|
y: horizontal ? null : pixel
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function computeCircularBoundary(source) {
|
|
const { scale , fill } = source;
|
|
const options = scale.options;
|
|
const length = scale.getLabels().length;
|
|
const start = options.reverse ? scale.max : scale.min;
|
|
const value = _getTargetValue(fill, scale, start);
|
|
const target = [];
|
|
if (options.grid.circular) {
|
|
const center = scale.getPointPositionForValue(0, start);
|
|
return new simpleArc({
|
|
x: center.x,
|
|
y: center.y,
|
|
radius: scale.getDistanceFromCenterForValue(value)
|
|
});
|
|
}
|
|
for(let i = 0; i < length; ++i){
|
|
target.push(scale.getPointPositionForValue(i, value));
|
|
}
|
|
return target;
|
|
}
|
|
|
|
function _drawfill(ctx, source, area) {
|
|
const target = _getTarget(source);
|
|
const { chart , index , line , scale , axis } = source;
|
|
const lineOpts = line.options;
|
|
const fillOption = lineOpts.fill;
|
|
const color = lineOpts.backgroundColor;
|
|
const { above =color , below =color } = fillOption || {};
|
|
const meta = chart.getDatasetMeta(index);
|
|
const clip = getDatasetClipArea(chart, meta);
|
|
if (target && line.points.length) {
|
|
clipArea(ctx, area);
|
|
doFill(ctx, {
|
|
line,
|
|
target,
|
|
above,
|
|
below,
|
|
area,
|
|
scale,
|
|
axis,
|
|
clip
|
|
});
|
|
unclipArea(ctx);
|
|
}
|
|
}
|
|
function doFill(ctx, cfg) {
|
|
const { line , target , above , below , area , scale , clip } = cfg;
|
|
const property = line._loop ? 'angle' : cfg.axis;
|
|
ctx.save();
|
|
let fillColor = below;
|
|
if (below !== above) {
|
|
if (property === 'x') {
|
|
clipVertical(ctx, target, area.top);
|
|
fill(ctx, {
|
|
line,
|
|
target,
|
|
color: above,
|
|
scale,
|
|
property,
|
|
clip
|
|
});
|
|
ctx.restore();
|
|
ctx.save();
|
|
clipVertical(ctx, target, area.bottom);
|
|
} else if (property === 'y') {
|
|
clipHorizontal(ctx, target, area.left);
|
|
fill(ctx, {
|
|
line,
|
|
target,
|
|
color: below,
|
|
scale,
|
|
property,
|
|
clip
|
|
});
|
|
ctx.restore();
|
|
ctx.save();
|
|
clipHorizontal(ctx, target, area.right);
|
|
fillColor = above;
|
|
}
|
|
}
|
|
fill(ctx, {
|
|
line,
|
|
target,
|
|
color: fillColor,
|
|
scale,
|
|
property,
|
|
clip
|
|
});
|
|
ctx.restore();
|
|
}
|
|
function clipVertical(ctx, target, clipY) {
|
|
const { segments , points } = target;
|
|
let first = true;
|
|
let lineLoop = false;
|
|
ctx.beginPath();
|
|
for (const segment of segments){
|
|
const { start , end } = segment;
|
|
const firstPoint = points[start];
|
|
const lastPoint = points[_findSegmentEnd(start, end, points)];
|
|
if (first) {
|
|
ctx.moveTo(firstPoint.x, firstPoint.y);
|
|
first = false;
|
|
} else {
|
|
ctx.lineTo(firstPoint.x, clipY);
|
|
ctx.lineTo(firstPoint.x, firstPoint.y);
|
|
}
|
|
lineLoop = !!target.pathSegment(ctx, segment, {
|
|
move: lineLoop
|
|
});
|
|
if (lineLoop) {
|
|
ctx.closePath();
|
|
} else {
|
|
ctx.lineTo(lastPoint.x, clipY);
|
|
}
|
|
}
|
|
ctx.lineTo(target.first().x, clipY);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
}
|
|
function clipHorizontal(ctx, target, clipX) {
|
|
const { segments , points } = target;
|
|
let first = true;
|
|
let lineLoop = false;
|
|
ctx.beginPath();
|
|
for (const segment of segments){
|
|
const { start , end } = segment;
|
|
const firstPoint = points[start];
|
|
const lastPoint = points[_findSegmentEnd(start, end, points)];
|
|
if (first) {
|
|
ctx.moveTo(firstPoint.x, firstPoint.y);
|
|
first = false;
|
|
} else {
|
|
ctx.lineTo(clipX, firstPoint.y);
|
|
ctx.lineTo(firstPoint.x, firstPoint.y);
|
|
}
|
|
lineLoop = !!target.pathSegment(ctx, segment, {
|
|
move: lineLoop
|
|
});
|
|
if (lineLoop) {
|
|
ctx.closePath();
|
|
} else {
|
|
ctx.lineTo(clipX, lastPoint.y);
|
|
}
|
|
}
|
|
ctx.lineTo(clipX, target.first().y);
|
|
ctx.closePath();
|
|
ctx.clip();
|
|
}
|
|
function fill(ctx, cfg) {
|
|
const { line , target , property , color , scale , clip } = cfg;
|
|
const segments = _segments(line, target, property);
|
|
for (const { source: src , target: tgt , start , end } of segments){
|
|
const { style: { backgroundColor =color } = {} } = src;
|
|
const notShape = target !== true;
|
|
ctx.save();
|
|
ctx.fillStyle = backgroundColor;
|
|
clipBounds(ctx, scale, clip, notShape && _getBounds(property, start, end));
|
|
ctx.beginPath();
|
|
const lineLoop = !!line.pathSegment(ctx, src);
|
|
let loop;
|
|
if (notShape) {
|
|
if (lineLoop) {
|
|
ctx.closePath();
|
|
} else {
|
|
interpolatedLineTo(ctx, target, end, property);
|
|
}
|
|
const targetLoop = !!target.pathSegment(ctx, tgt, {
|
|
move: lineLoop,
|
|
reverse: true
|
|
});
|
|
loop = lineLoop && targetLoop;
|
|
if (!loop) {
|
|
interpolatedLineTo(ctx, target, start, property);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill(loop ? 'evenodd' : 'nonzero');
|
|
ctx.restore();
|
|
}
|
|
}
|
|
function clipBounds(ctx, scale, clip, bounds) {
|
|
const chartArea = scale.chart.chartArea;
|
|
const { property , start , end } = bounds || {};
|
|
if (property === 'x' || property === 'y') {
|
|
let left, top, right, bottom;
|
|
if (property === 'x') {
|
|
left = start;
|
|
top = chartArea.top;
|
|
right = end;
|
|
bottom = chartArea.bottom;
|
|
} else {
|
|
left = chartArea.left;
|
|
top = start;
|
|
right = chartArea.right;
|
|
bottom = end;
|
|
}
|
|
ctx.beginPath();
|
|
if (clip) {
|
|
left = Math.max(left, clip.left);
|
|
right = Math.min(right, clip.right);
|
|
top = Math.max(top, clip.top);
|
|
bottom = Math.min(bottom, clip.bottom);
|
|
}
|
|
ctx.rect(left, top, right - left, bottom - top);
|
|
ctx.clip();
|
|
}
|
|
}
|
|
function interpolatedLineTo(ctx, target, point, property) {
|
|
const interpolatedPoint = target.interpolate(point, property);
|
|
if (interpolatedPoint) {
|
|
ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y);
|
|
}
|
|
}
|
|
|
|
var index = {
|
|
id: 'filler',
|
|
afterDatasetsUpdate (chart, _args, options) {
|
|
const count = (chart.data.datasets || []).length;
|
|
const sources = [];
|
|
let meta, i, line, source;
|
|
for(i = 0; i < count; ++i){
|
|
meta = chart.getDatasetMeta(i);
|
|
line = meta.dataset;
|
|
source = null;
|
|
if (line && line.options && line instanceof LineElement) {
|
|
source = {
|
|
visible: chart.isDatasetVisible(i),
|
|
index: i,
|
|
fill: _decodeFill(line, i, count),
|
|
chart,
|
|
axis: meta.controller.options.indexAxis,
|
|
scale: meta.vScale,
|
|
line
|
|
};
|
|
}
|
|
meta.$filler = source;
|
|
sources.push(source);
|
|
}
|
|
for(i = 0; i < count; ++i){
|
|
source = sources[i];
|
|
if (!source || source.fill === false) {
|
|
continue;
|
|
}
|
|
source.fill = _resolveTarget(sources, i, options.propagate);
|
|
}
|
|
},
|
|
beforeDraw (chart, _args, options) {
|
|
const draw = options.drawTime === 'beforeDraw';
|
|
const metasets = chart.getSortedVisibleDatasetMetas();
|
|
const area = chart.chartArea;
|
|
for(let i = metasets.length - 1; i >= 0; --i){
|
|
const source = metasets[i].$filler;
|
|
if (!source) {
|
|
continue;
|
|
}
|
|
source.line.updateControlPoints(area, source.axis);
|
|
if (draw && source.fill) {
|
|
_drawfill(chart.ctx, source, area);
|
|
}
|
|
}
|
|
},
|
|
beforeDatasetsDraw (chart, _args, options) {
|
|
if (options.drawTime !== 'beforeDatasetsDraw') {
|
|
return;
|
|
}
|
|
const metasets = chart.getSortedVisibleDatasetMetas();
|
|
for(let i = metasets.length - 1; i >= 0; --i){
|
|
const source = metasets[i].$filler;
|
|
if (_shouldApplyFill(source)) {
|
|
_drawfill(chart.ctx, source, chart.chartArea);
|
|
}
|
|
}
|
|
},
|
|
beforeDatasetDraw (chart, args, options) {
|
|
const source = args.meta.$filler;
|
|
if (!_shouldApplyFill(source) || options.drawTime !== 'beforeDatasetDraw') {
|
|
return;
|
|
}
|
|
_drawfill(chart.ctx, source, chart.chartArea);
|
|
},
|
|
defaults: {
|
|
propagate: true,
|
|
drawTime: 'beforeDatasetDraw'
|
|
}
|
|
};
|
|
|
|
const getBoxSize = (labelOpts, fontSize)=>{
|
|
let { boxHeight =fontSize , boxWidth =fontSize } = labelOpts;
|
|
if (labelOpts.usePointStyle) {
|
|
boxHeight = Math.min(boxHeight, fontSize);
|
|
boxWidth = labelOpts.pointStyleWidth || Math.min(boxWidth, fontSize);
|
|
}
|
|
return {
|
|
boxWidth,
|
|
boxHeight,
|
|
itemHeight: Math.max(fontSize, boxHeight)
|
|
};
|
|
};
|
|
const itemsEqual = (a, b)=>a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index;
|
|
class Legend extends chart_Element {
|
|
constructor(config){
|
|
super();
|
|
this._added = false;
|
|
this.legendHitBoxes = [];
|
|
this._hoveredItem = null;
|
|
this.doughnutMode = false;
|
|
this.chart = config.chart;
|
|
this.options = config.options;
|
|
this.ctx = config.ctx;
|
|
this.legendItems = undefined;
|
|
this.columnSizes = undefined;
|
|
this.lineWidths = undefined;
|
|
this.maxHeight = undefined;
|
|
this.maxWidth = undefined;
|
|
this.top = undefined;
|
|
this.bottom = undefined;
|
|
this.left = undefined;
|
|
this.right = undefined;
|
|
this.height = undefined;
|
|
this.width = undefined;
|
|
this._margins = undefined;
|
|
this.position = undefined;
|
|
this.weight = undefined;
|
|
this.fullSize = undefined;
|
|
}
|
|
update(maxWidth, maxHeight, margins) {
|
|
this.maxWidth = maxWidth;
|
|
this.maxHeight = maxHeight;
|
|
this._margins = margins;
|
|
this.setDimensions();
|
|
this.buildLabels();
|
|
this.fit();
|
|
}
|
|
setDimensions() {
|
|
if (this.isHorizontal()) {
|
|
this.width = this.maxWidth;
|
|
this.left = this._margins.left;
|
|
this.right = this.width;
|
|
} else {
|
|
this.height = this.maxHeight;
|
|
this.top = this._margins.top;
|
|
this.bottom = this.height;
|
|
}
|
|
}
|
|
buildLabels() {
|
|
const labelOpts = this.options.labels || {};
|
|
let legendItems = callback(labelOpts.generateLabels, [
|
|
this.chart
|
|
], this) || [];
|
|
if (labelOpts.filter) {
|
|
legendItems = legendItems.filter((item)=>labelOpts.filter(item, this.chart.data));
|
|
}
|
|
if (labelOpts.sort) {
|
|
legendItems = legendItems.sort((a, b)=>labelOpts.sort(a, b, this.chart.data));
|
|
}
|
|
if (this.options.reverse) {
|
|
legendItems.reverse();
|
|
}
|
|
this.legendItems = legendItems;
|
|
}
|
|
fit() {
|
|
const { options , ctx } = this;
|
|
if (!options.display) {
|
|
this.width = this.height = 0;
|
|
return;
|
|
}
|
|
const labelOpts = options.labels;
|
|
const labelFont = toFont(labelOpts.font);
|
|
const fontSize = labelFont.size;
|
|
const titleHeight = this._computeTitleHeight();
|
|
const { boxWidth , itemHeight } = getBoxSize(labelOpts, fontSize);
|
|
let width, height;
|
|
ctx.font = labelFont.string;
|
|
if (this.isHorizontal()) {
|
|
width = this.maxWidth;
|
|
height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10;
|
|
} else {
|
|
height = this.maxHeight;
|
|
width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10;
|
|
}
|
|
this.width = Math.min(width, options.maxWidth || this.maxWidth);
|
|
this.height = Math.min(height, options.maxHeight || this.maxHeight);
|
|
}
|
|
_fitRows(titleHeight, fontSize, boxWidth, itemHeight) {
|
|
const { ctx , maxWidth , options: { labels: { padding } } } = this;
|
|
const hitboxes = this.legendHitBoxes = [];
|
|
const lineWidths = this.lineWidths = [
|
|
0
|
|
];
|
|
const lineHeight = itemHeight + padding;
|
|
let totalHeight = titleHeight;
|
|
ctx.textAlign = 'left';
|
|
ctx.textBaseline = 'middle';
|
|
let row = -1;
|
|
let top = -lineHeight;
|
|
this.legendItems.forEach((legendItem, i)=>{
|
|
const itemWidth = boxWidth + fontSize / 2 + ctx.measureText(legendItem.text).width;
|
|
if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) {
|
|
totalHeight += lineHeight;
|
|
lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0;
|
|
top += lineHeight;
|
|
row++;
|
|
}
|
|
hitboxes[i] = {
|
|
left: 0,
|
|
top,
|
|
row,
|
|
width: itemWidth,
|
|
height: itemHeight
|
|
};
|
|
lineWidths[lineWidths.length - 1] += itemWidth + padding;
|
|
});
|
|
return totalHeight;
|
|
}
|
|
_fitCols(titleHeight, labelFont, boxWidth, _itemHeight) {
|
|
const { ctx , maxHeight , options: { labels: { padding } } } = this;
|
|
const hitboxes = this.legendHitBoxes = [];
|
|
const columnSizes = this.columnSizes = [];
|
|
const heightLimit = maxHeight - titleHeight;
|
|
let totalWidth = padding;
|
|
let currentColWidth = 0;
|
|
let currentColHeight = 0;
|
|
let left = 0;
|
|
let col = 0;
|
|
this.legendItems.forEach((legendItem, i)=>{
|
|
const { itemWidth , itemHeight } = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight);
|
|
if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) {
|
|
totalWidth += currentColWidth + padding;
|
|
columnSizes.push({
|
|
width: currentColWidth,
|
|
height: currentColHeight
|
|
});
|
|
left += currentColWidth + padding;
|
|
col++;
|
|
currentColWidth = currentColHeight = 0;
|
|
}
|
|
hitboxes[i] = {
|
|
left,
|
|
top: currentColHeight,
|
|
col,
|
|
width: itemWidth,
|
|
height: itemHeight
|
|
};
|
|
currentColWidth = Math.max(currentColWidth, itemWidth);
|
|
currentColHeight += itemHeight + padding;
|
|
});
|
|
totalWidth += currentColWidth;
|
|
columnSizes.push({
|
|
width: currentColWidth,
|
|
height: currentColHeight
|
|
});
|
|
return totalWidth;
|
|
}
|
|
adjustHitBoxes() {
|
|
if (!this.options.display) {
|
|
return;
|
|
}
|
|
const titleHeight = this._computeTitleHeight();
|
|
const { legendHitBoxes: hitboxes , options: { align , labels: { padding } , rtl } } = this;
|
|
const rtlHelper = getRtlAdapter(rtl, this.left, this.width);
|
|
if (this.isHorizontal()) {
|
|
let row = 0;
|
|
let left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]);
|
|
for (const hitbox of hitboxes){
|
|
if (row !== hitbox.row) {
|
|
row = hitbox.row;
|
|
left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]);
|
|
}
|
|
hitbox.top += this.top + titleHeight + padding;
|
|
hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(left), hitbox.width);
|
|
left += hitbox.width + padding;
|
|
}
|
|
} else {
|
|
let col = 0;
|
|
let top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height);
|
|
for (const hitbox of hitboxes){
|
|
if (hitbox.col !== col) {
|
|
col = hitbox.col;
|
|
top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height);
|
|
}
|
|
hitbox.top = top;
|
|
hitbox.left += this.left + padding;
|
|
hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(hitbox.left), hitbox.width);
|
|
top += hitbox.height + padding;
|
|
}
|
|
}
|
|
}
|
|
isHorizontal() {
|
|
return this.options.position === 'top' || this.options.position === 'bottom';
|
|
}
|
|
draw() {
|
|
if (this.options.display) {
|
|
const ctx = this.ctx;
|
|
clipArea(ctx, this);
|
|
this._draw();
|
|
unclipArea(ctx);
|
|
}
|
|
}
|
|
_draw() {
|
|
const { options: opts , columnSizes , lineWidths , ctx } = this;
|
|
const { align , labels: labelOpts } = opts;
|
|
const defaultColor = defaults.color;
|
|
const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width);
|
|
const labelFont = toFont(labelOpts.font);
|
|
const { padding } = labelOpts;
|
|
const fontSize = labelFont.size;
|
|
const halfFontSize = fontSize / 2;
|
|
let cursor;
|
|
this.drawTitle();
|
|
ctx.textAlign = rtlHelper.textAlign('left');
|
|
ctx.textBaseline = 'middle';
|
|
ctx.lineWidth = 0.5;
|
|
ctx.font = labelFont.string;
|
|
const { boxWidth , boxHeight , itemHeight } = getBoxSize(labelOpts, fontSize);
|
|
const drawLegendBox = function(x, y, legendItem) {
|
|
if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) {
|
|
return;
|
|
}
|
|
ctx.save();
|
|
const lineWidth = valueOrDefault(legendItem.lineWidth, 1);
|
|
ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor);
|
|
ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt');
|
|
ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0);
|
|
ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter');
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor);
|
|
ctx.setLineDash(valueOrDefault(legendItem.lineDash, []));
|
|
if (labelOpts.usePointStyle) {
|
|
const drawOptions = {
|
|
radius: boxHeight * Math.SQRT2 / 2,
|
|
pointStyle: legendItem.pointStyle,
|
|
rotation: legendItem.rotation,
|
|
borderWidth: lineWidth
|
|
};
|
|
const centerX = rtlHelper.xPlus(x, boxWidth / 2);
|
|
const centerY = y + halfFontSize;
|
|
drawPointLegend(ctx, drawOptions, centerX, centerY, labelOpts.pointStyleWidth && boxWidth);
|
|
} else {
|
|
const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0);
|
|
const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth);
|
|
const borderRadius = toTRBLCorners(legendItem.borderRadius);
|
|
ctx.beginPath();
|
|
if (Object.values(borderRadius).some((v)=>v !== 0)) {
|
|
addRoundedRectPath(ctx, {
|
|
x: xBoxLeft,
|
|
y: yBoxTop,
|
|
w: boxWidth,
|
|
h: boxHeight,
|
|
radius: borderRadius
|
|
});
|
|
} else {
|
|
ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight);
|
|
}
|
|
ctx.fill();
|
|
if (lineWidth !== 0) {
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
ctx.restore();
|
|
};
|
|
const fillText = function(x, y, legendItem) {
|
|
renderText(ctx, legendItem.text, x, y + itemHeight / 2, labelFont, {
|
|
strikethrough: legendItem.hidden,
|
|
textAlign: rtlHelper.textAlign(legendItem.textAlign)
|
|
});
|
|
};
|
|
const isHorizontal = this.isHorizontal();
|
|
const titleHeight = this._computeTitleHeight();
|
|
if (isHorizontal) {
|
|
cursor = {
|
|
x: _alignStartEnd(align, this.left + padding, this.right - lineWidths[0]),
|
|
y: this.top + padding + titleHeight,
|
|
line: 0
|
|
};
|
|
} else {
|
|
cursor = {
|
|
x: this.left + padding,
|
|
y: _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[0].height),
|
|
line: 0
|
|
};
|
|
}
|
|
overrideTextDirection(this.ctx, opts.textDirection);
|
|
const lineHeight = itemHeight + padding;
|
|
this.legendItems.forEach((legendItem, i)=>{
|
|
ctx.strokeStyle = legendItem.fontColor;
|
|
ctx.fillStyle = legendItem.fontColor;
|
|
const textWidth = ctx.measureText(legendItem.text).width;
|
|
const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign));
|
|
const width = boxWidth + halfFontSize + textWidth;
|
|
let x = cursor.x;
|
|
let y = cursor.y;
|
|
rtlHelper.setWidth(this.width);
|
|
if (isHorizontal) {
|
|
if (i > 0 && x + width + padding > this.right) {
|
|
y = cursor.y += lineHeight;
|
|
cursor.line++;
|
|
x = cursor.x = _alignStartEnd(align, this.left + padding, this.right - lineWidths[cursor.line]);
|
|
}
|
|
} else if (i > 0 && y + lineHeight > this.bottom) {
|
|
x = cursor.x = x + columnSizes[cursor.line].width + padding;
|
|
cursor.line++;
|
|
y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[cursor.line].height);
|
|
}
|
|
const realX = rtlHelper.x(x);
|
|
drawLegendBox(realX, y, legendItem);
|
|
x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : this.right, opts.rtl);
|
|
fillText(rtlHelper.x(x), y, legendItem);
|
|
if (isHorizontal) {
|
|
cursor.x += width + padding;
|
|
} else if (typeof legendItem.text !== 'string') {
|
|
const fontLineHeight = labelFont.lineHeight;
|
|
cursor.y += calculateLegendItemHeight(legendItem, fontLineHeight) + padding;
|
|
} else {
|
|
cursor.y += lineHeight;
|
|
}
|
|
});
|
|
restoreTextDirection(this.ctx, opts.textDirection);
|
|
}
|
|
drawTitle() {
|
|
const opts = this.options;
|
|
const titleOpts = opts.title;
|
|
const titleFont = toFont(titleOpts.font);
|
|
const titlePadding = toPadding(titleOpts.padding);
|
|
if (!titleOpts.display) {
|
|
return;
|
|
}
|
|
const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width);
|
|
const ctx = this.ctx;
|
|
const position = titleOpts.position;
|
|
const halfFontSize = titleFont.size / 2;
|
|
const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize;
|
|
let y;
|
|
let left = this.left;
|
|
let maxWidth = this.width;
|
|
if (this.isHorizontal()) {
|
|
maxWidth = Math.max(...this.lineWidths);
|
|
y = this.top + topPaddingPlusHalfFontSize;
|
|
left = _alignStartEnd(opts.align, left, this.right - maxWidth);
|
|
} else {
|
|
const maxHeight = this.columnSizes.reduce((acc, size)=>Math.max(acc, size.height), 0);
|
|
y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, this.top, this.bottom - maxHeight - opts.labels.padding - this._computeTitleHeight());
|
|
}
|
|
const x = _alignStartEnd(position, left, left + maxWidth);
|
|
ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position));
|
|
ctx.textBaseline = 'middle';
|
|
ctx.strokeStyle = titleOpts.color;
|
|
ctx.fillStyle = titleOpts.color;
|
|
ctx.font = titleFont.string;
|
|
renderText(ctx, titleOpts.text, x, y, titleFont);
|
|
}
|
|
_computeTitleHeight() {
|
|
const titleOpts = this.options.title;
|
|
const titleFont = toFont(titleOpts.font);
|
|
const titlePadding = toPadding(titleOpts.padding);
|
|
return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0;
|
|
}
|
|
_getLegendItemAt(x, y) {
|
|
let i, hitBox, lh;
|
|
if (_isBetween(x, this.left, this.right) && _isBetween(y, this.top, this.bottom)) {
|
|
lh = this.legendHitBoxes;
|
|
for(i = 0; i < lh.length; ++i){
|
|
hitBox = lh[i];
|
|
if (_isBetween(x, hitBox.left, hitBox.left + hitBox.width) && _isBetween(y, hitBox.top, hitBox.top + hitBox.height)) {
|
|
return this.legendItems[i];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
handleEvent(e) {
|
|
const opts = this.options;
|
|
if (!isListened(e.type, opts)) {
|
|
return;
|
|
}
|
|
const hoveredItem = this._getLegendItemAt(e.x, e.y);
|
|
if (e.type === 'mousemove' || e.type === 'mouseout') {
|
|
const previous = this._hoveredItem;
|
|
const sameItem = itemsEqual(previous, hoveredItem);
|
|
if (previous && !sameItem) {
|
|
callback(opts.onLeave, [
|
|
e,
|
|
previous,
|
|
this
|
|
], this);
|
|
}
|
|
this._hoveredItem = hoveredItem;
|
|
if (hoveredItem && !sameItem) {
|
|
callback(opts.onHover, [
|
|
e,
|
|
hoveredItem,
|
|
this
|
|
], this);
|
|
}
|
|
} else if (hoveredItem) {
|
|
callback(opts.onClick, [
|
|
e,
|
|
hoveredItem,
|
|
this
|
|
], this);
|
|
}
|
|
}
|
|
}
|
|
function calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) {
|
|
const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx);
|
|
const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight);
|
|
return {
|
|
itemWidth,
|
|
itemHeight
|
|
};
|
|
}
|
|
function calculateItemWidth(legendItem, boxWidth, labelFont, ctx) {
|
|
let legendItemText = legendItem.text;
|
|
if (legendItemText && typeof legendItemText !== 'string') {
|
|
legendItemText = legendItemText.reduce((a, b)=>a.length > b.length ? a : b);
|
|
}
|
|
return boxWidth + labelFont.size / 2 + ctx.measureText(legendItemText).width;
|
|
}
|
|
function calculateItemHeight(_itemHeight, legendItem, fontLineHeight) {
|
|
let itemHeight = _itemHeight;
|
|
if (typeof legendItem.text !== 'string') {
|
|
itemHeight = calculateLegendItemHeight(legendItem, fontLineHeight);
|
|
}
|
|
return itemHeight;
|
|
}
|
|
function calculateLegendItemHeight(legendItem, fontLineHeight) {
|
|
const labelHeight = legendItem.text ? legendItem.text.length : 0;
|
|
return fontLineHeight * labelHeight;
|
|
}
|
|
function isListened(type, opts) {
|
|
if ((type === 'mousemove' || type === 'mouseout') && (opts.onHover || opts.onLeave)) {
|
|
return true;
|
|
}
|
|
if (opts.onClick && (type === 'click' || type === 'mouseup')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
var plugin_legend = {
|
|
id: 'legend',
|
|
_element: Legend,
|
|
start (chart, _args, options) {
|
|
const legend = chart.legend = new Legend({
|
|
ctx: chart.ctx,
|
|
options,
|
|
chart
|
|
});
|
|
layouts.configure(chart, legend, options);
|
|
layouts.addBox(chart, legend);
|
|
},
|
|
stop (chart) {
|
|
layouts.removeBox(chart, chart.legend);
|
|
delete chart.legend;
|
|
},
|
|
beforeUpdate (chart, _args, options) {
|
|
const legend = chart.legend;
|
|
layouts.configure(chart, legend, options);
|
|
legend.options = options;
|
|
},
|
|
afterUpdate (chart) {
|
|
const legend = chart.legend;
|
|
legend.buildLabels();
|
|
legend.adjustHitBoxes();
|
|
},
|
|
afterEvent (chart, args) {
|
|
if (!args.replay) {
|
|
chart.legend.handleEvent(args.event);
|
|
}
|
|
},
|
|
defaults: {
|
|
display: true,
|
|
position: 'top',
|
|
align: 'center',
|
|
fullSize: true,
|
|
reverse: false,
|
|
weight: 1000,
|
|
onClick (e, legendItem, legend) {
|
|
const index = legendItem.datasetIndex;
|
|
const ci = legend.chart;
|
|
if (ci.isDatasetVisible(index)) {
|
|
ci.hide(index);
|
|
legendItem.hidden = true;
|
|
} else {
|
|
ci.show(index);
|
|
legendItem.hidden = false;
|
|
}
|
|
},
|
|
onHover: null,
|
|
onLeave: null,
|
|
labels: {
|
|
color: (ctx)=>ctx.chart.options.color,
|
|
boxWidth: 40,
|
|
padding: 10,
|
|
generateLabels (chart) {
|
|
const datasets = chart.data.datasets;
|
|
const { labels: { usePointStyle , pointStyle , textAlign , color , useBorderRadius , borderRadius } } = chart.legend.options;
|
|
return chart._getSortedDatasetMetas().map((meta)=>{
|
|
const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);
|
|
const borderWidth = toPadding(style.borderWidth);
|
|
return {
|
|
text: datasets[meta.index].label,
|
|
fillStyle: style.backgroundColor,
|
|
fontColor: color,
|
|
hidden: !meta.visible,
|
|
lineCap: style.borderCapStyle,
|
|
lineDash: style.borderDash,
|
|
lineDashOffset: style.borderDashOffset,
|
|
lineJoin: style.borderJoinStyle,
|
|
lineWidth: (borderWidth.width + borderWidth.height) / 4,
|
|
strokeStyle: style.borderColor,
|
|
pointStyle: pointStyle || style.pointStyle,
|
|
rotation: style.rotation,
|
|
textAlign: textAlign || style.textAlign,
|
|
borderRadius: useBorderRadius && (borderRadius || style.borderRadius),
|
|
datasetIndex: meta.index
|
|
};
|
|
}, this);
|
|
}
|
|
},
|
|
title: {
|
|
color: (ctx)=>ctx.chart.options.color,
|
|
display: false,
|
|
position: 'center',
|
|
text: ''
|
|
}
|
|
},
|
|
descriptors: {
|
|
_scriptable: (name)=>!name.startsWith('on'),
|
|
labels: {
|
|
_scriptable: (name)=>![
|
|
'generateLabels',
|
|
'filter',
|
|
'sort'
|
|
].includes(name)
|
|
}
|
|
}
|
|
};
|
|
|
|
class Title extends chart_Element {
|
|
constructor(config){
|
|
super();
|
|
this.chart = config.chart;
|
|
this.options = config.options;
|
|
this.ctx = config.ctx;
|
|
this._padding = undefined;
|
|
this.top = undefined;
|
|
this.bottom = undefined;
|
|
this.left = undefined;
|
|
this.right = undefined;
|
|
this.width = undefined;
|
|
this.height = undefined;
|
|
this.position = undefined;
|
|
this.weight = undefined;
|
|
this.fullSize = undefined;
|
|
}
|
|
update(maxWidth, maxHeight) {
|
|
const opts = this.options;
|
|
this.left = 0;
|
|
this.top = 0;
|
|
if (!opts.display) {
|
|
this.width = this.height = this.right = this.bottom = 0;
|
|
return;
|
|
}
|
|
this.width = this.right = maxWidth;
|
|
this.height = this.bottom = maxHeight;
|
|
const lineCount = isArray(opts.text) ? opts.text.length : 1;
|
|
this._padding = toPadding(opts.padding);
|
|
const textSize = lineCount * toFont(opts.font).lineHeight + this._padding.height;
|
|
if (this.isHorizontal()) {
|
|
this.height = textSize;
|
|
} else {
|
|
this.width = textSize;
|
|
}
|
|
}
|
|
isHorizontal() {
|
|
const pos = this.options.position;
|
|
return pos === 'top' || pos === 'bottom';
|
|
}
|
|
_drawArgs(offset) {
|
|
const { top , left , bottom , right , options } = this;
|
|
const align = options.align;
|
|
let rotation = 0;
|
|
let maxWidth, titleX, titleY;
|
|
if (this.isHorizontal()) {
|
|
titleX = _alignStartEnd(align, left, right);
|
|
titleY = top + offset;
|
|
maxWidth = right - left;
|
|
} else {
|
|
if (options.position === 'left') {
|
|
titleX = left + offset;
|
|
titleY = _alignStartEnd(align, bottom, top);
|
|
rotation = PI * -0.5;
|
|
} else {
|
|
titleX = right - offset;
|
|
titleY = _alignStartEnd(align, top, bottom);
|
|
rotation = PI * 0.5;
|
|
}
|
|
maxWidth = bottom - top;
|
|
}
|
|
return {
|
|
titleX,
|
|
titleY,
|
|
maxWidth,
|
|
rotation
|
|
};
|
|
}
|
|
draw() {
|
|
const ctx = this.ctx;
|
|
const opts = this.options;
|
|
if (!opts.display) {
|
|
return;
|
|
}
|
|
const fontOpts = toFont(opts.font);
|
|
const lineHeight = fontOpts.lineHeight;
|
|
const offset = lineHeight / 2 + this._padding.top;
|
|
const { titleX , titleY , maxWidth , rotation } = this._drawArgs(offset);
|
|
renderText(ctx, opts.text, 0, 0, fontOpts, {
|
|
color: opts.color,
|
|
maxWidth,
|
|
rotation,
|
|
textAlign: _toLeftRightCenter(opts.align),
|
|
textBaseline: 'middle',
|
|
translation: [
|
|
titleX,
|
|
titleY
|
|
]
|
|
});
|
|
}
|
|
}
|
|
function createTitle(chart, titleOpts) {
|
|
const title = new Title({
|
|
ctx: chart.ctx,
|
|
options: titleOpts,
|
|
chart
|
|
});
|
|
layouts.configure(chart, title, titleOpts);
|
|
layouts.addBox(chart, title);
|
|
chart.titleBlock = title;
|
|
}
|
|
var plugin_title = {
|
|
id: 'title',
|
|
_element: Title,
|
|
start (chart, _args, options) {
|
|
createTitle(chart, options);
|
|
},
|
|
stop (chart) {
|
|
const titleBlock = chart.titleBlock;
|
|
layouts.removeBox(chart, titleBlock);
|
|
delete chart.titleBlock;
|
|
},
|
|
beforeUpdate (chart, _args, options) {
|
|
const title = chart.titleBlock;
|
|
layouts.configure(chart, title, options);
|
|
title.options = options;
|
|
},
|
|
defaults: {
|
|
align: 'center',
|
|
display: false,
|
|
font: {
|
|
weight: 'bold'
|
|
},
|
|
fullSize: true,
|
|
padding: 10,
|
|
position: 'top',
|
|
text: '',
|
|
weight: 2000
|
|
},
|
|
defaultRoutes: {
|
|
color: 'color'
|
|
},
|
|
descriptors: {
|
|
_scriptable: true,
|
|
_indexable: false
|
|
}
|
|
};
|
|
|
|
const chart_map = new WeakMap();
|
|
var plugin_subtitle = {
|
|
id: 'subtitle',
|
|
start (chart, _args, options) {
|
|
const title = new Title({
|
|
ctx: chart.ctx,
|
|
options,
|
|
chart
|
|
});
|
|
layouts.configure(chart, title, options);
|
|
layouts.addBox(chart, title);
|
|
chart_map.set(chart, title);
|
|
},
|
|
stop (chart) {
|
|
layouts.removeBox(chart, chart_map.get(chart));
|
|
chart_map.delete(chart);
|
|
},
|
|
beforeUpdate (chart, _args, options) {
|
|
const title = chart_map.get(chart);
|
|
layouts.configure(chart, title, options);
|
|
title.options = options;
|
|
},
|
|
defaults: {
|
|
align: 'center',
|
|
display: false,
|
|
font: {
|
|
weight: 'normal'
|
|
},
|
|
fullSize: true,
|
|
padding: 0,
|
|
position: 'top',
|
|
text: '',
|
|
weight: 1500
|
|
},
|
|
defaultRoutes: {
|
|
color: 'color'
|
|
},
|
|
descriptors: {
|
|
_scriptable: true,
|
|
_indexable: false
|
|
}
|
|
};
|
|
|
|
const positioners = {
|
|
average (items) {
|
|
if (!items.length) {
|
|
return false;
|
|
}
|
|
let i, len;
|
|
let xSet = new Set();
|
|
let y = 0;
|
|
let count = 0;
|
|
for(i = 0, len = items.length; i < len; ++i){
|
|
const el = items[i].element;
|
|
if (el && el.hasValue()) {
|
|
const pos = el.tooltipPosition();
|
|
xSet.add(pos.x);
|
|
y += pos.y;
|
|
++count;
|
|
}
|
|
}
|
|
if (count === 0 || xSet.size === 0) {
|
|
return false;
|
|
}
|
|
const xAverage = [
|
|
...xSet
|
|
].reduce((a, b)=>a + b) / xSet.size;
|
|
return {
|
|
x: xAverage,
|
|
y: y / count
|
|
};
|
|
},
|
|
nearest (items, eventPosition) {
|
|
if (!items.length) {
|
|
return false;
|
|
}
|
|
let x = eventPosition.x;
|
|
let y = eventPosition.y;
|
|
let minDistance = Number.POSITIVE_INFINITY;
|
|
let i, len, nearestElement;
|
|
for(i = 0, len = items.length; i < len; ++i){
|
|
const el = items[i].element;
|
|
if (el && el.hasValue()) {
|
|
const center = el.getCenterPoint();
|
|
const d = distanceBetweenPoints(eventPosition, center);
|
|
if (d < minDistance) {
|
|
minDistance = d;
|
|
nearestElement = el;
|
|
}
|
|
}
|
|
}
|
|
if (nearestElement) {
|
|
const tp = nearestElement.tooltipPosition();
|
|
x = tp.x;
|
|
y = tp.y;
|
|
}
|
|
return {
|
|
x,
|
|
y
|
|
};
|
|
}
|
|
};
|
|
function pushOrConcat(base, toPush) {
|
|
if (toPush) {
|
|
if (isArray(toPush)) {
|
|
Array.prototype.push.apply(base, toPush);
|
|
} else {
|
|
base.push(toPush);
|
|
}
|
|
}
|
|
return base;
|
|
}
|
|
function splitNewlines(str) {
|
|
if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) {
|
|
return str.split('\n');
|
|
}
|
|
return str;
|
|
}
|
|
function createTooltipItem(chart, item) {
|
|
const { element , datasetIndex , index } = item;
|
|
const controller = chart.getDatasetMeta(datasetIndex).controller;
|
|
const { label , value } = controller.getLabelAndValue(index);
|
|
return {
|
|
chart,
|
|
label,
|
|
parsed: controller.getParsed(index),
|
|
raw: chart.data.datasets[datasetIndex].data[index],
|
|
formattedValue: value,
|
|
dataset: controller.getDataset(),
|
|
dataIndex: index,
|
|
datasetIndex,
|
|
element
|
|
};
|
|
}
|
|
function getTooltipSize(tooltip, options) {
|
|
const ctx = tooltip.chart.ctx;
|
|
const { body , footer , title } = tooltip;
|
|
const { boxWidth , boxHeight } = options;
|
|
const bodyFont = toFont(options.bodyFont);
|
|
const titleFont = toFont(options.titleFont);
|
|
const footerFont = toFont(options.footerFont);
|
|
const titleLineCount = title.length;
|
|
const footerLineCount = footer.length;
|
|
const bodyLineItemCount = body.length;
|
|
const padding = toPadding(options.padding);
|
|
let height = padding.height;
|
|
let width = 0;
|
|
let combinedBodyLength = body.reduce((count, bodyItem)=>count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0);
|
|
combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length;
|
|
if (titleLineCount) {
|
|
height += titleLineCount * titleFont.lineHeight + (titleLineCount - 1) * options.titleSpacing + options.titleMarginBottom;
|
|
}
|
|
if (combinedBodyLength) {
|
|
const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight;
|
|
height += bodyLineItemCount * bodyLineHeight + (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight + (combinedBodyLength - 1) * options.bodySpacing;
|
|
}
|
|
if (footerLineCount) {
|
|
height += options.footerMarginTop + footerLineCount * footerFont.lineHeight + (footerLineCount - 1) * options.footerSpacing;
|
|
}
|
|
let widthPadding = 0;
|
|
const maxLineWidth = function(line) {
|
|
width = Math.max(width, ctx.measureText(line).width + widthPadding);
|
|
};
|
|
ctx.save();
|
|
ctx.font = titleFont.string;
|
|
each(tooltip.title, maxLineWidth);
|
|
ctx.font = bodyFont.string;
|
|
each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth);
|
|
widthPadding = options.displayColors ? boxWidth + 2 + options.boxPadding : 0;
|
|
each(body, (bodyItem)=>{
|
|
each(bodyItem.before, maxLineWidth);
|
|
each(bodyItem.lines, maxLineWidth);
|
|
each(bodyItem.after, maxLineWidth);
|
|
});
|
|
widthPadding = 0;
|
|
ctx.font = footerFont.string;
|
|
each(tooltip.footer, maxLineWidth);
|
|
ctx.restore();
|
|
width += padding.width;
|
|
return {
|
|
width,
|
|
height
|
|
};
|
|
}
|
|
function determineYAlign(chart, size) {
|
|
const { y , height } = size;
|
|
if (y < height / 2) {
|
|
return 'top';
|
|
} else if (y > chart.height - height / 2) {
|
|
return 'bottom';
|
|
}
|
|
return 'center';
|
|
}
|
|
function doesNotFitWithAlign(xAlign, chart, options, size) {
|
|
const { x , width } = size;
|
|
const caret = options.caretSize + options.caretPadding;
|
|
if (xAlign === 'left' && x + width + caret > chart.width) {
|
|
return true;
|
|
}
|
|
if (xAlign === 'right' && x - width - caret < 0) {
|
|
return true;
|
|
}
|
|
}
|
|
function determineXAlign(chart, options, size, yAlign) {
|
|
const { x , width } = size;
|
|
const { width: chartWidth , chartArea: { left , right } } = chart;
|
|
let xAlign = 'center';
|
|
if (yAlign === 'center') {
|
|
xAlign = x <= (left + right) / 2 ? 'left' : 'right';
|
|
} else if (x <= width / 2) {
|
|
xAlign = 'left';
|
|
} else if (x >= chartWidth - width / 2) {
|
|
xAlign = 'right';
|
|
}
|
|
if (doesNotFitWithAlign(xAlign, chart, options, size)) {
|
|
xAlign = 'center';
|
|
}
|
|
return xAlign;
|
|
}
|
|
function determineAlignment(chart, options, size) {
|
|
const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size);
|
|
return {
|
|
xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign),
|
|
yAlign
|
|
};
|
|
}
|
|
function alignX(size, xAlign) {
|
|
let { x , width } = size;
|
|
if (xAlign === 'right') {
|
|
x -= width;
|
|
} else if (xAlign === 'center') {
|
|
x -= width / 2;
|
|
}
|
|
return x;
|
|
}
|
|
function alignY(size, yAlign, paddingAndSize) {
|
|
let { y , height } = size;
|
|
if (yAlign === 'top') {
|
|
y += paddingAndSize;
|
|
} else if (yAlign === 'bottom') {
|
|
y -= height + paddingAndSize;
|
|
} else {
|
|
y -= height / 2;
|
|
}
|
|
return y;
|
|
}
|
|
function getBackgroundPoint(options, size, alignment, chart) {
|
|
const { caretSize , caretPadding , cornerRadius } = options;
|
|
const { xAlign , yAlign } = alignment;
|
|
const paddingAndSize = caretSize + caretPadding;
|
|
const { topLeft , topRight , bottomLeft , bottomRight } = toTRBLCorners(cornerRadius);
|
|
let x = alignX(size, xAlign);
|
|
const y = alignY(size, yAlign, paddingAndSize);
|
|
if (yAlign === 'center') {
|
|
if (xAlign === 'left') {
|
|
x += paddingAndSize;
|
|
} else if (xAlign === 'right') {
|
|
x -= paddingAndSize;
|
|
}
|
|
} else if (xAlign === 'left') {
|
|
x -= Math.max(topLeft, bottomLeft) + caretSize;
|
|
} else if (xAlign === 'right') {
|
|
x += Math.max(topRight, bottomRight) + caretSize;
|
|
}
|
|
return {
|
|
x: _limitValue(x, 0, chart.width - size.width),
|
|
y: _limitValue(y, 0, chart.height - size.height)
|
|
};
|
|
}
|
|
function getAlignedX(tooltip, align, options) {
|
|
const padding = toPadding(options.padding);
|
|
return align === 'center' ? tooltip.x + tooltip.width / 2 : align === 'right' ? tooltip.x + tooltip.width - padding.right : tooltip.x + padding.left;
|
|
}
|
|
function getBeforeAfterBodyLines(callback) {
|
|
return pushOrConcat([], splitNewlines(callback));
|
|
}
|
|
function createTooltipContext(parent, tooltip, tooltipItems) {
|
|
return createContext(parent, {
|
|
tooltip,
|
|
tooltipItems,
|
|
type: 'tooltip'
|
|
});
|
|
}
|
|
function overrideCallbacks(callbacks, context) {
|
|
const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks;
|
|
return override ? callbacks.override(override) : callbacks;
|
|
}
|
|
const defaultCallbacks = {
|
|
beforeTitle: noop,
|
|
title (tooltipItems) {
|
|
if (tooltipItems.length > 0) {
|
|
const item = tooltipItems[0];
|
|
const labels = item.chart.data.labels;
|
|
const labelCount = labels ? labels.length : 0;
|
|
if (this && this.options && this.options.mode === 'dataset') {
|
|
return item.dataset.label || '';
|
|
} else if (item.label) {
|
|
return item.label;
|
|
} else if (labelCount > 0 && item.dataIndex < labelCount) {
|
|
return labels[item.dataIndex];
|
|
}
|
|
}
|
|
return '';
|
|
},
|
|
afterTitle: noop,
|
|
beforeBody: noop,
|
|
beforeLabel: noop,
|
|
label (tooltipItem) {
|
|
if (this && this.options && this.options.mode === 'dataset') {
|
|
return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue;
|
|
}
|
|
let label = tooltipItem.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
const value = tooltipItem.formattedValue;
|
|
if (!isNullOrUndef(value)) {
|
|
label += value;
|
|
}
|
|
return label;
|
|
},
|
|
labelColor (tooltipItem) {
|
|
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
|
|
const options = meta.controller.getStyle(tooltipItem.dataIndex);
|
|
return {
|
|
borderColor: options.borderColor,
|
|
backgroundColor: options.backgroundColor,
|
|
borderWidth: options.borderWidth,
|
|
borderDash: options.borderDash,
|
|
borderDashOffset: options.borderDashOffset,
|
|
borderRadius: 0
|
|
};
|
|
},
|
|
labelTextColor () {
|
|
return this.options.bodyColor;
|
|
},
|
|
labelPointStyle (tooltipItem) {
|
|
const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);
|
|
const options = meta.controller.getStyle(tooltipItem.dataIndex);
|
|
return {
|
|
pointStyle: options.pointStyle,
|
|
rotation: options.rotation
|
|
};
|
|
},
|
|
afterLabel: noop,
|
|
afterBody: noop,
|
|
beforeFooter: noop,
|
|
footer: noop,
|
|
afterFooter: noop
|
|
};
|
|
function invokeCallbackWithFallback(callbacks, name, ctx, arg) {
|
|
const result = callbacks[name].call(ctx, arg);
|
|
if (typeof result === 'undefined') {
|
|
return defaultCallbacks[name].call(ctx, arg);
|
|
}
|
|
return result;
|
|
}
|
|
class Tooltip extends chart_Element {
|
|
static positioners = positioners;
|
|
constructor(config){
|
|
super();
|
|
this.opacity = 0;
|
|
this._active = [];
|
|
this._eventPosition = undefined;
|
|
this._size = undefined;
|
|
this._cachedAnimations = undefined;
|
|
this._tooltipItems = [];
|
|
this.$animations = undefined;
|
|
this.$context = undefined;
|
|
this.chart = config.chart;
|
|
this.options = config.options;
|
|
this.dataPoints = undefined;
|
|
this.title = undefined;
|
|
this.beforeBody = undefined;
|
|
this.body = undefined;
|
|
this.afterBody = undefined;
|
|
this.footer = undefined;
|
|
this.xAlign = undefined;
|
|
this.yAlign = undefined;
|
|
this.x = undefined;
|
|
this.y = undefined;
|
|
this.height = undefined;
|
|
this.width = undefined;
|
|
this.caretX = undefined;
|
|
this.caretY = undefined;
|
|
this.labelColors = undefined;
|
|
this.labelPointStyles = undefined;
|
|
this.labelTextColors = undefined;
|
|
}
|
|
initialize(options) {
|
|
this.options = options;
|
|
this._cachedAnimations = undefined;
|
|
this.$context = undefined;
|
|
}
|
|
_resolveAnimations() {
|
|
const cached = this._cachedAnimations;
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const chart = this.chart;
|
|
const options = this.options.setContext(this.getContext());
|
|
const opts = options.enabled && chart.options.animation && options.animations;
|
|
const animations = new Animations(this.chart, opts);
|
|
if (opts._cacheable) {
|
|
this._cachedAnimations = Object.freeze(animations);
|
|
}
|
|
return animations;
|
|
}
|
|
getContext() {
|
|
return this.$context || (this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems));
|
|
}
|
|
getTitle(context, options) {
|
|
const { callbacks } = options;
|
|
const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context);
|
|
const title = invokeCallbackWithFallback(callbacks, 'title', this, context);
|
|
const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context);
|
|
let lines = [];
|
|
lines = pushOrConcat(lines, splitNewlines(beforeTitle));
|
|
lines = pushOrConcat(lines, splitNewlines(title));
|
|
lines = pushOrConcat(lines, splitNewlines(afterTitle));
|
|
return lines;
|
|
}
|
|
getBeforeBody(tooltipItems, options) {
|
|
return getBeforeAfterBodyLines(invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems));
|
|
}
|
|
getBody(tooltipItems, options) {
|
|
const { callbacks } = options;
|
|
const bodyItems = [];
|
|
each(tooltipItems, (context)=>{
|
|
const bodyItem = {
|
|
before: [],
|
|
lines: [],
|
|
after: []
|
|
};
|
|
const scoped = overrideCallbacks(callbacks, context);
|
|
pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context)));
|
|
pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context));
|
|
pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context)));
|
|
bodyItems.push(bodyItem);
|
|
});
|
|
return bodyItems;
|
|
}
|
|
getAfterBody(tooltipItems, options) {
|
|
return getBeforeAfterBodyLines(invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems));
|
|
}
|
|
getFooter(tooltipItems, options) {
|
|
const { callbacks } = options;
|
|
const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems);
|
|
const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems);
|
|
const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems);
|
|
let lines = [];
|
|
lines = pushOrConcat(lines, splitNewlines(beforeFooter));
|
|
lines = pushOrConcat(lines, splitNewlines(footer));
|
|
lines = pushOrConcat(lines, splitNewlines(afterFooter));
|
|
return lines;
|
|
}
|
|
_createItems(options) {
|
|
const active = this._active;
|
|
const data = this.chart.data;
|
|
const labelColors = [];
|
|
const labelPointStyles = [];
|
|
const labelTextColors = [];
|
|
let tooltipItems = [];
|
|
let i, len;
|
|
for(i = 0, len = active.length; i < len; ++i){
|
|
tooltipItems.push(createTooltipItem(this.chart, active[i]));
|
|
}
|
|
if (options.filter) {
|
|
tooltipItems = tooltipItems.filter((element, index, array)=>options.filter(element, index, array, data));
|
|
}
|
|
if (options.itemSort) {
|
|
tooltipItems = tooltipItems.sort((a, b)=>options.itemSort(a, b, data));
|
|
}
|
|
each(tooltipItems, (context)=>{
|
|
const scoped = overrideCallbacks(options.callbacks, context);
|
|
labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context));
|
|
labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context));
|
|
labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context));
|
|
});
|
|
this.labelColors = labelColors;
|
|
this.labelPointStyles = labelPointStyles;
|
|
this.labelTextColors = labelTextColors;
|
|
this.dataPoints = tooltipItems;
|
|
return tooltipItems;
|
|
}
|
|
update(changed, replay) {
|
|
const options = this.options.setContext(this.getContext());
|
|
const active = this._active;
|
|
let properties;
|
|
let tooltipItems = [];
|
|
if (!active.length) {
|
|
if (this.opacity !== 0) {
|
|
properties = {
|
|
opacity: 0
|
|
};
|
|
}
|
|
} else {
|
|
const position = positioners[options.position].call(this, active, this._eventPosition);
|
|
tooltipItems = this._createItems(options);
|
|
this.title = this.getTitle(tooltipItems, options);
|
|
this.beforeBody = this.getBeforeBody(tooltipItems, options);
|
|
this.body = this.getBody(tooltipItems, options);
|
|
this.afterBody = this.getAfterBody(tooltipItems, options);
|
|
this.footer = this.getFooter(tooltipItems, options);
|
|
const size = this._size = getTooltipSize(this, options);
|
|
const positionAndSize = Object.assign({}, position, size);
|
|
const alignment = determineAlignment(this.chart, options, positionAndSize);
|
|
const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart);
|
|
this.xAlign = alignment.xAlign;
|
|
this.yAlign = alignment.yAlign;
|
|
properties = {
|
|
opacity: 1,
|
|
x: backgroundPoint.x,
|
|
y: backgroundPoint.y,
|
|
width: size.width,
|
|
height: size.height,
|
|
caretX: position.x,
|
|
caretY: position.y
|
|
};
|
|
}
|
|
this._tooltipItems = tooltipItems;
|
|
this.$context = undefined;
|
|
if (properties) {
|
|
this._resolveAnimations().update(this, properties);
|
|
}
|
|
if (changed && options.external) {
|
|
options.external.call(this, {
|
|
chart: this.chart,
|
|
tooltip: this,
|
|
replay
|
|
});
|
|
}
|
|
}
|
|
drawCaret(tooltipPoint, ctx, size, options) {
|
|
const caretPosition = this.getCaretPosition(tooltipPoint, size, options);
|
|
ctx.lineTo(caretPosition.x1, caretPosition.y1);
|
|
ctx.lineTo(caretPosition.x2, caretPosition.y2);
|
|
ctx.lineTo(caretPosition.x3, caretPosition.y3);
|
|
}
|
|
getCaretPosition(tooltipPoint, size, options) {
|
|
const { xAlign , yAlign } = this;
|
|
const { caretSize , cornerRadius } = options;
|
|
const { topLeft , topRight , bottomLeft , bottomRight } = toTRBLCorners(cornerRadius);
|
|
const { x: ptX , y: ptY } = tooltipPoint;
|
|
const { width , height } = size;
|
|
let x1, x2, x3, y1, y2, y3;
|
|
if (yAlign === 'center') {
|
|
y2 = ptY + height / 2;
|
|
if (xAlign === 'left') {
|
|
x1 = ptX;
|
|
x2 = x1 - caretSize;
|
|
y1 = y2 + caretSize;
|
|
y3 = y2 - caretSize;
|
|
} else {
|
|
x1 = ptX + width;
|
|
x2 = x1 + caretSize;
|
|
y1 = y2 - caretSize;
|
|
y3 = y2 + caretSize;
|
|
}
|
|
x3 = x1;
|
|
} else {
|
|
if (xAlign === 'left') {
|
|
x2 = ptX + Math.max(topLeft, bottomLeft) + caretSize;
|
|
} else if (xAlign === 'right') {
|
|
x2 = ptX + width - Math.max(topRight, bottomRight) - caretSize;
|
|
} else {
|
|
x2 = this.caretX;
|
|
}
|
|
if (yAlign === 'top') {
|
|
y1 = ptY;
|
|
y2 = y1 - caretSize;
|
|
x1 = x2 - caretSize;
|
|
x3 = x2 + caretSize;
|
|
} else {
|
|
y1 = ptY + height;
|
|
y2 = y1 + caretSize;
|
|
x1 = x2 + caretSize;
|
|
x3 = x2 - caretSize;
|
|
}
|
|
y3 = y1;
|
|
}
|
|
return {
|
|
x1,
|
|
x2,
|
|
x3,
|
|
y1,
|
|
y2,
|
|
y3
|
|
};
|
|
}
|
|
drawTitle(pt, ctx, options) {
|
|
const title = this.title;
|
|
const length = title.length;
|
|
let titleFont, titleSpacing, i;
|
|
if (length) {
|
|
const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);
|
|
pt.x = getAlignedX(this, options.titleAlign, options);
|
|
ctx.textAlign = rtlHelper.textAlign(options.titleAlign);
|
|
ctx.textBaseline = 'middle';
|
|
titleFont = toFont(options.titleFont);
|
|
titleSpacing = options.titleSpacing;
|
|
ctx.fillStyle = options.titleColor;
|
|
ctx.font = titleFont.string;
|
|
for(i = 0; i < length; ++i){
|
|
ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2);
|
|
pt.y += titleFont.lineHeight + titleSpacing;
|
|
if (i + 1 === length) {
|
|
pt.y += options.titleMarginBottom - titleSpacing;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_drawColorBox(ctx, pt, i, rtlHelper, options) {
|
|
const labelColor = this.labelColors[i];
|
|
const labelPointStyle = this.labelPointStyles[i];
|
|
const { boxHeight , boxWidth } = options;
|
|
const bodyFont = toFont(options.bodyFont);
|
|
const colorX = getAlignedX(this, 'left', options);
|
|
const rtlColorX = rtlHelper.x(colorX);
|
|
const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0;
|
|
const colorY = pt.y + yOffSet;
|
|
if (options.usePointStyle) {
|
|
const drawOptions = {
|
|
radius: Math.min(boxWidth, boxHeight) / 2,
|
|
pointStyle: labelPointStyle.pointStyle,
|
|
rotation: labelPointStyle.rotation,
|
|
borderWidth: 1
|
|
};
|
|
const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2;
|
|
const centerY = colorY + boxHeight / 2;
|
|
ctx.strokeStyle = options.multiKeyBackground;
|
|
ctx.fillStyle = options.multiKeyBackground;
|
|
drawPoint(ctx, drawOptions, centerX, centerY);
|
|
ctx.strokeStyle = labelColor.borderColor;
|
|
ctx.fillStyle = labelColor.backgroundColor;
|
|
drawPoint(ctx, drawOptions, centerX, centerY);
|
|
} else {
|
|
ctx.lineWidth = isObject(labelColor.borderWidth) ? Math.max(...Object.values(labelColor.borderWidth)) : labelColor.borderWidth || 1;
|
|
ctx.strokeStyle = labelColor.borderColor;
|
|
ctx.setLineDash(labelColor.borderDash || []);
|
|
ctx.lineDashOffset = labelColor.borderDashOffset || 0;
|
|
const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth);
|
|
const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2);
|
|
const borderRadius = toTRBLCorners(labelColor.borderRadius);
|
|
if (Object.values(borderRadius).some((v)=>v !== 0)) {
|
|
ctx.beginPath();
|
|
ctx.fillStyle = options.multiKeyBackground;
|
|
addRoundedRectPath(ctx, {
|
|
x: outerX,
|
|
y: colorY,
|
|
w: boxWidth,
|
|
h: boxHeight,
|
|
radius: borderRadius
|
|
});
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
ctx.fillStyle = labelColor.backgroundColor;
|
|
ctx.beginPath();
|
|
addRoundedRectPath(ctx, {
|
|
x: innerX,
|
|
y: colorY + 1,
|
|
w: boxWidth - 2,
|
|
h: boxHeight - 2,
|
|
radius: borderRadius
|
|
});
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillStyle = options.multiKeyBackground;
|
|
ctx.fillRect(outerX, colorY, boxWidth, boxHeight);
|
|
ctx.strokeRect(outerX, colorY, boxWidth, boxHeight);
|
|
ctx.fillStyle = labelColor.backgroundColor;
|
|
ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2);
|
|
}
|
|
}
|
|
ctx.fillStyle = this.labelTextColors[i];
|
|
}
|
|
drawBody(pt, ctx, options) {
|
|
const { body } = this;
|
|
const { bodySpacing , bodyAlign , displayColors , boxHeight , boxWidth , boxPadding } = options;
|
|
const bodyFont = toFont(options.bodyFont);
|
|
let bodyLineHeight = bodyFont.lineHeight;
|
|
let xLinePadding = 0;
|
|
const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);
|
|
const fillLineOfText = function(line) {
|
|
ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2);
|
|
pt.y += bodyLineHeight + bodySpacing;
|
|
};
|
|
const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign);
|
|
let bodyItem, textColor, lines, i, j, ilen, jlen;
|
|
ctx.textAlign = bodyAlign;
|
|
ctx.textBaseline = 'middle';
|
|
ctx.font = bodyFont.string;
|
|
pt.x = getAlignedX(this, bodyAlignForCalculation, options);
|
|
ctx.fillStyle = options.bodyColor;
|
|
each(this.beforeBody, fillLineOfText);
|
|
xLinePadding = displayColors && bodyAlignForCalculation !== 'right' ? bodyAlign === 'center' ? boxWidth / 2 + boxPadding : boxWidth + 2 + boxPadding : 0;
|
|
for(i = 0, ilen = body.length; i < ilen; ++i){
|
|
bodyItem = body[i];
|
|
textColor = this.labelTextColors[i];
|
|
ctx.fillStyle = textColor;
|
|
each(bodyItem.before, fillLineOfText);
|
|
lines = bodyItem.lines;
|
|
if (displayColors && lines.length) {
|
|
this._drawColorBox(ctx, pt, i, rtlHelper, options);
|
|
bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight);
|
|
}
|
|
for(j = 0, jlen = lines.length; j < jlen; ++j){
|
|
fillLineOfText(lines[j]);
|
|
bodyLineHeight = bodyFont.lineHeight;
|
|
}
|
|
each(bodyItem.after, fillLineOfText);
|
|
}
|
|
xLinePadding = 0;
|
|
bodyLineHeight = bodyFont.lineHeight;
|
|
each(this.afterBody, fillLineOfText);
|
|
pt.y -= bodySpacing;
|
|
}
|
|
drawFooter(pt, ctx, options) {
|
|
const footer = this.footer;
|
|
const length = footer.length;
|
|
let footerFont, i;
|
|
if (length) {
|
|
const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);
|
|
pt.x = getAlignedX(this, options.footerAlign, options);
|
|
pt.y += options.footerMarginTop;
|
|
ctx.textAlign = rtlHelper.textAlign(options.footerAlign);
|
|
ctx.textBaseline = 'middle';
|
|
footerFont = toFont(options.footerFont);
|
|
ctx.fillStyle = options.footerColor;
|
|
ctx.font = footerFont.string;
|
|
for(i = 0; i < length; ++i){
|
|
ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2);
|
|
pt.y += footerFont.lineHeight + options.footerSpacing;
|
|
}
|
|
}
|
|
}
|
|
drawBackground(pt, ctx, tooltipSize, options) {
|
|
const { xAlign , yAlign } = this;
|
|
const { x , y } = pt;
|
|
const { width , height } = tooltipSize;
|
|
const { topLeft , topRight , bottomLeft , bottomRight } = toTRBLCorners(options.cornerRadius);
|
|
ctx.fillStyle = options.backgroundColor;
|
|
ctx.strokeStyle = options.borderColor;
|
|
ctx.lineWidth = options.borderWidth;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + topLeft, y);
|
|
if (yAlign === 'top') {
|
|
this.drawCaret(pt, ctx, tooltipSize, options);
|
|
}
|
|
ctx.lineTo(x + width - topRight, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + topRight);
|
|
if (yAlign === 'center' && xAlign === 'right') {
|
|
this.drawCaret(pt, ctx, tooltipSize, options);
|
|
}
|
|
ctx.lineTo(x + width, y + height - bottomRight);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - bottomRight, y + height);
|
|
if (yAlign === 'bottom') {
|
|
this.drawCaret(pt, ctx, tooltipSize, options);
|
|
}
|
|
ctx.lineTo(x + bottomLeft, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - bottomLeft);
|
|
if (yAlign === 'center' && xAlign === 'left') {
|
|
this.drawCaret(pt, ctx, tooltipSize, options);
|
|
}
|
|
ctx.lineTo(x, y + topLeft);
|
|
ctx.quadraticCurveTo(x, y, x + topLeft, y);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
if (options.borderWidth > 0) {
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
_updateAnimationTarget(options) {
|
|
const chart = this.chart;
|
|
const anims = this.$animations;
|
|
const animX = anims && anims.x;
|
|
const animY = anims && anims.y;
|
|
if (animX || animY) {
|
|
const position = positioners[options.position].call(this, this._active, this._eventPosition);
|
|
if (!position) {
|
|
return;
|
|
}
|
|
const size = this._size = getTooltipSize(this, options);
|
|
const positionAndSize = Object.assign({}, position, this._size);
|
|
const alignment = determineAlignment(chart, options, positionAndSize);
|
|
const point = getBackgroundPoint(options, positionAndSize, alignment, chart);
|
|
if (animX._to !== point.x || animY._to !== point.y) {
|
|
this.xAlign = alignment.xAlign;
|
|
this.yAlign = alignment.yAlign;
|
|
this.width = size.width;
|
|
this.height = size.height;
|
|
this.caretX = position.x;
|
|
this.caretY = position.y;
|
|
this._resolveAnimations().update(this, point);
|
|
}
|
|
}
|
|
}
|
|
_willRender() {
|
|
return !!this.opacity;
|
|
}
|
|
draw(ctx) {
|
|
const options = this.options.setContext(this.getContext());
|
|
let opacity = this.opacity;
|
|
if (!opacity) {
|
|
return;
|
|
}
|
|
this._updateAnimationTarget(options);
|
|
const tooltipSize = {
|
|
width: this.width,
|
|
height: this.height
|
|
};
|
|
const pt = {
|
|
x: this.x,
|
|
y: this.y
|
|
};
|
|
opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity;
|
|
const padding = toPadding(options.padding);
|
|
const hasTooltipContent = this.title.length || this.beforeBody.length || this.body.length || this.afterBody.length || this.footer.length;
|
|
if (options.enabled && hasTooltipContent) {
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
this.drawBackground(pt, ctx, tooltipSize, options);
|
|
overrideTextDirection(ctx, options.textDirection);
|
|
pt.y += padding.top;
|
|
this.drawTitle(pt, ctx, options);
|
|
this.drawBody(pt, ctx, options);
|
|
this.drawFooter(pt, ctx, options);
|
|
restoreTextDirection(ctx, options.textDirection);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
getActiveElements() {
|
|
return this._active || [];
|
|
}
|
|
setActiveElements(activeElements, eventPosition) {
|
|
const lastActive = this._active;
|
|
const active = activeElements.map(({ datasetIndex , index })=>{
|
|
const meta = this.chart.getDatasetMeta(datasetIndex);
|
|
if (!meta) {
|
|
throw new Error('Cannot find a dataset at index ' + datasetIndex);
|
|
}
|
|
return {
|
|
datasetIndex,
|
|
element: meta.data[index],
|
|
index
|
|
};
|
|
});
|
|
const changed = !_elementsEqual(lastActive, active);
|
|
const positionChanged = this._positionChanged(active, eventPosition);
|
|
if (changed || positionChanged) {
|
|
this._active = active;
|
|
this._eventPosition = eventPosition;
|
|
this._ignoreReplayEvents = true;
|
|
this.update(true);
|
|
}
|
|
}
|
|
handleEvent(e, replay, inChartArea = true) {
|
|
if (replay && this._ignoreReplayEvents) {
|
|
return false;
|
|
}
|
|
this._ignoreReplayEvents = false;
|
|
const options = this.options;
|
|
const lastActive = this._active || [];
|
|
const active = this._getActiveElements(e, lastActive, replay, inChartArea);
|
|
const positionChanged = this._positionChanged(active, e);
|
|
const changed = replay || !_elementsEqual(active, lastActive) || positionChanged;
|
|
if (changed) {
|
|
this._active = active;
|
|
if (options.enabled || options.external) {
|
|
this._eventPosition = {
|
|
x: e.x,
|
|
y: e.y
|
|
};
|
|
this.update(true, replay);
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
_getActiveElements(e, lastActive, replay, inChartArea) {
|
|
const options = this.options;
|
|
if (e.type === 'mouseout') {
|
|
return [];
|
|
}
|
|
if (!inChartArea) {
|
|
return lastActive.filter((i)=>this.chart.data.datasets[i.datasetIndex] && this.chart.getDatasetMeta(i.datasetIndex).controller.getParsed(i.index) !== undefined);
|
|
}
|
|
const active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay);
|
|
if (options.reverse) {
|
|
active.reverse();
|
|
}
|
|
return active;
|
|
}
|
|
_positionChanged(active, e) {
|
|
const { caretX , caretY , options } = this;
|
|
const position = positioners[options.position].call(this, active, e);
|
|
return position !== false && (caretX !== position.x || caretY !== position.y);
|
|
}
|
|
}
|
|
var plugin_tooltip = {
|
|
id: 'tooltip',
|
|
_element: Tooltip,
|
|
positioners,
|
|
afterInit (chart, _args, options) {
|
|
if (options) {
|
|
chart.tooltip = new Tooltip({
|
|
chart,
|
|
options
|
|
});
|
|
}
|
|
},
|
|
beforeUpdate (chart, _args, options) {
|
|
if (chart.tooltip) {
|
|
chart.tooltip.initialize(options);
|
|
}
|
|
},
|
|
reset (chart, _args, options) {
|
|
if (chart.tooltip) {
|
|
chart.tooltip.initialize(options);
|
|
}
|
|
},
|
|
afterDraw (chart) {
|
|
const tooltip = chart.tooltip;
|
|
if (tooltip && tooltip._willRender()) {
|
|
const args = {
|
|
tooltip
|
|
};
|
|
if (chart.notifyPlugins('beforeTooltipDraw', {
|
|
...args,
|
|
cancelable: true
|
|
}) === false) {
|
|
return;
|
|
}
|
|
tooltip.draw(chart.ctx);
|
|
chart.notifyPlugins('afterTooltipDraw', args);
|
|
}
|
|
},
|
|
afterEvent (chart, args) {
|
|
if (chart.tooltip) {
|
|
const useFinalPosition = args.replay;
|
|
if (chart.tooltip.handleEvent(args.event, useFinalPosition, args.inChartArea)) {
|
|
args.changed = true;
|
|
}
|
|
}
|
|
},
|
|
defaults: {
|
|
enabled: true,
|
|
external: null,
|
|
position: 'average',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
titleColor: '#fff',
|
|
titleFont: {
|
|
weight: 'bold'
|
|
},
|
|
titleSpacing: 2,
|
|
titleMarginBottom: 6,
|
|
titleAlign: 'left',
|
|
bodyColor: '#fff',
|
|
bodySpacing: 2,
|
|
bodyFont: {},
|
|
bodyAlign: 'left',
|
|
footerColor: '#fff',
|
|
footerSpacing: 2,
|
|
footerMarginTop: 6,
|
|
footerFont: {
|
|
weight: 'bold'
|
|
},
|
|
footerAlign: 'left',
|
|
padding: 6,
|
|
caretPadding: 2,
|
|
caretSize: 5,
|
|
cornerRadius: 6,
|
|
boxHeight: (ctx, opts)=>opts.bodyFont.size,
|
|
boxWidth: (ctx, opts)=>opts.bodyFont.size,
|
|
multiKeyBackground: '#fff',
|
|
displayColors: true,
|
|
boxPadding: 0,
|
|
borderColor: 'rgba(0,0,0,0)',
|
|
borderWidth: 0,
|
|
animation: {
|
|
duration: 400,
|
|
easing: 'easeOutQuart'
|
|
},
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: [
|
|
'x',
|
|
'y',
|
|
'width',
|
|
'height',
|
|
'caretX',
|
|
'caretY'
|
|
]
|
|
},
|
|
opacity: {
|
|
easing: 'linear',
|
|
duration: 200
|
|
}
|
|
},
|
|
callbacks: defaultCallbacks
|
|
},
|
|
defaultRoutes: {
|
|
bodyFont: 'font',
|
|
footerFont: 'font',
|
|
titleFont: 'font'
|
|
},
|
|
descriptors: {
|
|
_scriptable: (name)=>name !== 'filter' && name !== 'itemSort' && name !== 'external',
|
|
_indexable: false,
|
|
callbacks: {
|
|
_scriptable: false,
|
|
_indexable: false
|
|
},
|
|
animation: {
|
|
_fallback: false
|
|
},
|
|
animations: {
|
|
_fallback: 'animation'
|
|
}
|
|
},
|
|
additionalOptionScopes: [
|
|
'interaction'
|
|
]
|
|
};
|
|
|
|
var plugins = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
Colors: plugin_colors,
|
|
Decimation: plugin_decimation,
|
|
Filler: index,
|
|
Legend: plugin_legend,
|
|
SubTitle: plugin_subtitle,
|
|
Title: plugin_title,
|
|
Tooltip: plugin_tooltip
|
|
});
|
|
|
|
const addIfString = (labels, raw, index, addedLabels)=>{
|
|
if (typeof raw === 'string') {
|
|
index = labels.push(raw) - 1;
|
|
addedLabels.unshift({
|
|
index,
|
|
label: raw
|
|
});
|
|
} else if (isNaN(raw)) {
|
|
index = null;
|
|
}
|
|
return index;
|
|
};
|
|
function findOrAddLabel(labels, raw, index, addedLabels) {
|
|
const first = labels.indexOf(raw);
|
|
if (first === -1) {
|
|
return addIfString(labels, raw, index, addedLabels);
|
|
}
|
|
const last = labels.lastIndexOf(raw);
|
|
return first !== last ? index : first;
|
|
}
|
|
const validIndex = (index, max)=>index === null ? null : _limitValue(Math.round(index), 0, max);
|
|
function _getLabelForValue(value) {
|
|
const labels = this.getLabels();
|
|
if (value >= 0 && value < labels.length) {
|
|
return labels[value];
|
|
}
|
|
return value;
|
|
}
|
|
class CategoryScale extends Scale {
|
|
static id = 'category';
|
|
static defaults = {
|
|
ticks: {
|
|
callback: _getLabelForValue
|
|
}
|
|
};
|
|
constructor(cfg){
|
|
super(cfg);
|
|
this._startValue = undefined;
|
|
this._valueRange = 0;
|
|
this._addedLabels = [];
|
|
}
|
|
init(scaleOptions) {
|
|
const added = this._addedLabels;
|
|
if (added.length) {
|
|
const labels = this.getLabels();
|
|
for (const { index , label } of added){
|
|
if (labels[index] === label) {
|
|
labels.splice(index, 1);
|
|
}
|
|
}
|
|
this._addedLabels = [];
|
|
}
|
|
super.init(scaleOptions);
|
|
}
|
|
parse(raw, index) {
|
|
if (isNullOrUndef(raw)) {
|
|
return null;
|
|
}
|
|
const labels = this.getLabels();
|
|
index = isFinite(index) && labels[index] === raw ? index : findOrAddLabel(labels, raw, valueOrDefault(index, raw), this._addedLabels);
|
|
return validIndex(index, labels.length - 1);
|
|
}
|
|
determineDataLimits() {
|
|
const { minDefined , maxDefined } = this.getUserBounds();
|
|
let { min , max } = this.getMinMax(true);
|
|
if (this.options.bounds === 'ticks') {
|
|
if (!minDefined) {
|
|
min = 0;
|
|
}
|
|
if (!maxDefined) {
|
|
max = this.getLabels().length - 1;
|
|
}
|
|
}
|
|
this.min = min;
|
|
this.max = max;
|
|
}
|
|
buildTicks() {
|
|
const min = this.min;
|
|
const max = this.max;
|
|
const offset = this.options.offset;
|
|
const ticks = [];
|
|
let labels = this.getLabels();
|
|
labels = min === 0 && max === labels.length - 1 ? labels : labels.slice(min, max + 1);
|
|
this._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1);
|
|
this._startValue = this.min - (offset ? 0.5 : 0);
|
|
for(let value = min; value <= max; value++){
|
|
ticks.push({
|
|
value
|
|
});
|
|
}
|
|
return ticks;
|
|
}
|
|
getLabelForValue(value) {
|
|
return _getLabelForValue.call(this, value);
|
|
}
|
|
configure() {
|
|
super.configure();
|
|
if (!this.isHorizontal()) {
|
|
this._reversePixels = !this._reversePixels;
|
|
}
|
|
}
|
|
getPixelForValue(value) {
|
|
if (typeof value !== 'number') {
|
|
value = this.parse(value);
|
|
}
|
|
return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange);
|
|
}
|
|
getPixelForTick(index) {
|
|
const ticks = this.ticks;
|
|
if (index < 0 || index > ticks.length - 1) {
|
|
return null;
|
|
}
|
|
return this.getPixelForValue(ticks[index].value);
|
|
}
|
|
getValueForPixel(pixel) {
|
|
return Math.round(this._startValue + this.getDecimalForPixel(pixel) * this._valueRange);
|
|
}
|
|
getBasePixel() {
|
|
return this.bottom;
|
|
}
|
|
}
|
|
|
|
function generateTicks$1(generationOptions, dataRange) {
|
|
const ticks = [];
|
|
const MIN_SPACING = 1e-14;
|
|
const { bounds , step , min , max , precision , count , maxTicks , maxDigits , includeBounds } = generationOptions;
|
|
const unit = step || 1;
|
|
const maxSpaces = maxTicks - 1;
|
|
const { min: rmin , max: rmax } = dataRange;
|
|
const minDefined = !isNullOrUndef(min);
|
|
const maxDefined = !isNullOrUndef(max);
|
|
const countDefined = !isNullOrUndef(count);
|
|
const minSpacing = (rmax - rmin) / (maxDigits + 1);
|
|
let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit;
|
|
let factor, niceMin, niceMax, numSpaces;
|
|
if (spacing < MIN_SPACING && !minDefined && !maxDefined) {
|
|
return [
|
|
{
|
|
value: rmin
|
|
},
|
|
{
|
|
value: rmax
|
|
}
|
|
];
|
|
}
|
|
numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing);
|
|
if (numSpaces > maxSpaces) {
|
|
spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit;
|
|
}
|
|
if (!isNullOrUndef(precision)) {
|
|
factor = Math.pow(10, precision);
|
|
spacing = Math.ceil(spacing * factor) / factor;
|
|
}
|
|
if (bounds === 'ticks') {
|
|
niceMin = Math.floor(rmin / spacing) * spacing;
|
|
niceMax = Math.ceil(rmax / spacing) * spacing;
|
|
} else {
|
|
niceMin = rmin;
|
|
niceMax = rmax;
|
|
}
|
|
if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) {
|
|
numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks));
|
|
spacing = (max - min) / numSpaces;
|
|
niceMin = min;
|
|
niceMax = max;
|
|
} else if (countDefined) {
|
|
niceMin = minDefined ? min : niceMin;
|
|
niceMax = maxDefined ? max : niceMax;
|
|
numSpaces = count - 1;
|
|
spacing = (niceMax - niceMin) / numSpaces;
|
|
} else {
|
|
numSpaces = (niceMax - niceMin) / spacing;
|
|
if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
|
|
numSpaces = Math.round(numSpaces);
|
|
} else {
|
|
numSpaces = Math.ceil(numSpaces);
|
|
}
|
|
}
|
|
const decimalPlaces = Math.max(_decimalPlaces(spacing), _decimalPlaces(niceMin));
|
|
factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision);
|
|
niceMin = Math.round(niceMin * factor) / factor;
|
|
niceMax = Math.round(niceMax * factor) / factor;
|
|
let j = 0;
|
|
if (minDefined) {
|
|
if (includeBounds && niceMin !== min) {
|
|
ticks.push({
|
|
value: min
|
|
});
|
|
if (niceMin < min) {
|
|
j++;
|
|
}
|
|
if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) {
|
|
j++;
|
|
}
|
|
} else if (niceMin < min) {
|
|
j++;
|
|
}
|
|
}
|
|
for(; j < numSpaces; ++j){
|
|
const tickValue = Math.round((niceMin + j * spacing) * factor) / factor;
|
|
if (maxDefined && tickValue > max) {
|
|
break;
|
|
}
|
|
ticks.push({
|
|
value: tickValue
|
|
});
|
|
}
|
|
if (maxDefined && includeBounds && niceMax !== max) {
|
|
if (ticks.length && almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) {
|
|
ticks[ticks.length - 1].value = max;
|
|
} else {
|
|
ticks.push({
|
|
value: max
|
|
});
|
|
}
|
|
} else if (!maxDefined || niceMax === max) {
|
|
ticks.push({
|
|
value: niceMax
|
|
});
|
|
}
|
|
return ticks;
|
|
}
|
|
function relativeLabelSize(value, minSpacing, { horizontal , minRotation }) {
|
|
const rad = toRadians(minRotation);
|
|
const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001;
|
|
const length = 0.75 * minSpacing * ('' + value).length;
|
|
return Math.min(minSpacing / ratio, length);
|
|
}
|
|
class LinearScaleBase extends Scale {
|
|
constructor(cfg){
|
|
super(cfg);
|
|
this.start = undefined;
|
|
this.end = undefined;
|
|
this._startValue = undefined;
|
|
this._endValue = undefined;
|
|
this._valueRange = 0;
|
|
}
|
|
parse(raw, index) {
|
|
if (isNullOrUndef(raw)) {
|
|
return null;
|
|
}
|
|
if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) {
|
|
return null;
|
|
}
|
|
return +raw;
|
|
}
|
|
handleTickRangeOptions() {
|
|
const { beginAtZero } = this.options;
|
|
const { minDefined , maxDefined } = this.getUserBounds();
|
|
let { min , max } = this;
|
|
const setMin = (v)=>min = minDefined ? min : v;
|
|
const setMax = (v)=>max = maxDefined ? max : v;
|
|
if (beginAtZero) {
|
|
const minSign = sign(min);
|
|
const maxSign = sign(max);
|
|
if (minSign < 0 && maxSign < 0) {
|
|
setMax(0);
|
|
} else if (minSign > 0 && maxSign > 0) {
|
|
setMin(0);
|
|
}
|
|
}
|
|
if (min === max) {
|
|
let offset = max === 0 ? 1 : Math.abs(max * 0.05);
|
|
setMax(max + offset);
|
|
if (!beginAtZero) {
|
|
setMin(min - offset);
|
|
}
|
|
}
|
|
this.min = min;
|
|
this.max = max;
|
|
}
|
|
getTickLimit() {
|
|
const tickOpts = this.options.ticks;
|
|
let { maxTicksLimit , stepSize } = tickOpts;
|
|
let maxTicks;
|
|
if (stepSize) {
|
|
maxTicks = Math.ceil(this.max / stepSize) - Math.floor(this.min / stepSize) + 1;
|
|
if (maxTicks > 1000) {
|
|
console.warn(`scales.${this.id}.ticks.stepSize: ${stepSize} would result generating up to ${maxTicks} ticks. Limiting to 1000.`);
|
|
maxTicks = 1000;
|
|
}
|
|
} else {
|
|
maxTicks = this.computeTickLimit();
|
|
maxTicksLimit = maxTicksLimit || 11;
|
|
}
|
|
if (maxTicksLimit) {
|
|
maxTicks = Math.min(maxTicksLimit, maxTicks);
|
|
}
|
|
return maxTicks;
|
|
}
|
|
computeTickLimit() {
|
|
return Number.POSITIVE_INFINITY;
|
|
}
|
|
buildTicks() {
|
|
const opts = this.options;
|
|
const tickOpts = opts.ticks;
|
|
let maxTicks = this.getTickLimit();
|
|
maxTicks = Math.max(2, maxTicks);
|
|
const numericGeneratorOptions = {
|
|
maxTicks,
|
|
bounds: opts.bounds,
|
|
min: opts.min,
|
|
max: opts.max,
|
|
precision: tickOpts.precision,
|
|
step: tickOpts.stepSize,
|
|
count: tickOpts.count,
|
|
maxDigits: this._maxDigits(),
|
|
horizontal: this.isHorizontal(),
|
|
minRotation: tickOpts.minRotation || 0,
|
|
includeBounds: tickOpts.includeBounds !== false
|
|
};
|
|
const dataRange = this._range || this;
|
|
const ticks = generateTicks$1(numericGeneratorOptions, dataRange);
|
|
if (opts.bounds === 'ticks') {
|
|
_setMinAndMaxByKey(ticks, this, 'value');
|
|
}
|
|
if (opts.reverse) {
|
|
ticks.reverse();
|
|
this.start = this.max;
|
|
this.end = this.min;
|
|
} else {
|
|
this.start = this.min;
|
|
this.end = this.max;
|
|
}
|
|
return ticks;
|
|
}
|
|
configure() {
|
|
const ticks = this.ticks;
|
|
let start = this.min;
|
|
let end = this.max;
|
|
super.configure();
|
|
if (this.options.offset && ticks.length) {
|
|
const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2;
|
|
start -= offset;
|
|
end += offset;
|
|
}
|
|
this._startValue = start;
|
|
this._endValue = end;
|
|
this._valueRange = end - start;
|
|
}
|
|
getLabelForValue(value) {
|
|
return formatNumber(value, this.chart.options.locale, this.options.ticks.format);
|
|
}
|
|
}
|
|
|
|
class LinearScale extends LinearScaleBase {
|
|
static id = 'linear';
|
|
static defaults = {
|
|
ticks: {
|
|
callback: Ticks.formatters.numeric
|
|
}
|
|
};
|
|
determineDataLimits() {
|
|
const { min , max } = this.getMinMax(true);
|
|
this.min = isNumberFinite(min) ? min : 0;
|
|
this.max = isNumberFinite(max) ? max : 1;
|
|
this.handleTickRangeOptions();
|
|
}
|
|
computeTickLimit() {
|
|
const horizontal = this.isHorizontal();
|
|
const length = horizontal ? this.width : this.height;
|
|
const minRotation = toRadians(this.options.ticks.minRotation);
|
|
const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001;
|
|
const tickFont = this._resolveTickFontOptions(0);
|
|
return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio));
|
|
}
|
|
getPixelForValue(value) {
|
|
return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange);
|
|
}
|
|
getValueForPixel(pixel) {
|
|
return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange;
|
|
}
|
|
}
|
|
|
|
const log10Floor = (v)=>Math.floor(log10(v));
|
|
const changeExponent = (v, m)=>Math.pow(10, log10Floor(v) + m);
|
|
function isMajor(tickVal) {
|
|
const remain = tickVal / Math.pow(10, log10Floor(tickVal));
|
|
return remain === 1;
|
|
}
|
|
function steps(min, max, rangeExp) {
|
|
const rangeStep = Math.pow(10, rangeExp);
|
|
const start = Math.floor(min / rangeStep);
|
|
const end = Math.ceil(max / rangeStep);
|
|
return end - start;
|
|
}
|
|
function startExp(min, max) {
|
|
const range = max - min;
|
|
let rangeExp = log10Floor(range);
|
|
while(steps(min, max, rangeExp) > 10){
|
|
rangeExp++;
|
|
}
|
|
while(steps(min, max, rangeExp) < 10){
|
|
rangeExp--;
|
|
}
|
|
return Math.min(rangeExp, log10Floor(min));
|
|
}
|
|
function generateTicks(generationOptions, { min , max }) {
|
|
min = finiteOrDefault(generationOptions.min, min);
|
|
const ticks = [];
|
|
const minExp = log10Floor(min);
|
|
let exp = startExp(min, max);
|
|
let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;
|
|
const stepSize = Math.pow(10, exp);
|
|
const base = minExp > exp ? Math.pow(10, minExp) : 0;
|
|
const start = Math.round((min - base) * precision) / precision;
|
|
const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10;
|
|
let significand = Math.floor((start - offset) / Math.pow(10, exp));
|
|
let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision);
|
|
while(value < max){
|
|
ticks.push({
|
|
value,
|
|
major: isMajor(value),
|
|
significand
|
|
});
|
|
if (significand >= 10) {
|
|
significand = significand < 15 ? 15 : 20;
|
|
} else {
|
|
significand++;
|
|
}
|
|
if (significand >= 20) {
|
|
exp++;
|
|
significand = 2;
|
|
precision = exp >= 0 ? 1 : precision;
|
|
}
|
|
value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision;
|
|
}
|
|
const lastTick = finiteOrDefault(generationOptions.max, value);
|
|
ticks.push({
|
|
value: lastTick,
|
|
major: isMajor(lastTick),
|
|
significand
|
|
});
|
|
return ticks;
|
|
}
|
|
class LogarithmicScale extends Scale {
|
|
static id = 'logarithmic';
|
|
static defaults = {
|
|
ticks: {
|
|
callback: Ticks.formatters.logarithmic,
|
|
major: {
|
|
enabled: true
|
|
}
|
|
}
|
|
};
|
|
constructor(cfg){
|
|
super(cfg);
|
|
this.start = undefined;
|
|
this.end = undefined;
|
|
this._startValue = undefined;
|
|
this._valueRange = 0;
|
|
}
|
|
parse(raw, index) {
|
|
const value = LinearScaleBase.prototype.parse.apply(this, [
|
|
raw,
|
|
index
|
|
]);
|
|
if (value === 0) {
|
|
this._zero = true;
|
|
return undefined;
|
|
}
|
|
return isNumberFinite(value) && value > 0 ? value : null;
|
|
}
|
|
determineDataLimits() {
|
|
const { min , max } = this.getMinMax(true);
|
|
this.min = isNumberFinite(min) ? Math.max(0, min) : null;
|
|
this.max = isNumberFinite(max) ? Math.max(0, max) : null;
|
|
if (this.options.beginAtZero) {
|
|
this._zero = true;
|
|
}
|
|
if (this._zero && this.min !== this._suggestedMin && !isNumberFinite(this._userMin)) {
|
|
this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0);
|
|
}
|
|
this.handleTickRangeOptions();
|
|
}
|
|
handleTickRangeOptions() {
|
|
const { minDefined , maxDefined } = this.getUserBounds();
|
|
let min = this.min;
|
|
let max = this.max;
|
|
const setMin = (v)=>min = minDefined ? min : v;
|
|
const setMax = (v)=>max = maxDefined ? max : v;
|
|
if (min === max) {
|
|
if (min <= 0) {
|
|
setMin(1);
|
|
setMax(10);
|
|
} else {
|
|
setMin(changeExponent(min, -1));
|
|
setMax(changeExponent(max, +1));
|
|
}
|
|
}
|
|
if (min <= 0) {
|
|
setMin(changeExponent(max, -1));
|
|
}
|
|
if (max <= 0) {
|
|
setMax(changeExponent(min, +1));
|
|
}
|
|
this.min = min;
|
|
this.max = max;
|
|
}
|
|
buildTicks() {
|
|
const opts = this.options;
|
|
const generationOptions = {
|
|
min: this._userMin,
|
|
max: this._userMax
|
|
};
|
|
const ticks = generateTicks(generationOptions, this);
|
|
if (opts.bounds === 'ticks') {
|
|
_setMinAndMaxByKey(ticks, this, 'value');
|
|
}
|
|
if (opts.reverse) {
|
|
ticks.reverse();
|
|
this.start = this.max;
|
|
this.end = this.min;
|
|
} else {
|
|
this.start = this.min;
|
|
this.end = this.max;
|
|
}
|
|
return ticks;
|
|
}
|
|
getLabelForValue(value) {
|
|
return value === undefined ? '0' : formatNumber(value, this.chart.options.locale, this.options.ticks.format);
|
|
}
|
|
configure() {
|
|
const start = this.min;
|
|
super.configure();
|
|
this._startValue = log10(start);
|
|
this._valueRange = log10(this.max) - log10(start);
|
|
}
|
|
getPixelForValue(value) {
|
|
if (value === undefined || value === 0) {
|
|
value = this.min;
|
|
}
|
|
if (value === null || isNaN(value)) {
|
|
return NaN;
|
|
}
|
|
return this.getPixelForDecimal(value === this.min ? 0 : (log10(value) - this._startValue) / this._valueRange);
|
|
}
|
|
getValueForPixel(pixel) {
|
|
const decimal = this.getDecimalForPixel(pixel);
|
|
return Math.pow(10, this._startValue + decimal * this._valueRange);
|
|
}
|
|
}
|
|
|
|
function getTickBackdropHeight(opts) {
|
|
const tickOpts = opts.ticks;
|
|
if (tickOpts.display && opts.display) {
|
|
const padding = toPadding(tickOpts.backdropPadding);
|
|
return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height;
|
|
}
|
|
return 0;
|
|
}
|
|
function measureLabelSize(ctx, font, label) {
|
|
label = isArray(label) ? label : [
|
|
label
|
|
];
|
|
return {
|
|
w: _longestText(ctx, font.string, label),
|
|
h: label.length * font.lineHeight
|
|
};
|
|
}
|
|
function determineLimits(angle, pos, size, min, max) {
|
|
if (angle === min || angle === max) {
|
|
return {
|
|
start: pos - size / 2,
|
|
end: pos + size / 2
|
|
};
|
|
} else if (angle < min || angle > max) {
|
|
return {
|
|
start: pos - size,
|
|
end: pos
|
|
};
|
|
}
|
|
return {
|
|
start: pos,
|
|
end: pos + size
|
|
};
|
|
}
|
|
function fitWithPointLabels(scale) {
|
|
const orig = {
|
|
l: scale.left + scale._padding.left,
|
|
r: scale.right - scale._padding.right,
|
|
t: scale.top + scale._padding.top,
|
|
b: scale.bottom - scale._padding.bottom
|
|
};
|
|
const limits = Object.assign({}, orig);
|
|
const labelSizes = [];
|
|
const padding = [];
|
|
const valueCount = scale._pointLabels.length;
|
|
const pointLabelOpts = scale.options.pointLabels;
|
|
const additionalAngle = pointLabelOpts.centerPointLabels ? PI / valueCount : 0;
|
|
for(let i = 0; i < valueCount; i++){
|
|
const opts = pointLabelOpts.setContext(scale.getPointLabelContext(i));
|
|
padding[i] = opts.padding;
|
|
const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i], additionalAngle);
|
|
const plFont = toFont(opts.font);
|
|
const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]);
|
|
labelSizes[i] = textSize;
|
|
const angleRadians = _normalizeAngle(scale.getIndexAngle(i) + additionalAngle);
|
|
const angle = Math.round(toDegrees(angleRadians));
|
|
const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180);
|
|
const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270);
|
|
updateLimits(limits, orig, angleRadians, hLimits, vLimits);
|
|
}
|
|
scale.setCenterPoint(orig.l - limits.l, limits.r - orig.r, orig.t - limits.t, limits.b - orig.b);
|
|
scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding);
|
|
}
|
|
function updateLimits(limits, orig, angle, hLimits, vLimits) {
|
|
const sin = Math.abs(Math.sin(angle));
|
|
const cos = Math.abs(Math.cos(angle));
|
|
let x = 0;
|
|
let y = 0;
|
|
if (hLimits.start < orig.l) {
|
|
x = (orig.l - hLimits.start) / sin;
|
|
limits.l = Math.min(limits.l, orig.l - x);
|
|
} else if (hLimits.end > orig.r) {
|
|
x = (hLimits.end - orig.r) / sin;
|
|
limits.r = Math.max(limits.r, orig.r + x);
|
|
}
|
|
if (vLimits.start < orig.t) {
|
|
y = (orig.t - vLimits.start) / cos;
|
|
limits.t = Math.min(limits.t, orig.t - y);
|
|
} else if (vLimits.end > orig.b) {
|
|
y = (vLimits.end - orig.b) / cos;
|
|
limits.b = Math.max(limits.b, orig.b + y);
|
|
}
|
|
}
|
|
function createPointLabelItem(scale, index, itemOpts) {
|
|
const outerDistance = scale.drawingArea;
|
|
const { extra , additionalAngle , padding , size } = itemOpts;
|
|
const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle);
|
|
const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));
|
|
const y = yForAngle(pointLabelPosition.y, size.h, angle);
|
|
const textAlign = getTextAlignForAngle(angle);
|
|
const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);
|
|
return {
|
|
visible: true,
|
|
x: pointLabelPosition.x,
|
|
y,
|
|
textAlign,
|
|
left,
|
|
top: y,
|
|
right: left + size.w,
|
|
bottom: y + size.h
|
|
};
|
|
}
|
|
function isNotOverlapped(item, area) {
|
|
if (!area) {
|
|
return true;
|
|
}
|
|
const { left , top , right , bottom } = item;
|
|
const apexesInArea = _isPointInArea({
|
|
x: left,
|
|
y: top
|
|
}, area) || _isPointInArea({
|
|
x: left,
|
|
y: bottom
|
|
}, area) || _isPointInArea({
|
|
x: right,
|
|
y: top
|
|
}, area) || _isPointInArea({
|
|
x: right,
|
|
y: bottom
|
|
}, area);
|
|
return !apexesInArea;
|
|
}
|
|
function buildPointLabelItems(scale, labelSizes, padding) {
|
|
const items = [];
|
|
const valueCount = scale._pointLabels.length;
|
|
const opts = scale.options;
|
|
const { centerPointLabels , display } = opts.pointLabels;
|
|
const itemOpts = {
|
|
extra: getTickBackdropHeight(opts) / 2,
|
|
additionalAngle: centerPointLabels ? PI / valueCount : 0
|
|
};
|
|
let area;
|
|
for(let i = 0; i < valueCount; i++){
|
|
itemOpts.padding = padding[i];
|
|
itemOpts.size = labelSizes[i];
|
|
const item = createPointLabelItem(scale, i, itemOpts);
|
|
items.push(item);
|
|
if (display === 'auto') {
|
|
item.visible = isNotOverlapped(item, area);
|
|
if (item.visible) {
|
|
area = item;
|
|
}
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
function getTextAlignForAngle(angle) {
|
|
if (angle === 0 || angle === 180) {
|
|
return 'center';
|
|
} else if (angle < 180) {
|
|
return 'left';
|
|
}
|
|
return 'right';
|
|
}
|
|
function leftForTextAlign(x, w, align) {
|
|
if (align === 'right') {
|
|
x -= w;
|
|
} else if (align === 'center') {
|
|
x -= w / 2;
|
|
}
|
|
return x;
|
|
}
|
|
function yForAngle(y, h, angle) {
|
|
if (angle === 90 || angle === 270) {
|
|
y -= h / 2;
|
|
} else if (angle > 270 || angle < 90) {
|
|
y -= h;
|
|
}
|
|
return y;
|
|
}
|
|
function drawPointLabelBox(ctx, opts, item) {
|
|
const { left , top , right , bottom } = item;
|
|
const { backdropColor } = opts;
|
|
if (!isNullOrUndef(backdropColor)) {
|
|
const borderRadius = toTRBLCorners(opts.borderRadius);
|
|
const padding = toPadding(opts.backdropPadding);
|
|
ctx.fillStyle = backdropColor;
|
|
const backdropLeft = left - padding.left;
|
|
const backdropTop = top - padding.top;
|
|
const backdropWidth = right - left + padding.width;
|
|
const backdropHeight = bottom - top + padding.height;
|
|
if (Object.values(borderRadius).some((v)=>v !== 0)) {
|
|
ctx.beginPath();
|
|
addRoundedRectPath(ctx, {
|
|
x: backdropLeft,
|
|
y: backdropTop,
|
|
w: backdropWidth,
|
|
h: backdropHeight,
|
|
radius: borderRadius
|
|
});
|
|
ctx.fill();
|
|
} else {
|
|
ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);
|
|
}
|
|
}
|
|
}
|
|
function drawPointLabels(scale, labelCount) {
|
|
const { ctx , options: { pointLabels } } = scale;
|
|
for(let i = labelCount - 1; i >= 0; i--){
|
|
const item = scale._pointLabelItems[i];
|
|
if (!item.visible) {
|
|
continue;
|
|
}
|
|
const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i));
|
|
drawPointLabelBox(ctx, optsAtIndex, item);
|
|
const plFont = toFont(optsAtIndex.font);
|
|
const { x , y , textAlign } = item;
|
|
renderText(ctx, scale._pointLabels[i], x, y + plFont.lineHeight / 2, plFont, {
|
|
color: optsAtIndex.color,
|
|
textAlign: textAlign,
|
|
textBaseline: 'middle'
|
|
});
|
|
}
|
|
}
|
|
function pathRadiusLine(scale, radius, circular, labelCount) {
|
|
const { ctx } = scale;
|
|
if (circular) {
|
|
ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU);
|
|
} else {
|
|
let pointPosition = scale.getPointPosition(0, radius);
|
|
ctx.moveTo(pointPosition.x, pointPosition.y);
|
|
for(let i = 1; i < labelCount; i++){
|
|
pointPosition = scale.getPointPosition(i, radius);
|
|
ctx.lineTo(pointPosition.x, pointPosition.y);
|
|
}
|
|
}
|
|
}
|
|
function drawRadiusLine(scale, gridLineOpts, radius, labelCount, borderOpts) {
|
|
const ctx = scale.ctx;
|
|
const circular = gridLineOpts.circular;
|
|
const { color , lineWidth } = gridLineOpts;
|
|
if (!circular && !labelCount || !color || !lineWidth || radius < 0) {
|
|
return;
|
|
}
|
|
ctx.save();
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.setLineDash(borderOpts.dash || []);
|
|
ctx.lineDashOffset = borderOpts.dashOffset;
|
|
ctx.beginPath();
|
|
pathRadiusLine(scale, radius, circular, labelCount);
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
function createPointLabelContext(parent, index, label) {
|
|
return createContext(parent, {
|
|
label,
|
|
index,
|
|
type: 'pointLabel'
|
|
});
|
|
}
|
|
class RadialLinearScale extends LinearScaleBase {
|
|
static id = 'radialLinear';
|
|
static defaults = {
|
|
display: true,
|
|
animate: true,
|
|
position: 'chartArea',
|
|
angleLines: {
|
|
display: true,
|
|
lineWidth: 1,
|
|
borderDash: [],
|
|
borderDashOffset: 0.0
|
|
},
|
|
grid: {
|
|
circular: false
|
|
},
|
|
startAngle: 0,
|
|
ticks: {
|
|
showLabelBackdrop: true,
|
|
callback: Ticks.formatters.numeric
|
|
},
|
|
pointLabels: {
|
|
backdropColor: undefined,
|
|
backdropPadding: 2,
|
|
display: true,
|
|
font: {
|
|
size: 10
|
|
},
|
|
callback (label) {
|
|
return label;
|
|
},
|
|
padding: 5,
|
|
centerPointLabels: false
|
|
}
|
|
};
|
|
static defaultRoutes = {
|
|
'angleLines.color': 'borderColor',
|
|
'pointLabels.color': 'color',
|
|
'ticks.color': 'color'
|
|
};
|
|
static descriptors = {
|
|
angleLines: {
|
|
_fallback: 'grid'
|
|
}
|
|
};
|
|
constructor(cfg){
|
|
super(cfg);
|
|
this.xCenter = undefined;
|
|
this.yCenter = undefined;
|
|
this.drawingArea = undefined;
|
|
this._pointLabels = [];
|
|
this._pointLabelItems = [];
|
|
}
|
|
setDimensions() {
|
|
const padding = this._padding = toPadding(getTickBackdropHeight(this.options) / 2);
|
|
const w = this.width = this.maxWidth - padding.width;
|
|
const h = this.height = this.maxHeight - padding.height;
|
|
this.xCenter = Math.floor(this.left + w / 2 + padding.left);
|
|
this.yCenter = Math.floor(this.top + h / 2 + padding.top);
|
|
this.drawingArea = Math.floor(Math.min(w, h) / 2);
|
|
}
|
|
determineDataLimits() {
|
|
const { min , max } = this.getMinMax(false);
|
|
this.min = isNumberFinite(min) && !isNaN(min) ? min : 0;
|
|
this.max = isNumberFinite(max) && !isNaN(max) ? max : 0;
|
|
this.handleTickRangeOptions();
|
|
}
|
|
computeTickLimit() {
|
|
return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options));
|
|
}
|
|
generateTickLabels(ticks) {
|
|
LinearScaleBase.prototype.generateTickLabels.call(this, ticks);
|
|
this._pointLabels = this.getLabels().map((value, index)=>{
|
|
const label = callback(this.options.pointLabels.callback, [
|
|
value,
|
|
index
|
|
], this);
|
|
return label || label === 0 ? label : '';
|
|
}).filter((v, i)=>this.chart.getDataVisibility(i));
|
|
}
|
|
fit() {
|
|
const opts = this.options;
|
|
if (opts.display && opts.pointLabels.display) {
|
|
fitWithPointLabels(this);
|
|
} else {
|
|
this.setCenterPoint(0, 0, 0, 0);
|
|
}
|
|
}
|
|
setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) {
|
|
this.xCenter += Math.floor((leftMovement - rightMovement) / 2);
|
|
this.yCenter += Math.floor((topMovement - bottomMovement) / 2);
|
|
this.drawingArea -= Math.min(this.drawingArea / 2, Math.max(leftMovement, rightMovement, topMovement, bottomMovement));
|
|
}
|
|
getIndexAngle(index) {
|
|
const angleMultiplier = TAU / (this._pointLabels.length || 1);
|
|
const startAngle = this.options.startAngle || 0;
|
|
return _normalizeAngle(index * angleMultiplier + toRadians(startAngle));
|
|
}
|
|
getDistanceFromCenterForValue(value) {
|
|
if (isNullOrUndef(value)) {
|
|
return NaN;
|
|
}
|
|
const scalingFactor = this.drawingArea / (this.max - this.min);
|
|
if (this.options.reverse) {
|
|
return (this.max - value) * scalingFactor;
|
|
}
|
|
return (value - this.min) * scalingFactor;
|
|
}
|
|
getValueForDistanceFromCenter(distance) {
|
|
if (isNullOrUndef(distance)) {
|
|
return NaN;
|
|
}
|
|
const scaledDistance = distance / (this.drawingArea / (this.max - this.min));
|
|
return this.options.reverse ? this.max - scaledDistance : this.min + scaledDistance;
|
|
}
|
|
getPointLabelContext(index) {
|
|
const pointLabels = this._pointLabels || [];
|
|
if (index >= 0 && index < pointLabels.length) {
|
|
const pointLabel = pointLabels[index];
|
|
return createPointLabelContext(this.getContext(), index, pointLabel);
|
|
}
|
|
}
|
|
getPointPosition(index, distanceFromCenter, additionalAngle = 0) {
|
|
const angle = this.getIndexAngle(index) - HALF_PI + additionalAngle;
|
|
return {
|
|
x: Math.cos(angle) * distanceFromCenter + this.xCenter,
|
|
y: Math.sin(angle) * distanceFromCenter + this.yCenter,
|
|
angle
|
|
};
|
|
}
|
|
getPointPositionForValue(index, value) {
|
|
return this.getPointPosition(index, this.getDistanceFromCenterForValue(value));
|
|
}
|
|
getBasePosition(index) {
|
|
return this.getPointPositionForValue(index || 0, this.getBaseValue());
|
|
}
|
|
getPointLabelPosition(index) {
|
|
const { left , top , right , bottom } = this._pointLabelItems[index];
|
|
return {
|
|
left,
|
|
top,
|
|
right,
|
|
bottom
|
|
};
|
|
}
|
|
drawBackground() {
|
|
const { backgroundColor , grid: { circular } } = this.options;
|
|
if (backgroundColor) {
|
|
const ctx = this.ctx;
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
pathRadiusLine(this, this.getDistanceFromCenterForValue(this._endValue), circular, this._pointLabels.length);
|
|
ctx.closePath();
|
|
ctx.fillStyle = backgroundColor;
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
drawGrid() {
|
|
const ctx = this.ctx;
|
|
const opts = this.options;
|
|
const { angleLines , grid , border } = opts;
|
|
const labelCount = this._pointLabels.length;
|
|
let i, offset, position;
|
|
if (opts.pointLabels.display) {
|
|
drawPointLabels(this, labelCount);
|
|
}
|
|
if (grid.display) {
|
|
this.ticks.forEach((tick, index)=>{
|
|
if (index !== 0 || index === 0 && this.min < 0) {
|
|
offset = this.getDistanceFromCenterForValue(tick.value);
|
|
const context = this.getContext(index);
|
|
const optsAtIndex = grid.setContext(context);
|
|
const optsAtIndexBorder = border.setContext(context);
|
|
drawRadiusLine(this, optsAtIndex, offset, labelCount, optsAtIndexBorder);
|
|
}
|
|
});
|
|
}
|
|
if (angleLines.display) {
|
|
ctx.save();
|
|
for(i = labelCount - 1; i >= 0; i--){
|
|
const optsAtIndex = angleLines.setContext(this.getPointLabelContext(i));
|
|
const { color , lineWidth } = optsAtIndex;
|
|
if (!lineWidth || !color) {
|
|
continue;
|
|
}
|
|
ctx.lineWidth = lineWidth;
|
|
ctx.strokeStyle = color;
|
|
ctx.setLineDash(optsAtIndex.borderDash);
|
|
ctx.lineDashOffset = optsAtIndex.borderDashOffset;
|
|
offset = this.getDistanceFromCenterForValue(opts.reverse ? this.min : this.max);
|
|
position = this.getPointPosition(i, offset);
|
|
ctx.beginPath();
|
|
ctx.moveTo(this.xCenter, this.yCenter);
|
|
ctx.lineTo(position.x, position.y);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
}
|
|
}
|
|
drawBorder() {}
|
|
drawLabels() {
|
|
const ctx = this.ctx;
|
|
const opts = this.options;
|
|
const tickOpts = opts.ticks;
|
|
if (!tickOpts.display) {
|
|
return;
|
|
}
|
|
const startAngle = this.getIndexAngle(0);
|
|
let offset, width;
|
|
ctx.save();
|
|
ctx.translate(this.xCenter, this.yCenter);
|
|
ctx.rotate(startAngle);
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
this.ticks.forEach((tick, index)=>{
|
|
if (index === 0 && this.min >= 0 && !opts.reverse) {
|
|
return;
|
|
}
|
|
const optsAtIndex = tickOpts.setContext(this.getContext(index));
|
|
const tickFont = toFont(optsAtIndex.font);
|
|
offset = this.getDistanceFromCenterForValue(this.ticks[index].value);
|
|
if (optsAtIndex.showLabelBackdrop) {
|
|
ctx.font = tickFont.string;
|
|
width = ctx.measureText(tick.label).width;
|
|
ctx.fillStyle = optsAtIndex.backdropColor;
|
|
const padding = toPadding(optsAtIndex.backdropPadding);
|
|
ctx.fillRect(-width / 2 - padding.left, -offset - tickFont.size / 2 - padding.top, width + padding.width, tickFont.size + padding.height);
|
|
}
|
|
renderText(ctx, tick.label, 0, -offset, tickFont, {
|
|
color: optsAtIndex.color,
|
|
strokeColor: optsAtIndex.textStrokeColor,
|
|
strokeWidth: optsAtIndex.textStrokeWidth
|
|
});
|
|
});
|
|
ctx.restore();
|
|
}
|
|
drawTitle() {}
|
|
}
|
|
|
|
const INTERVALS = {
|
|
millisecond: {
|
|
common: true,
|
|
size: 1,
|
|
steps: 1000
|
|
},
|
|
second: {
|
|
common: true,
|
|
size: 1000,
|
|
steps: 60
|
|
},
|
|
minute: {
|
|
common: true,
|
|
size: 60000,
|
|
steps: 60
|
|
},
|
|
hour: {
|
|
common: true,
|
|
size: 3600000,
|
|
steps: 24
|
|
},
|
|
day: {
|
|
common: true,
|
|
size: 86400000,
|
|
steps: 30
|
|
},
|
|
week: {
|
|
common: false,
|
|
size: 604800000,
|
|
steps: 4
|
|
},
|
|
month: {
|
|
common: true,
|
|
size: 2.628e9,
|
|
steps: 12
|
|
},
|
|
quarter: {
|
|
common: false,
|
|
size: 7.884e9,
|
|
steps: 4
|
|
},
|
|
year: {
|
|
common: true,
|
|
size: 3.154e10
|
|
}
|
|
};
|
|
const UNITS = /* #__PURE__ */ Object.keys(INTERVALS);
|
|
function sorter(a, b) {
|
|
return a - b;
|
|
}
|
|
function parse(scale, input) {
|
|
if (isNullOrUndef(input)) {
|
|
return null;
|
|
}
|
|
const adapter = scale._adapter;
|
|
const { parser , round , isoWeekday } = scale._parseOpts;
|
|
let value = input;
|
|
if (typeof parser === 'function') {
|
|
value = parser(value);
|
|
}
|
|
if (!isNumberFinite(value)) {
|
|
value = typeof parser === 'string' ? adapter.parse(value, parser) : adapter.parse(value);
|
|
}
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
if (round) {
|
|
value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true) ? adapter.startOf(value, 'isoWeek', isoWeekday) : adapter.startOf(value, round);
|
|
}
|
|
return +value;
|
|
}
|
|
function determineUnitForAutoTicks(minUnit, min, max, capacity) {
|
|
const ilen = UNITS.length;
|
|
for(let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i){
|
|
const interval = INTERVALS[UNITS[i]];
|
|
const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER;
|
|
if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) {
|
|
return UNITS[i];
|
|
}
|
|
}
|
|
return UNITS[ilen - 1];
|
|
}
|
|
function determineUnitForFormatting(scale, numTicks, minUnit, min, max) {
|
|
for(let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--){
|
|
const unit = UNITS[i];
|
|
if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) {
|
|
return unit;
|
|
}
|
|
}
|
|
return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0];
|
|
}
|
|
function determineMajorUnit(unit) {
|
|
for(let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i){
|
|
if (INTERVALS[UNITS[i]].common) {
|
|
return UNITS[i];
|
|
}
|
|
}
|
|
}
|
|
function addTick(ticks, time, timestamps) {
|
|
if (!timestamps) {
|
|
ticks[time] = true;
|
|
} else if (timestamps.length) {
|
|
const { lo , hi } = _lookup(timestamps, time);
|
|
const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi];
|
|
ticks[timestamp] = true;
|
|
}
|
|
}
|
|
function setMajorTicks(scale, ticks, map, majorUnit) {
|
|
const adapter = scale._adapter;
|
|
const first = +adapter.startOf(ticks[0].value, majorUnit);
|
|
const last = ticks[ticks.length - 1].value;
|
|
let major, index;
|
|
for(major = first; major <= last; major = +adapter.add(major, 1, majorUnit)){
|
|
index = map[major];
|
|
if (index >= 0) {
|
|
ticks[index].major = true;
|
|
}
|
|
}
|
|
return ticks;
|
|
}
|
|
function ticksFromTimestamps(scale, values, majorUnit) {
|
|
const ticks = [];
|
|
const map = {};
|
|
const ilen = values.length;
|
|
let i, value;
|
|
for(i = 0; i < ilen; ++i){
|
|
value = values[i];
|
|
map[value] = i;
|
|
ticks.push({
|
|
value,
|
|
major: false
|
|
});
|
|
}
|
|
return ilen === 0 || !majorUnit ? ticks : setMajorTicks(scale, ticks, map, majorUnit);
|
|
}
|
|
class TimeScale extends Scale {
|
|
static id = 'time';
|
|
static defaults = {
|
|
bounds: 'data',
|
|
adapters: {},
|
|
time: {
|
|
parser: false,
|
|
unit: false,
|
|
round: false,
|
|
isoWeekday: false,
|
|
minUnit: 'millisecond',
|
|
displayFormats: {}
|
|
},
|
|
ticks: {
|
|
source: 'auto',
|
|
callback: false,
|
|
major: {
|
|
enabled: false
|
|
}
|
|
}
|
|
};
|
|
constructor(props){
|
|
super(props);
|
|
this._cache = {
|
|
data: [],
|
|
labels: [],
|
|
all: []
|
|
};
|
|
this._unit = 'day';
|
|
this._majorUnit = undefined;
|
|
this._offsets = {};
|
|
this._normalized = false;
|
|
this._parseOpts = undefined;
|
|
}
|
|
init(scaleOpts, opts = {}) {
|
|
const time = scaleOpts.time || (scaleOpts.time = {});
|
|
const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date);
|
|
adapter.init(opts);
|
|
mergeIf(time.displayFormats, adapter.formats());
|
|
this._parseOpts = {
|
|
parser: time.parser,
|
|
round: time.round,
|
|
isoWeekday: time.isoWeekday
|
|
};
|
|
super.init(scaleOpts);
|
|
this._normalized = opts.normalized;
|
|
}
|
|
parse(raw, index) {
|
|
if (raw === undefined) {
|
|
return null;
|
|
}
|
|
return parse(this, raw);
|
|
}
|
|
beforeLayout() {
|
|
super.beforeLayout();
|
|
this._cache = {
|
|
data: [],
|
|
labels: [],
|
|
all: []
|
|
};
|
|
}
|
|
determineDataLimits() {
|
|
const options = this.options;
|
|
const adapter = this._adapter;
|
|
const unit = options.time.unit || 'day';
|
|
let { min , max , minDefined , maxDefined } = this.getUserBounds();
|
|
function _applyBounds(bounds) {
|
|
if (!minDefined && !isNaN(bounds.min)) {
|
|
min = Math.min(min, bounds.min);
|
|
}
|
|
if (!maxDefined && !isNaN(bounds.max)) {
|
|
max = Math.max(max, bounds.max);
|
|
}
|
|
}
|
|
if (!minDefined || !maxDefined) {
|
|
_applyBounds(this._getLabelBounds());
|
|
if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') {
|
|
_applyBounds(this.getMinMax(false));
|
|
}
|
|
}
|
|
min = isNumberFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);
|
|
max = isNumberFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1;
|
|
this.min = Math.min(min, max - 1);
|
|
this.max = Math.max(min + 1, max);
|
|
}
|
|
_getLabelBounds() {
|
|
const arr = this.getLabelTimestamps();
|
|
let min = Number.POSITIVE_INFINITY;
|
|
let max = Number.NEGATIVE_INFINITY;
|
|
if (arr.length) {
|
|
min = arr[0];
|
|
max = arr[arr.length - 1];
|
|
}
|
|
return {
|
|
min,
|
|
max
|
|
};
|
|
}
|
|
buildTicks() {
|
|
const options = this.options;
|
|
const timeOpts = options.time;
|
|
const tickOpts = options.ticks;
|
|
const timestamps = tickOpts.source === 'labels' ? this.getLabelTimestamps() : this._generate();
|
|
if (options.bounds === 'ticks' && timestamps.length) {
|
|
this.min = this._userMin || timestamps[0];
|
|
this.max = this._userMax || timestamps[timestamps.length - 1];
|
|
}
|
|
const min = this.min;
|
|
const max = this.max;
|
|
const ticks = _filterBetween(timestamps, min, max);
|
|
this._unit = timeOpts.unit || (tickOpts.autoSkip ? determineUnitForAutoTicks(timeOpts.minUnit, this.min, this.max, this._getLabelCapacity(min)) : determineUnitForFormatting(this, ticks.length, timeOpts.minUnit, this.min, this.max));
|
|
this._majorUnit = !tickOpts.major.enabled || this._unit === 'year' ? undefined : determineMajorUnit(this._unit);
|
|
this.initOffsets(timestamps);
|
|
if (options.reverse) {
|
|
ticks.reverse();
|
|
}
|
|
return ticksFromTimestamps(this, ticks, this._majorUnit);
|
|
}
|
|
afterAutoSkip() {
|
|
if (this.options.offsetAfterAutoskip) {
|
|
this.initOffsets(this.ticks.map((tick)=>+tick.value));
|
|
}
|
|
}
|
|
initOffsets(timestamps = []) {
|
|
let start = 0;
|
|
let end = 0;
|
|
let first, last;
|
|
if (this.options.offset && timestamps.length) {
|
|
first = this.getDecimalForValue(timestamps[0]);
|
|
if (timestamps.length === 1) {
|
|
start = 1 - first;
|
|
} else {
|
|
start = (this.getDecimalForValue(timestamps[1]) - first) / 2;
|
|
}
|
|
last = this.getDecimalForValue(timestamps[timestamps.length - 1]);
|
|
if (timestamps.length === 1) {
|
|
end = last;
|
|
} else {
|
|
end = (last - this.getDecimalForValue(timestamps[timestamps.length - 2])) / 2;
|
|
}
|
|
}
|
|
const limit = timestamps.length < 3 ? 0.5 : 0.25;
|
|
start = _limitValue(start, 0, limit);
|
|
end = _limitValue(end, 0, limit);
|
|
this._offsets = {
|
|
start,
|
|
end,
|
|
factor: 1 / (start + 1 + end)
|
|
};
|
|
}
|
|
_generate() {
|
|
const adapter = this._adapter;
|
|
const min = this.min;
|
|
const max = this.max;
|
|
const options = this.options;
|
|
const timeOpts = options.time;
|
|
const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, this._getLabelCapacity(min));
|
|
const stepSize = valueOrDefault(options.ticks.stepSize, 1);
|
|
const weekday = minor === 'week' ? timeOpts.isoWeekday : false;
|
|
const hasWeekday = isNumber(weekday) || weekday === true;
|
|
const ticks = {};
|
|
let first = min;
|
|
let time, count;
|
|
if (hasWeekday) {
|
|
first = +adapter.startOf(first, 'isoWeek', weekday);
|
|
}
|
|
first = +adapter.startOf(first, hasWeekday ? 'day' : minor);
|
|
if (adapter.diff(max, min, minor) > 100000 * stepSize) {
|
|
throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor);
|
|
}
|
|
const timestamps = options.ticks.source === 'data' && this.getDataTimestamps();
|
|
for(time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++){
|
|
addTick(ticks, time, timestamps);
|
|
}
|
|
if (time === max || options.bounds === 'ticks' || count === 1) {
|
|
addTick(ticks, time, timestamps);
|
|
}
|
|
return Object.keys(ticks).sort(sorter).map((x)=>+x);
|
|
}
|
|
getLabelForValue(value) {
|
|
const adapter = this._adapter;
|
|
const timeOpts = this.options.time;
|
|
if (timeOpts.tooltipFormat) {
|
|
return adapter.format(value, timeOpts.tooltipFormat);
|
|
}
|
|
return adapter.format(value, timeOpts.displayFormats.datetime);
|
|
}
|
|
format(value, format) {
|
|
const options = this.options;
|
|
const formats = options.time.displayFormats;
|
|
const unit = this._unit;
|
|
const fmt = format || formats[unit];
|
|
return this._adapter.format(value, fmt);
|
|
}
|
|
_tickFormatFunction(time, index, ticks, format) {
|
|
const options = this.options;
|
|
const formatter = options.ticks.callback;
|
|
if (formatter) {
|
|
return callback(formatter, [
|
|
time,
|
|
index,
|
|
ticks
|
|
], this);
|
|
}
|
|
const formats = options.time.displayFormats;
|
|
const unit = this._unit;
|
|
const majorUnit = this._majorUnit;
|
|
const minorFormat = unit && formats[unit];
|
|
const majorFormat = majorUnit && formats[majorUnit];
|
|
const tick = ticks[index];
|
|
const major = majorUnit && majorFormat && tick && tick.major;
|
|
return this._adapter.format(time, format || (major ? majorFormat : minorFormat));
|
|
}
|
|
generateTickLabels(ticks) {
|
|
let i, ilen, tick;
|
|
for(i = 0, ilen = ticks.length; i < ilen; ++i){
|
|
tick = ticks[i];
|
|
tick.label = this._tickFormatFunction(tick.value, i, ticks);
|
|
}
|
|
}
|
|
getDecimalForValue(value) {
|
|
return value === null ? NaN : (value - this.min) / (this.max - this.min);
|
|
}
|
|
getPixelForValue(value) {
|
|
const offsets = this._offsets;
|
|
const pos = this.getDecimalForValue(value);
|
|
return this.getPixelForDecimal((offsets.start + pos) * offsets.factor);
|
|
}
|
|
getValueForPixel(pixel) {
|
|
const offsets = this._offsets;
|
|
const pos = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
|
|
return this.min + pos * (this.max - this.min);
|
|
}
|
|
_getLabelSize(label) {
|
|
const ticksOpts = this.options.ticks;
|
|
const tickLabelWidth = this.ctx.measureText(label).width;
|
|
const angle = toRadians(this.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation);
|
|
const cosRotation = Math.cos(angle);
|
|
const sinRotation = Math.sin(angle);
|
|
const tickFontSize = this._resolveTickFontOptions(0).size;
|
|
return {
|
|
w: tickLabelWidth * cosRotation + tickFontSize * sinRotation,
|
|
h: tickLabelWidth * sinRotation + tickFontSize * cosRotation
|
|
};
|
|
}
|
|
_getLabelCapacity(exampleTime) {
|
|
const timeOpts = this.options.time;
|
|
const displayFormats = timeOpts.displayFormats;
|
|
const format = displayFormats[timeOpts.unit] || displayFormats.millisecond;
|
|
const exampleLabel = this._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(this, [
|
|
exampleTime
|
|
], this._majorUnit), format);
|
|
const size = this._getLabelSize(exampleLabel);
|
|
const capacity = Math.floor(this.isHorizontal() ? this.width / size.w : this.height / size.h) - 1;
|
|
return capacity > 0 ? capacity : 1;
|
|
}
|
|
getDataTimestamps() {
|
|
let timestamps = this._cache.data || [];
|
|
let i, ilen;
|
|
if (timestamps.length) {
|
|
return timestamps;
|
|
}
|
|
const metas = this.getMatchingVisibleMetas();
|
|
if (this._normalized && metas.length) {
|
|
return this._cache.data = metas[0].controller.getAllParsedValues(this);
|
|
}
|
|
for(i = 0, ilen = metas.length; i < ilen; ++i){
|
|
timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(this));
|
|
}
|
|
return this._cache.data = this.normalize(timestamps);
|
|
}
|
|
getLabelTimestamps() {
|
|
const timestamps = this._cache.labels || [];
|
|
let i, ilen;
|
|
if (timestamps.length) {
|
|
return timestamps;
|
|
}
|
|
const labels = this.getLabels();
|
|
for(i = 0, ilen = labels.length; i < ilen; ++i){
|
|
timestamps.push(parse(this, labels[i]));
|
|
}
|
|
return this._cache.labels = this._normalized ? timestamps : this.normalize(timestamps);
|
|
}
|
|
normalize(values) {
|
|
return _arrayUnique(values.sort(sorter));
|
|
}
|
|
}
|
|
|
|
function chart_interpolate(table, val, reverse) {
|
|
let lo = 0;
|
|
let hi = table.length - 1;
|
|
let prevSource, nextSource, prevTarget, nextTarget;
|
|
if (reverse) {
|
|
if (val >= table[lo].pos && val <= table[hi].pos) {
|
|
({ lo , hi } = _lookupByKey(table, 'pos', val));
|
|
}
|
|
({ pos: prevSource , time: prevTarget } = table[lo]);
|
|
({ pos: nextSource , time: nextTarget } = table[hi]);
|
|
} else {
|
|
if (val >= table[lo].time && val <= table[hi].time) {
|
|
({ lo , hi } = _lookupByKey(table, 'time', val));
|
|
}
|
|
({ time: prevSource , pos: prevTarget } = table[lo]);
|
|
({ time: nextSource , pos: nextTarget } = table[hi]);
|
|
}
|
|
const span = nextSource - prevSource;
|
|
return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget;
|
|
}
|
|
class TimeSeriesScale extends TimeScale {
|
|
static id = 'timeseries';
|
|
static defaults = TimeScale.defaults;
|
|
constructor(props){
|
|
super(props);
|
|
this._table = [];
|
|
this._minPos = undefined;
|
|
this._tableRange = undefined;
|
|
}
|
|
initOffsets() {
|
|
const timestamps = this._getTimestampsForTable();
|
|
const table = this._table = this.buildLookupTable(timestamps);
|
|
this._minPos = chart_interpolate(table, this.min);
|
|
this._tableRange = chart_interpolate(table, this.max) - this._minPos;
|
|
super.initOffsets(timestamps);
|
|
}
|
|
buildLookupTable(timestamps) {
|
|
const { min , max } = this;
|
|
const items = [];
|
|
const table = [];
|
|
let i, ilen, prev, curr, next;
|
|
for(i = 0, ilen = timestamps.length; i < ilen; ++i){
|
|
curr = timestamps[i];
|
|
if (curr >= min && curr <= max) {
|
|
items.push(curr);
|
|
}
|
|
}
|
|
if (items.length < 2) {
|
|
return [
|
|
{
|
|
time: min,
|
|
pos: 0
|
|
},
|
|
{
|
|
time: max,
|
|
pos: 1
|
|
}
|
|
];
|
|
}
|
|
for(i = 0, ilen = items.length; i < ilen; ++i){
|
|
next = items[i + 1];
|
|
prev = items[i - 1];
|
|
curr = items[i];
|
|
if (Math.round((next + prev) / 2) !== curr) {
|
|
table.push({
|
|
time: curr,
|
|
pos: i / (ilen - 1)
|
|
});
|
|
}
|
|
}
|
|
return table;
|
|
}
|
|
_generate() {
|
|
const min = this.min;
|
|
const max = this.max;
|
|
let timestamps = super.getDataTimestamps();
|
|
if (!timestamps.includes(min) || !timestamps.length) {
|
|
timestamps.splice(0, 0, min);
|
|
}
|
|
if (!timestamps.includes(max) || timestamps.length === 1) {
|
|
timestamps.push(max);
|
|
}
|
|
return timestamps.sort((a, b)=>a - b);
|
|
}
|
|
_getTimestampsForTable() {
|
|
let timestamps = this._cache.all || [];
|
|
if (timestamps.length) {
|
|
return timestamps;
|
|
}
|
|
const data = this.getDataTimestamps();
|
|
const label = this.getLabelTimestamps();
|
|
if (data.length && label.length) {
|
|
timestamps = this.normalize(data.concat(label));
|
|
} else {
|
|
timestamps = data.length ? data : label;
|
|
}
|
|
timestamps = this._cache.all = timestamps;
|
|
return timestamps;
|
|
}
|
|
getDecimalForValue(value) {
|
|
return (chart_interpolate(this._table, value) - this._minPos) / this._tableRange;
|
|
}
|
|
getValueForPixel(pixel) {
|
|
const offsets = this._offsets;
|
|
const decimal = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
|
|
return chart_interpolate(this._table, decimal * this._tableRange + this._minPos, true);
|
|
}
|
|
}
|
|
|
|
var scales = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
CategoryScale: CategoryScale,
|
|
LinearScale: LinearScale,
|
|
LogarithmicScale: LogarithmicScale,
|
|
RadialLinearScale: RadialLinearScale,
|
|
TimeScale: TimeScale,
|
|
TimeSeriesScale: TimeSeriesScale
|
|
});
|
|
|
|
const registerables = [
|
|
controllers,
|
|
chart_elements,
|
|
plugins,
|
|
scales
|
|
];
|
|
|
|
|
|
//# sourceMappingURL=chart.js.map
|
|
|
|
;// ../assets/scripts/constants/colors.js
|
|
const COLORS = {
|
|
'white': '#ffffff',
|
|
'red-50': '#ffebee',
|
|
'red-100': '#ffcdd2',
|
|
'red-200': '#ef9a9a',
|
|
'red-300': '#e57373',
|
|
'red-400': '#ef5350',
|
|
'red-500': '#f44336',
|
|
'red-600': '#e53935',
|
|
'red-700': '#d32f2f',
|
|
'red-800': '#c62828',
|
|
'red-900': '#b71c1c',
|
|
'red-a100': '#ff8a80',
|
|
'red-a200': '#ff5252',
|
|
'red-a400': '#ff1744',
|
|
'red-a700': '#d50000',
|
|
'pink-50': '#fce4ec',
|
|
'pink-100': '#f8bbd0',
|
|
'pink-200': '#f48fb1',
|
|
'pink-300': '#f06292',
|
|
'pink-400': '#ec407a',
|
|
'pink-500': '#e91e63',
|
|
'pink-600': '#d81b60',
|
|
'pink-700': '#c2185b',
|
|
'pink-800': '#ad1457',
|
|
'pink-900': '#880e4f',
|
|
'pink-a100': '#ff80ab',
|
|
'pink-a200': '#ff4081',
|
|
'pink-a400': '#f50057',
|
|
'pink-a700': '#c51162',
|
|
'purple-50': '#f3e5f5',
|
|
'purple-100': '#e1bee7',
|
|
'purple-200': '#ce93d8',
|
|
'purple-300': '#ba68c8',
|
|
'purple-400': '#ab47bc',
|
|
'purple-500': '#9c27b0',
|
|
'purple-600': '#8e24aa',
|
|
'purple-700': '#7b1fa2',
|
|
'purple-800': '#6a1b9a',
|
|
'purple-900': '#4a148c',
|
|
'purple-a100': '#ea80fc',
|
|
'purple-a200': '#e040fb',
|
|
'purple-a400': '#d500f9',
|
|
'purple-a700': '#aa00ff',
|
|
'deep-purple-50': '#ede7f6',
|
|
'deep-purple-100': '#d1c4e9',
|
|
'deep-purple-200': '#b39ddb',
|
|
'deep-purple-300': '#9575cd',
|
|
'deep-purple-400': '#7e57c2',
|
|
'deep-purple-500': '#673ab7',
|
|
'deep-purple-600': '#5e35b1',
|
|
'deep-purple-700': '#512da8',
|
|
'deep-purple-800': '#4527a0',
|
|
'deep-purple-900': '#311b92',
|
|
'deep-purple-a100': '#b388ff',
|
|
'deep-purple-a200': '#7c4dff',
|
|
'deep-purple-a400': '#651fff',
|
|
'deep-purple-a700': '#6200ea',
|
|
'indigo-50': '#e8eaf6',
|
|
'indigo-100': '#c5cae9',
|
|
'indigo-200': '#9fa8da',
|
|
'indigo-300': '#7986cb',
|
|
'indigo-400': '#5c6bc0',
|
|
'indigo-500': '#3f51b5',
|
|
'indigo-600': '#3949ab',
|
|
'indigo-700': '#303f9f',
|
|
'indigo-800': '#283593',
|
|
'indigo-900': '#1a237e',
|
|
'indigo-a100': '#8c9eff',
|
|
'indigo-a200': '#536dfe',
|
|
'indigo-a400': '#3d5afe',
|
|
'indigo-a700': '#304ffe',
|
|
'blue-50': '#e3f2fd',
|
|
'blue-100': '#bbdefb',
|
|
'blue-200': '#90caf9',
|
|
'blue-300': '#64b5f6',
|
|
'blue-400': '#42a5f5',
|
|
'blue-500': '#2196f3',
|
|
'blue-600': '#1e88e5',
|
|
'blue-700': '#1976d2',
|
|
'blue-800': '#1565c0',
|
|
'blue-900': '#0d47a1',
|
|
'blue-a100': '#82b1ff',
|
|
'blue-a200': '#448aff',
|
|
'blue-a400': '#2979ff',
|
|
'blue-a700': '#2962ff',
|
|
'light-blue-50': '#e1f5fe',
|
|
'light-blue-100': '#b3e5fc',
|
|
'light-blue-200': '#81d4fa',
|
|
'light-blue-300': '#4fc3f7',
|
|
'light-blue-400': '#29b6f6',
|
|
'light-blue-500': '#03a9f4',
|
|
'light-blue-600': '#039be5',
|
|
'light-blue-700': '#0288d1',
|
|
'light-blue-800': '#0277bd',
|
|
'light-blue-900': '#01579b',
|
|
'light-blue-a100': '#80d8ff',
|
|
'light-blue-a200': '#40c4ff',
|
|
'light-blue-a400': '#00b0ff',
|
|
'light-blue-a700': '#0091ea',
|
|
'cyan-50': '#e0f7fa',
|
|
'cyan-100': '#b2ebf2',
|
|
'cyan-200': '#80deea',
|
|
'cyan-300': '#4dd0e1',
|
|
'cyan-400': '#26c6da',
|
|
'cyan-500': '#00bcd4',
|
|
'cyan-600': '#00acc1',
|
|
'cyan-700': '#0097a7',
|
|
'cyan-800': '#00838f',
|
|
'cyan-900': '#006064',
|
|
'cyan-a100': '#84ffff',
|
|
'cyan-a200': '#18ffff',
|
|
'cyan-a400': '#00e5ff',
|
|
'cyan-a700': '#00b8d4',
|
|
'teal-50': '#e0f2f1',
|
|
'teal-100': '#b2dfdb',
|
|
'teal-200': '#80cbc4',
|
|
'teal-300': '#4db6ac',
|
|
'teal-400': '#26a69a',
|
|
'teal-500': '#009688',
|
|
'teal-600': '#00897b',
|
|
'teal-700': '#00796b',
|
|
'teal-800': '#00695c',
|
|
'teal-900': '#004d40',
|
|
'teal-a100': '#a7ffeb',
|
|
'teal-a200': '#64ffda',
|
|
'teal-a400': '#1de9b6',
|
|
'teal-a700': '#00bfa5',
|
|
'green-50': '#e8f5e9',
|
|
'green-100': '#c8e6c9',
|
|
'green-200': '#a5d6a7',
|
|
'green-300': '#81c784',
|
|
'green-400': '#66bb6a',
|
|
'green-500': '#4caf50',
|
|
'green-600': '#43a047',
|
|
'green-700': '#388e3c',
|
|
'green-800': '#2e7d32',
|
|
'green-900': '#1b5e20',
|
|
'green-a100': '#b9f6ca',
|
|
'green-a200': '#69f0ae',
|
|
'green-a400': '#00e676',
|
|
'green-a700': '#00c853',
|
|
'light-green-50': '#f1f8e9',
|
|
'light-green-100': '#dcedc8',
|
|
'light-green-200': '#c5e1a5',
|
|
'light-green-300': '#aed581',
|
|
'light-green-400': '#9ccc65',
|
|
'light-green-500': '#8bc34a',
|
|
'light-green-600': '#7cb342',
|
|
'light-green-700': '#689f38',
|
|
'light-green-800': '#558b2f',
|
|
'light-green-900': '#33691e',
|
|
'light-green-a100': '#ccff90',
|
|
'light-green-a200': '#b2ff59',
|
|
'light-green-a400': '#76ff03',
|
|
'light-green-a700': '#64dd17',
|
|
'lime-50': '#f9fbe7',
|
|
'lime-100': '#f0f4c3',
|
|
'lime-200': '#e6ee9c',
|
|
'lime-300': '#dce775',
|
|
'lime-400': '#d4e157',
|
|
'lime-500': '#cddc39',
|
|
'lime-600': '#c0ca33',
|
|
'lime-700': '#afb42b',
|
|
'lime-800': '#9e9d24',
|
|
'lime-900': '#827717',
|
|
'lime-a100': '#f4ff81',
|
|
'lime-a200': '#eeff41',
|
|
'lime-a400': '#c6ff00',
|
|
'lime-a700': '#aeea00',
|
|
'yellow-50': '#fffde7',
|
|
'yellow-100': '#fff9c4',
|
|
'yellow-200': '#fff59d',
|
|
'yellow-300': '#fff176',
|
|
'yellow-400': '#ffee58',
|
|
'yellow-500': '#ffeb3b',
|
|
'yellow-600': '#fdd835',
|
|
'yellow-700': '#fbc02d',
|
|
'yellow-800': '#f9a825',
|
|
'yellow-900': '#f57f17',
|
|
'yellow-a100': '#ffff8d',
|
|
'yellow-a200': '#ffff00',
|
|
'yellow-a400': '#ffea00',
|
|
'yellow-a700': '#ffd600',
|
|
'amber-50': '#fff8e1',
|
|
'amber-100': '#ffecb3',
|
|
'amber-200': '#ffe082',
|
|
'amber-300': '#ffd54f',
|
|
'amber-400': '#ffca28',
|
|
'amber-500': '#ffc107',
|
|
'amber-600': '#ffb300',
|
|
'amber-700': '#ffa000',
|
|
'amber-800': '#ff8f00',
|
|
'amber-900': '#ff6f00',
|
|
'amber-a100': '#ffe57f',
|
|
'amber-a200': '#ffd740',
|
|
'amber-a400': '#ffc400',
|
|
'amber-a700': '#ffab00',
|
|
'orange-50': '#fff3e0',
|
|
'orange-100': '#ffe0b2',
|
|
'orange-200': '#ffcc80',
|
|
'orange-300': '#ffb74d',
|
|
'orange-400': '#ffa726',
|
|
'orange-500': '#ff9800',
|
|
'orange-600': '#fb8c00',
|
|
'orange-700': '#f57c00',
|
|
'orange-800': '#ef6c00',
|
|
'orange-900': '#e65100',
|
|
'orange-a100': '#ffd180',
|
|
'orange-a200': '#ffab40',
|
|
'orange-a400': '#ff9100',
|
|
'orange-a700': '#ff6d00',
|
|
'deep-orange-50': '#fbe9e7',
|
|
'deep-orange-100': '#ffccbc',
|
|
'deep-orange-200': '#ffab91',
|
|
'deep-orange-300': '#ff8a65',
|
|
'deep-orange-400': '#ff7043',
|
|
'deep-orange-500': '#ff5722',
|
|
'deep-orange-600': '#f4511e',
|
|
'deep-orange-700': '#e64a19',
|
|
'deep-orange-800': '#d84315',
|
|
'deep-orange-900': '#bf360c',
|
|
'deep-orange-a100': '#ff9e80',
|
|
'deep-orange-a200': '#ff6e40',
|
|
'deep-orange-a400': '#ff3d00',
|
|
'deep-orange-a700': '#dd2c00',
|
|
'brown-50': '#efebe9',
|
|
'brown-100': '#d7ccc8',
|
|
'brown-200': '#bcaaa4',
|
|
'brown-300': '#a1887f',
|
|
'brown-400': '#8d6e63',
|
|
'brown-500': '#795548',
|
|
'brown-600': '#6d4c41',
|
|
'brown-700': '#5d4037',
|
|
'brown-800': '#4e342e',
|
|
'brown-900': '#3e2723',
|
|
'grey-50': '#fafafa',
|
|
'grey-100': '#f5f5f5',
|
|
'grey-200': '#eeeeee',
|
|
'grey-300': '#e0e0e0',
|
|
'grey-400': '#bdbdbd',
|
|
'grey-500': '#9e9e9e',
|
|
'grey-600': '#757575',
|
|
'grey-700': '#616161',
|
|
'grey-800': '#424242',
|
|
'grey-900': '#212121',
|
|
'blue-grey-50': '#eceff1',
|
|
'blue-grey-100': '#cfd8dc',
|
|
'blue-grey-200': '#b0bec5',
|
|
'blue-grey-300': '#90a4ae',
|
|
'blue-grey-400': '#78909c',
|
|
'blue-grey-500': '#607d8b',
|
|
'blue-grey-600': '#546e7a',
|
|
'blue-grey-700': '#455a64',
|
|
'blue-grey-800': '#37474f',
|
|
'blue-grey-900': '#263238'
|
|
};
|
|
const GREYS = {
|
|
'grey-100': '#f9fafb',
|
|
'grey-200': '#f2f3f5',
|
|
'grey-300': '#e6eaf0',
|
|
'grey-400': '#d3d9e3',
|
|
'grey-500': '#b9c2d0',
|
|
'grey-600': '#7c8695',
|
|
'grey-700': '#72777a',
|
|
'grey-800': '#565a5c',
|
|
'grey-900': '#313435'
|
|
};
|
|
|
|
;// ../assets/scripts/components/Chart.js
|
|
/**
|
|
* Modern Chart Component
|
|
* Replaces jQuery Sparkline with Chart.js
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Register Chart.js components
|
|
chart_Chart.register(...registerables);
|
|
class ChartComponent {
|
|
constructor() {
|
|
this.charts = new Map(); // Store chart instances
|
|
this.debounceTimer = null;
|
|
this.init();
|
|
}
|
|
init() {
|
|
// Only disable resizing for small sparkline charts
|
|
this.createSparklines();
|
|
this.createOtherCharts();
|
|
this.setupResizeHandler();
|
|
}
|
|
|
|
/**
|
|
* Create sparklines (only for dashboard page)
|
|
*/
|
|
createSparklines() {
|
|
// Only create sparklines if we're on a page that has them
|
|
const sparklineExists = document.getElementById('sparklinedash');
|
|
if (!sparklineExists) {
|
|
return;
|
|
}
|
|
const sparklineConfigs = [{
|
|
id: 'sparklinedash',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#4caf50'
|
|
}, {
|
|
id: 'sparklinedash2',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#9675ce'
|
|
}, {
|
|
id: 'sparklinedash3',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#03a9f3'
|
|
}, {
|
|
id: 'sparklinedash4',
|
|
data: [0, 5, 6, 10, 9, 12, 4, 9],
|
|
color: '#f96262'
|
|
}];
|
|
sparklineConfigs.forEach(config => {
|
|
// Only create if the target element exists
|
|
if (document.getElementById(config.id)) {
|
|
this.createSparklineChart(config);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create sparkline chart from configuration
|
|
*/
|
|
createSparklineChart(_ref) {
|
|
let {
|
|
id,
|
|
data,
|
|
color
|
|
} = _ref;
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if we have a valid target element
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace the span with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
// Set canvas dimensions to match original sparkline
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: color,
|
|
borderColor: color,
|
|
borderWidth: 0,
|
|
barPercentage: 0.6,
|
|
categoryPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
},
|
|
elements: {
|
|
bar: {
|
|
borderRadius: 1
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create other chart types (only if they exist on the page)
|
|
*/
|
|
createOtherCharts() {
|
|
// Determine if we're on the dashboard or charts page
|
|
const isChartsPage = document.getElementById('area-chart') !== null;
|
|
const isDashboard = !isChartsPage && document.getElementById('line-chart') !== null;
|
|
|
|
// Create Monthly Stats chart with enhanced dual-line data (dashboard only)
|
|
if (isDashboard) {
|
|
this.createMonthlyStatsChart();
|
|
}
|
|
|
|
// Charts page specific charts (only on charts page)
|
|
if (isChartsPage) {
|
|
this.createChartsPageCharts();
|
|
}
|
|
|
|
// Only create charts if their target elements exist
|
|
if (document.getElementById('sparkline')) {
|
|
this.createLineChart('sparkline', [5, 6, 7, 9, 9, 5, 3, 2, 2, 4, 6, 7]);
|
|
}
|
|
if (document.getElementById('compositebar')) {
|
|
this.createCompositeChart('compositebar', [4, 1, 5, 7, 9, 9, 8, 7, 6, 6, 4, 7, 8, 4, 3, 2, 2, 5, 6, 7]);
|
|
}
|
|
|
|
// Regular sparklines with custom colors (only on pages that have them)
|
|
this.createCustomSparklines();
|
|
|
|
// Easy Pie Charts (only if they exist)
|
|
this.createEasyPieCharts();
|
|
}
|
|
|
|
/**
|
|
* Create enhanced Monthly Stats chart with dual lines and more data
|
|
*/
|
|
createMonthlyStatsChart() {
|
|
const canvas = document.getElementById('line-chart');
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Enhanced data for monthly stats
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
const salesData = [120, 135, 145, 165, 180, 195, 210, 225, 240, 220, 200, 185];
|
|
const profitData = [45, 52, 58, 62, 68, 75, 82, 88, 92, 85, 78, 72];
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: months,
|
|
datasets: [{
|
|
label: 'Sales ($K)',
|
|
data: salesData,
|
|
borderColor: '#4caf50',
|
|
backgroundColor: 'rgba(76, 175, 80, 0.1)',
|
|
borderWidth: 3,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7,
|
|
pointBackgroundColor: '#4caf50',
|
|
pointBorderColor: '#ffffff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
fill: false
|
|
}, {
|
|
label: 'Profit ($K)',
|
|
data: profitData,
|
|
borderColor: '#2196f3',
|
|
backgroundColor: 'rgba(33, 150, 243, 0.1)',
|
|
borderWidth: 3,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7,
|
|
pointBackgroundColor: '#2196f3',
|
|
pointBorderColor: '#ffffff',
|
|
pointBorderWidth: 2,
|
|
tension: 0.4,
|
|
fill: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: '600'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
cornerRadius: 8,
|
|
displayColors: true,
|
|
intersect: false,
|
|
mode: 'index',
|
|
callbacks: {
|
|
label(context) {
|
|
return `${context.dataset.label}: $${context.parsed.y}K`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
},
|
|
callback(value) {
|
|
return `$${value}K`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index'
|
|
}
|
|
}
|
|
});
|
|
this.charts.set('line-chart', chart);
|
|
}
|
|
|
|
/**
|
|
* Create line chart (only if target exists)
|
|
*/
|
|
createLineChart(id, data) {
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if target element exists
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace element with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
borderColor: COLORS['blue-500'],
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
pointRadius: 0,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create composite chart (only if target exists)
|
|
*/
|
|
createCompositeChart(id, data) {
|
|
let canvas = document.getElementById(id);
|
|
|
|
// Only proceed if target element exists
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
// If element exists but isn't a canvas, replace it with canvas
|
|
if (canvas.tagName !== 'CANVAS') {
|
|
const parent = canvas.parentNode;
|
|
if (!parent) {
|
|
return;
|
|
}
|
|
|
|
// Create new canvas element
|
|
const newCanvas = document.createElement('canvas');
|
|
newCanvas.id = id;
|
|
newCanvas.width = 100;
|
|
newCanvas.height = 20;
|
|
newCanvas.style.width = '100px';
|
|
newCanvas.style.height = '20px';
|
|
|
|
// Replace element with canvas
|
|
parent.replaceChild(newCanvas, canvas);
|
|
canvas = newCanvas;
|
|
} else {
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
type: 'bar',
|
|
data,
|
|
backgroundColor: '#aaf',
|
|
borderColor: '#aaf',
|
|
borderWidth: 0
|
|
}, {
|
|
type: 'line',
|
|
data,
|
|
borderColor: 'red',
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 1,
|
|
pointRadius: 0,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
events: [],
|
|
onResize: null,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create custom sparklines for different elements (only if they exist)
|
|
*/
|
|
createCustomSparklines() {
|
|
const sparklineElements = document.querySelectorAll('.sparkline');
|
|
const sparkbarElements = document.querySelectorAll('.sparkbar');
|
|
const sparktriElements = document.querySelectorAll('.sparktri');
|
|
const sparkdiscElements = document.querySelectorAll('.sparkdisc');
|
|
const sparkbullElements = document.querySelectorAll('.sparkbull');
|
|
const sparkboxElements = document.querySelectorAll('.sparkbox');
|
|
|
|
// Only create if we have elements
|
|
if (sparklineElements.length === 0 && sparkbarElements.length === 0 && sparktriElements.length === 0 && sparkdiscElements.length === 0 && sparkbullElements.length === 0 && sparkboxElements.length === 0) {
|
|
return;
|
|
}
|
|
const values = [5, 4, 5, -2, 0, 3, -5, 6, 7, 9, 9, 5, -3, -2, 2, -4];
|
|
const valuesAlt = [1, 1, 0, 1, -1, -1, 1, -1, 0, 0, 1, 1];
|
|
sparklineElements.forEach((element, index) => {
|
|
this.createCustomLineChart(element, values, `sparkline-${index}`);
|
|
});
|
|
sparkbarElements.forEach((element, index) => {
|
|
this.createCustomBarChart(element, values, `sparkbar-${index}`);
|
|
});
|
|
sparktriElements.forEach((element, index) => {
|
|
this.createTristateChart(element, valuesAlt, `sparktri-${index}`);
|
|
});
|
|
sparkdiscElements.forEach((element, index) => {
|
|
this.createDiscreteChart(element, values, `sparkdisc-${index}`);
|
|
});
|
|
sparkbullElements.forEach((element, index) => {
|
|
this.createBulletChart(element, values, `sparkbull-${index}`);
|
|
});
|
|
sparkboxElements.forEach((element, index) => {
|
|
this.createBoxChart(element, values, `sparkbox-${index}`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create custom line chart for sparkline elements
|
|
*/
|
|
createCustomLineChart(element, data, id) {
|
|
// Create canvas if it doesn't exist
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
borderColor: COLORS['red-500'],
|
|
backgroundColor: 'transparent',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointBackgroundColor: COLORS['red-500'],
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
// Disable animations to prevent resize triggers
|
|
events: [],
|
|
// Disable all events to prevent resize
|
|
onResize: null,
|
|
// Explicitly disable resize callback
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
} // Disable tooltip to prevent events
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create custom bar chart for sparkbar elements
|
|
*/
|
|
createCustomBarChart(element, data, id) {
|
|
// Create canvas if it doesn't exist
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: data.map(val => val < 0 ? COLORS['deep-purple-500'] : '#39f'),
|
|
borderColor: data.map(val => val < 0 ? COLORS['deep-purple-500'] : '#39f'),
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Setup resize handler for charts
|
|
*/
|
|
setupResizeHandler() {
|
|
// Setup responsive resize for large charts only
|
|
window.addEventListener('resize', () => {
|
|
this.debounceResize();
|
|
});
|
|
|
|
// Listen for sidebar toggle events
|
|
window.addEventListener('sidebar:toggle', () => {
|
|
this.debounceResize();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Debounced resize handler
|
|
*/
|
|
debounceResize() {
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
}
|
|
this.debounceTimer = setTimeout(() => {
|
|
this.redrawLargeChartsOnly();
|
|
}, 150);
|
|
}
|
|
|
|
/**
|
|
* Redraw only large charts, not sparklines
|
|
*/
|
|
redrawLargeChartsOnly() {
|
|
const largeChartIds = ['line-chart', 'area-chart', 'scatter-chart', 'bar-chart', 'doughnut-chart', 'polar-chart', 'radar-chart', 'mixed-chart', 'bubble-chart'];
|
|
largeChartIds.forEach(id => {
|
|
const chart = this.charts.get(id);
|
|
if (chart && chart.options.responsive) {
|
|
chart.resize();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Redraw all charts (used sparingly)
|
|
*/
|
|
redrawCharts() {
|
|
this.charts.forEach(chart => {
|
|
if (chart.options.responsive) {
|
|
chart.resize();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update chart data
|
|
*/
|
|
updateChart(id, newData) {
|
|
const chart = this.charts.get(id);
|
|
if (chart) {
|
|
chart.data.datasets[0].data = newData;
|
|
chart.update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create charts for the charts.html page
|
|
*/
|
|
createChartsPageCharts() {
|
|
// Line Chart
|
|
this.createLargeChart('line-chart', 'line', {
|
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [65, 59, 80, 81, 56, 55, 40],
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
tension: 0.4
|
|
}]
|
|
});
|
|
|
|
// Area Chart
|
|
this.createLargeChart('area-chart', 'line', {
|
|
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [65, 59, 80, 81, 56, 55, 40],
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
backgroundColor: 'rgba(54, 162, 235, 0.4)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
});
|
|
|
|
// Scatter Chart with more data points
|
|
this.createLargeChart('scatter-chart', 'scatter', {
|
|
datasets: [{
|
|
label: 'Dataset 1',
|
|
data: [{
|
|
x: -15,
|
|
y: 8
|
|
}, {
|
|
x: -12,
|
|
y: 12
|
|
}, {
|
|
x: -8,
|
|
y: 3
|
|
}, {
|
|
x: -5,
|
|
y: 15
|
|
}, {
|
|
x: -2,
|
|
y: 7
|
|
}, {
|
|
x: 0,
|
|
y: 10
|
|
}, {
|
|
x: 3,
|
|
y: 18
|
|
}, {
|
|
x: 6,
|
|
y: 5
|
|
}, {
|
|
x: 9,
|
|
y: 22
|
|
}, {
|
|
x: 12,
|
|
y: 8
|
|
}, {
|
|
x: 15,
|
|
y: 14
|
|
}, {
|
|
x: 18,
|
|
y: 19
|
|
}, {
|
|
x: -10,
|
|
y: 0
|
|
}, {
|
|
x: 10,
|
|
y: 5
|
|
}, {
|
|
x: 0.5,
|
|
y: 5.5
|
|
}, {
|
|
x: 7,
|
|
y: 12
|
|
}, {
|
|
x: -7,
|
|
y: 17
|
|
}, {
|
|
x: 4,
|
|
y: 9
|
|
}, {
|
|
x: 11,
|
|
y: 16
|
|
}, {
|
|
x: -3,
|
|
y: 11
|
|
}],
|
|
backgroundColor: 'rgba(255, 99, 132, 0.7)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 1
|
|
}, {
|
|
label: 'Dataset 2',
|
|
data: [{
|
|
x: -13,
|
|
y: 4
|
|
}, {
|
|
x: -9,
|
|
y: 8
|
|
}, {
|
|
x: -6,
|
|
y: 13
|
|
}, {
|
|
x: -1,
|
|
y: 6
|
|
}, {
|
|
x: 2,
|
|
y: 11
|
|
}, {
|
|
x: 5,
|
|
y: 15
|
|
}, {
|
|
x: 8,
|
|
y: 2
|
|
}, {
|
|
x: 13,
|
|
y: 17
|
|
}, {
|
|
x: 16,
|
|
y: 9
|
|
}, {
|
|
x: -4,
|
|
y: 14
|
|
}, {
|
|
x: 1,
|
|
y: 20
|
|
}, {
|
|
x: 14,
|
|
y: 4
|
|
}],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 1
|
|
}]
|
|
});
|
|
|
|
// Bar Chart
|
|
this.createLargeChart('bar-chart', 'bar', {
|
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
|
datasets: [{
|
|
label: '# of Votes',
|
|
data: [12, 19, 3, 5, 2, 3],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 205, 86, 0.6)', 'rgba(75, 192, 192, 0.6)', 'rgba(153, 102, 255, 0.6)', 'rgba(255, 159, 64, 0.6)'],
|
|
borderColor: ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 205, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'],
|
|
borderWidth: 1
|
|
}]
|
|
});
|
|
|
|
// Doughnut Chart
|
|
this.createLargeChart('doughnut-chart', 'doughnut', {
|
|
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
|
|
datasets: [{
|
|
label: 'My First Dataset',
|
|
data: [300, 50, 100, 75, 120, 60],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 205, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)', 'rgba(255, 159, 64, 0.8)'],
|
|
borderColor: ['rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 205, 86, 1)', 'rgba(75, 192, 192, 1)', 'rgba(153, 102, 255, 1)', 'rgba(255, 159, 64, 1)'],
|
|
borderWidth: 2,
|
|
hoverOffset: 10
|
|
}]
|
|
});
|
|
|
|
// Polar Area Chart
|
|
this.createLargeChart('polar-chart', 'polarArea', {
|
|
labels: ['Red', 'Green', 'Yellow', 'Grey', 'Blue'],
|
|
datasets: [{
|
|
label: 'My First Dataset',
|
|
data: [11, 16, 7, 3, 14],
|
|
backgroundColor: ['rgba(255, 99, 132, 0.7)', 'rgba(75, 192, 192, 0.7)', 'rgba(255, 205, 86, 0.7)', 'rgba(201, 203, 207, 0.7)', 'rgba(54, 162, 235, 0.7)'],
|
|
borderColor: ['rgb(255, 99, 132)', 'rgb(75, 192, 192)', 'rgb(255, 205, 86)', 'rgb(201, 203, 207)', 'rgb(54, 162, 235)'],
|
|
borderWidth: 2
|
|
}]
|
|
});
|
|
|
|
// Radar Chart
|
|
this.createLargeChart('radar-chart', 'radar', {
|
|
labels: ['Speed', 'Reliability', 'Comfort', 'Safety', 'Efficiency', 'Innovation'],
|
|
datasets: [{
|
|
label: 'Product A',
|
|
data: [65, 59, 90, 81, 56, 55],
|
|
fill: true,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 2,
|
|
pointBackgroundColor: 'rgb(54, 162, 235)',
|
|
pointBorderColor: '#fff',
|
|
pointHoverBackgroundColor: '#fff',
|
|
pointHoverBorderColor: 'rgb(54, 162, 235)'
|
|
}, {
|
|
label: 'Product B',
|
|
data: [28, 48, 40, 95, 86, 27],
|
|
fill: true,
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 2,
|
|
pointBackgroundColor: 'rgb(255, 99, 132)',
|
|
pointBorderColor: '#fff',
|
|
pointHoverBackgroundColor: '#fff',
|
|
pointHoverBorderColor: 'rgb(255, 99, 132)'
|
|
}]
|
|
});
|
|
|
|
// Mixed Chart (Bar + Line)
|
|
this.createLargeChart('mixed-chart', 'bar', {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
datasets: [{
|
|
type: 'bar',
|
|
label: 'Sales',
|
|
data: [12, 19, 3, 5, 2, 3],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 1
|
|
}, {
|
|
type: 'line',
|
|
label: 'Revenue',
|
|
data: [18, 25, 8, 15, 12, 18],
|
|
fill: false,
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
|
borderWidth: 3,
|
|
tension: 0.4,
|
|
pointRadius: 5,
|
|
pointHoverRadius: 7
|
|
}]
|
|
});
|
|
|
|
// Bubble Chart
|
|
this.createLargeChart('bubble-chart', 'bubble', {
|
|
datasets: [{
|
|
label: 'First Dataset',
|
|
data: [{
|
|
x: 20,
|
|
y: 30,
|
|
r: 15
|
|
}, {
|
|
x: 40,
|
|
y: 10,
|
|
r: 10
|
|
}, {
|
|
x: 30,
|
|
y: 40,
|
|
r: 20
|
|
}, {
|
|
x: 50,
|
|
y: 35,
|
|
r: 12
|
|
}, {
|
|
x: 10,
|
|
y: 50,
|
|
r: 8
|
|
}, {
|
|
x: 60,
|
|
y: 20,
|
|
r: 18
|
|
}, {
|
|
x: 25,
|
|
y: 25,
|
|
r: 14
|
|
}],
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
borderColor: 'rgb(54, 162, 235)',
|
|
borderWidth: 2
|
|
}, {
|
|
label: 'Second Dataset',
|
|
data: [{
|
|
x: 15,
|
|
y: 45,
|
|
r: 12
|
|
}, {
|
|
x: 35,
|
|
y: 15,
|
|
r: 16
|
|
}, {
|
|
x: 45,
|
|
y: 25,
|
|
r: 9
|
|
}, {
|
|
x: 55,
|
|
y: 45,
|
|
r: 14
|
|
}, {
|
|
x: 25,
|
|
y: 35,
|
|
r: 11
|
|
}],
|
|
backgroundColor: 'rgba(255, 99, 132, 0.6)',
|
|
borderColor: 'rgb(255, 99, 132)',
|
|
borderWidth: 2
|
|
}]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create large chart for charts page
|
|
*/
|
|
createLargeChart(id, type, data) {
|
|
const canvas = document.getElementById(id);
|
|
if (!canvas) return;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Define chart-specific options
|
|
const chartOptions = this.getChartOptions(type);
|
|
const chart = new chart_Chart(ctx, {
|
|
type,
|
|
data,
|
|
options: chartOptions
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Get chart-specific options based on chart type
|
|
*/
|
|
getChartOptions(type) {
|
|
const baseOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'top',
|
|
labels: {
|
|
padding: 20,
|
|
font: {
|
|
size: 12,
|
|
weight: '600'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
cornerRadius: 8,
|
|
displayColors: true
|
|
}
|
|
}
|
|
};
|
|
|
|
// Chart type specific configurations
|
|
switch (type) {
|
|
case 'doughnut':
|
|
case 'pie':
|
|
return {
|
|
...baseOptions,
|
|
plugins: {
|
|
...baseOptions.plugins,
|
|
legend: {
|
|
...baseOptions.plugins.legend,
|
|
position: 'right'
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false
|
|
}
|
|
};
|
|
case 'polarArea':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
r: {
|
|
pointLabels: {
|
|
display: true,
|
|
centerPointLabels: true,
|
|
font: {
|
|
size: 10
|
|
}
|
|
},
|
|
grid: {}
|
|
}
|
|
}
|
|
};
|
|
case 'radar':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
r: {
|
|
angleLines: {
|
|
display: true
|
|
},
|
|
grid: {},
|
|
pointLabels: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
},
|
|
ticks: {
|
|
display: true,
|
|
font: {
|
|
size: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
case 'bubble':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
type: 'linear',
|
|
position: 'bottom',
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
...baseOptions.plugins,
|
|
tooltip: {
|
|
...baseOptions.plugins.tooltip,
|
|
callbacks: {
|
|
label(context) {
|
|
return `${context.dataset.label}: (${context.parsed.x}, ${context.parsed.y}), Size: ${context.parsed._custom}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
case 'scatter':
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
type: 'linear',
|
|
position: 'bottom',
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
default:
|
|
// For line, bar, area, mixed charts
|
|
return {
|
|
...baseOptions,
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
borderDash: [5, 5]
|
|
},
|
|
ticks: {
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create tristate chart (for .sparktri elements)
|
|
*/
|
|
createTristateChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.map((_, i) => i),
|
|
datasets: [{
|
|
data: data.map(val => Math.abs(val)),
|
|
backgroundColor: data.map(val => {
|
|
if (val > 0) return COLORS['light-blue-500'];
|
|
if (val < 0) return '#f90';
|
|
return '#000';
|
|
}),
|
|
borderColor: data.map(val => {
|
|
if (val > 0) return COLORS['light-blue-500'];
|
|
if (val < 0) return '#f90';
|
|
return '#000';
|
|
}),
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create discrete chart (for .sparkdisc elements)
|
|
*/
|
|
createDiscreteChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'scatter',
|
|
data: {
|
|
datasets: [{
|
|
data: data.map((val, index) => ({
|
|
x: index,
|
|
y: val
|
|
})),
|
|
backgroundColor: '#9f0',
|
|
borderColor: '#9f0',
|
|
pointRadius: 2,
|
|
showLine: false
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create bullet chart (for .sparkbull elements)
|
|
*/
|
|
createBulletChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Simplified bullet chart as horizontal bar
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: [''],
|
|
datasets: [{
|
|
data: [Math.max(...data)],
|
|
backgroundColor: COLORS['amber-500'],
|
|
borderColor: COLORS['amber-500'],
|
|
borderWidth: 1,
|
|
barPercentage: 0.6
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
indexAxis: 'y',
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.x}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create box chart (for .sparkbox elements)
|
|
*/
|
|
createBoxChart(element, data, id) {
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = 100;
|
|
canvas.height = 20;
|
|
canvas.style.width = '100px';
|
|
canvas.style.height = '20px';
|
|
element.appendChild(canvas);
|
|
}
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Box plot simplified as bar chart showing quartiles
|
|
const sortedData = [...data].sort((a, b) => a - b);
|
|
const q1 = sortedData[Math.floor(sortedData.length * 0.25)];
|
|
const median = sortedData[Math.floor(sortedData.length * 0.5)];
|
|
const q3 = sortedData[Math.floor(sortedData.length * 0.75)];
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Q1', 'Med', 'Q3'],
|
|
datasets: [{
|
|
data: [q1, median, q3],
|
|
backgroundColor: '#9f0',
|
|
borderColor: '#9f0',
|
|
borderWidth: 1,
|
|
barPercentage: 0.8
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
display: false
|
|
},
|
|
y: {
|
|
display: false
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: true,
|
|
callbacks: {
|
|
label: context => `${context.parsed.y}°Celsius`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(id, chart);
|
|
}
|
|
|
|
/**
|
|
* Create Easy Pie Charts (replaces jQuery Easy Pie Chart)
|
|
*/
|
|
createEasyPieCharts() {
|
|
const easyPieElements = document.querySelectorAll('.easy-pie-chart');
|
|
easyPieElements.forEach((element, index) => {
|
|
const size = parseInt(element.dataset.size) || 80;
|
|
const percent = parseInt(element.dataset.percent) || 0;
|
|
const barColor = element.dataset.barColor || '#f44336';
|
|
|
|
// Create canvas for the pie chart
|
|
let canvas = element.querySelector('canvas');
|
|
if (!canvas) {
|
|
canvas = document.createElement('canvas');
|
|
canvas.width = size;
|
|
canvas.height = size;
|
|
canvas.style.width = `${size}px`;
|
|
canvas.style.height = `${size}px`;
|
|
element.appendChild(canvas);
|
|
}
|
|
|
|
// Create percentage display
|
|
const percentDisplay = element.querySelector('span');
|
|
if (percentDisplay) {
|
|
percentDisplay.textContent = `${percent}%`;
|
|
percentDisplay.style.position = 'absolute';
|
|
percentDisplay.style.top = '50%';
|
|
percentDisplay.style.left = '50%';
|
|
percentDisplay.style.transform = 'translate(-50%, -50%)';
|
|
percentDisplay.style.fontSize = '14px';
|
|
percentDisplay.style.fontWeight = 'bold';
|
|
}
|
|
|
|
// Set element position to relative for absolute positioning of text
|
|
element.style.position = 'relative';
|
|
element.style.display = 'inline-block';
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new chart_Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
datasets: [{
|
|
data: [percent, 100 - percent],
|
|
backgroundColor: [barColor, '#f0f0f0'],
|
|
borderWidth: 0,
|
|
cutout: '70%'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: false,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
enabled: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.charts.set(`easy-pie-${index}`, chart);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Destroy all charts
|
|
*/
|
|
destroy() {
|
|
this.charts.forEach(chart => {
|
|
chart.destroy();
|
|
});
|
|
this.charts.clear();
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
}
|
|
}
|
|
}
|
|
/* harmony default export */ const components_Chart = (ChartComponent);
|
|
;// ../../node_modules/preact/dist/preact.module.js
|
|
var n,l,u,i,t,r,o,f,e,c={},s=[],a=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function h(n,l){for(var u in l)n[u]=l[u];return n}function v(n){var l=n.parentNode;l&&l.removeChild(n)}function y(l,u,i){var t,r,o,f={};for(o in u)"key"==o?t=u[o]:"ref"==o?r=u[o]:f[o]=u[o];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):i),"function"==typeof l&&null!=l.defaultProps)for(o in l.defaultProps)void 0===f[o]&&(f[o]=l.defaultProps[o]);return p(l,f,t,r,null)}function p(n,i,t,r,o){var f={type:n,props:i,key:t,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++u:o};return null==o&&null!=l.vnode&&l.vnode(f),f}function d(){return{current:null}}function _(n){return n.children}function k(n,l,u,i,t){var r;for(r in u)"children"===r||"key"===r||r in l||g(n,r,null,u[r],i);for(r in l)t&&"function"!=typeof l[r]||"children"===r||"key"===r||"value"===r||"checked"===r||u[r]===l[r]||g(n,r,l[r],u[r],i)}function b(n,l,u){"-"===l[0]?n.setProperty(l,null==u?"":u):n[l]=null==u?"":"number"!=typeof u||a.test(l)?u:u+"px"}function g(n,l,u,i,t){var r;n:if("style"===l)if("string"==typeof u)n.style.cssText=u;else{if("string"==typeof i&&(n.style.cssText=i=""),i)for(l in i)u&&l in u||b(n.style,l,"");if(u)for(l in u)i&&u[l]===i[l]||b(n.style,l,u[l])}else if("o"===l[0]&&"n"===l[1])r=l!==(l=l.replace(/Capture$/,"")),l=l.toLowerCase()in n?l.toLowerCase().slice(2):l.slice(2),n.l||(n.l={}),n.l[l+r]=u,u?i||n.addEventListener(l,r?w:m,r):n.removeEventListener(l,r?w:m,r);else if("dangerouslySetInnerHTML"!==l){if(t)l=l.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==l&&"height"!==l&&"href"!==l&&"list"!==l&&"form"!==l&&"tabIndex"!==l&&"download"!==l&&l in n)try{n[l]=null==u?"":u;break n}catch(n){}"function"==typeof u||(null==u||!1===u&&-1==l.indexOf("-")?n.removeAttribute(l):n.setAttribute(l,u))}}function m(n){t=!0;try{return this.l[n.type+!1](l.event?l.event(n):n)}finally{t=!1}}function w(n){t=!0;try{return this.l[n.type+!0](l.event?l.event(n):n)}finally{t=!1}}function x(n,l){this.props=n,this.context=l}function A(n,l){if(null==l)return n.__?A(n.__,n.__.__k.indexOf(n)+1):null;for(var u;l<n.__k.length;l++)if(null!=(u=n.__k[l])&&null!=u.__e)return u.__e;return"function"==typeof n.type?A(n):null}function P(n){var l,u;if(null!=(n=n.__)&&null!=n.__c){for(n.__e=n.__c.base=null,l=0;l<n.__k.length;l++)if(null!=(u=n.__k[l])&&null!=u.__e){n.__e=n.__c.base=u.__e;break}return P(n)}}function C(n){t?setTimeout(n):f(n)}function T(n){(!n.__d&&(n.__d=!0)&&r.push(n)&&!$.__r++||o!==l.debounceRendering)&&((o=l.debounceRendering)||C)($)}function $(){var n,l,u,i,t,o,f,e;for(r.sort(function(n,l){return n.__v.__b-l.__v.__b});n=r.shift();)n.__d&&(l=r.length,i=void 0,t=void 0,f=(o=(u=n).__v).__e,(e=u.__P)&&(i=[],(t=h({},o)).__v=o.__v+1,M(e,o,t,u.__n,void 0!==e.ownerSVGElement,null!=o.__h?[f]:null,i,null==f?A(o):f,o.__h),N(i,o),o.__e!=f&&P(o)),r.length>l&&r.sort(function(n,l){return n.__v.__b-l.__v.__b}));$.__r=0}function H(n,l,u,i,t,r,o,f,e,a){var h,v,y,d,k,b,g,m=i&&i.__k||s,w=m.length;for(u.__k=[],h=0;h<l.length;h++)if(null!=(d=u.__k[h]=null==(d=l[h])||"boolean"==typeof d?null:"string"==typeof d||"number"==typeof d||"bigint"==typeof d?p(null,d,null,null,d):Array.isArray(d)?p(_,{children:d},null,null,null):d.__b>0?p(d.type,d.props,d.key,d.ref?d.ref:null,d.__v):d)){if(d.__=u,d.__b=u.__b+1,null===(y=m[h])||y&&d.key==y.key&&d.type===y.type)m[h]=void 0;else for(v=0;v<w;v++){if((y=m[v])&&d.key==y.key&&d.type===y.type){m[v]=void 0;break}y=null}M(n,d,y=y||c,t,r,o,f,e,a),k=d.__e,(v=d.ref)&&y.ref!=v&&(g||(g=[]),y.ref&&g.push(y.ref,null,d),g.push(v,d.__c||k,d)),null!=k?(null==b&&(b=k),"function"==typeof d.type&&d.__k===y.__k?d.__d=e=I(d,e,n):e=z(n,d,y,m,k,e),"function"==typeof u.type&&(u.__d=e)):e&&y.__e==e&&e.parentNode!=n&&(e=A(y))}for(u.__e=b,h=w;h--;)null!=m[h]&&("function"==typeof u.type&&null!=m[h].__e&&m[h].__e==u.__d&&(u.__d=L(i).nextSibling),q(m[h],m[h]));if(g)for(h=0;h<g.length;h++)S(g[h],g[++h],g[++h])}function I(n,l,u){for(var i,t=n.__k,r=0;t&&r<t.length;r++)(i=t[r])&&(i.__=n,l="function"==typeof i.type?I(i,l,u):z(u,i,i,t,i.__e,l));return l}function j(n,l){return l=l||[],null==n||"boolean"==typeof n||(Array.isArray(n)?n.some(function(n){j(n,l)}):l.push(n)),l}function z(n,l,u,i,t,r){var o,f,e;if(void 0!==l.__d)o=l.__d,l.__d=void 0;else if(null==u||t!=r||null==t.parentNode)n:if(null==r||r.parentNode!==n)n.appendChild(t),o=null;else{for(f=r,e=0;(f=f.nextSibling)&&e<i.length;e+=1)if(f==t)break n;n.insertBefore(t,r),o=r}return void 0!==o?o:t.nextSibling}function L(n){var l,u,i;if(null==n.type||"string"==typeof n.type)return n.__e;if(n.__k)for(l=n.__k.length-1;l>=0;l--)if((u=n.__k[l])&&(i=L(u)))return i;return null}function M(n,u,i,t,r,o,f,e,c){var s,a,v,y,p,d,k,b,g,m,w,A,P,C,T,$=u.type;if(void 0!==u.constructor)return null;null!=i.__h&&(c=i.__h,e=u.__e=i.__e,u.__h=null,o=[e]),(s=l.__b)&&s(u);try{n:if("function"==typeof $){if(b=u.props,g=(s=$.contextType)&&t[s.__c],m=s?g?g.props.value:s.__:t,i.__c?k=(a=u.__c=i.__c).__=a.__E:("prototype"in $&&$.prototype.render?u.__c=a=new $(b,m):(u.__c=a=new x(b,m),a.constructor=$,a.render=B),g&&g.sub(a),a.props=b,a.state||(a.state={}),a.context=m,a.__n=t,v=a.__d=!0,a.__h=[],a._sb=[]),null==a.__s&&(a.__s=a.state),null!=$.getDerivedStateFromProps&&(a.__s==a.state&&(a.__s=h({},a.__s)),h(a.__s,$.getDerivedStateFromProps(b,a.__s))),y=a.props,p=a.state,a.__v=u,v)null==$.getDerivedStateFromProps&&null!=a.componentWillMount&&a.componentWillMount(),null!=a.componentDidMount&&a.__h.push(a.componentDidMount);else{if(null==$.getDerivedStateFromProps&&b!==y&&null!=a.componentWillReceiveProps&&a.componentWillReceiveProps(b,m),!a.__e&&null!=a.shouldComponentUpdate&&!1===a.shouldComponentUpdate(b,a.__s,m)||u.__v===i.__v){for(u.__v!==i.__v&&(a.props=b,a.state=a.__s,a.__d=!1),u.__e=i.__e,u.__k=i.__k,u.__k.forEach(function(n){n&&(n.__=u)}),w=0;w<a._sb.length;w++)a.__h.push(a._sb[w]);a._sb=[],a.__h.length&&f.push(a);break n}null!=a.componentWillUpdate&&a.componentWillUpdate(b,a.__s,m),null!=a.componentDidUpdate&&a.__h.push(function(){a.componentDidUpdate(y,p,d)})}if(a.context=m,a.props=b,a.__P=n,A=l.__r,P=0,"prototype"in $&&$.prototype.render){for(a.state=a.__s,a.__d=!1,A&&A(u),s=a.render(a.props,a.state,a.context),C=0;C<a._sb.length;C++)a.__h.push(a._sb[C]);a._sb=[]}else do{a.__d=!1,A&&A(u),s=a.render(a.props,a.state,a.context),a.state=a.__s}while(a.__d&&++P<25);a.state=a.__s,null!=a.getChildContext&&(t=h(h({},t),a.getChildContext())),v||null==a.getSnapshotBeforeUpdate||(d=a.getSnapshotBeforeUpdate(y,p)),T=null!=s&&s.type===_&&null==s.key?s.props.children:s,H(n,Array.isArray(T)?T:[T],u,i,t,r,o,f,e,c),a.base=u.__e,u.__h=null,a.__h.length&&f.push(a),k&&(a.__E=a.__=null),a.__e=!1}else null==o&&u.__v===i.__v?(u.__k=i.__k,u.__e=i.__e):u.__e=O(i.__e,u,i,t,r,o,f,c);(s=l.diffed)&&s(u)}catch(n){u.__v=null,(c||null!=o)&&(u.__e=e,u.__h=!!c,o[o.indexOf(e)]=null),l.__e(n,u,i)}}function N(n,u){l.__c&&l.__c(u,n),n.some(function(u){try{n=u.__h,u.__h=[],n.some(function(n){n.call(u)})}catch(n){l.__e(n,u.__v)}})}function O(l,u,i,t,r,o,f,e){var s,a,h,y=i.props,p=u.props,d=u.type,_=0;if("svg"===d&&(r=!0),null!=o)for(;_<o.length;_++)if((s=o[_])&&"setAttribute"in s==!!d&&(d?s.localName===d:3===s.nodeType)){l=s,o[_]=null;break}if(null==l){if(null===d)return document.createTextNode(p);l=r?document.createElementNS("http://www.w3.org/2000/svg",d):document.createElement(d,p.is&&p),o=null,e=!1}if(null===d)y===p||e&&l.data===p||(l.data=p);else{if(o=o&&n.call(l.childNodes),a=(y=i.props||c).dangerouslySetInnerHTML,h=p.dangerouslySetInnerHTML,!e){if(null!=o)for(y={},_=0;_<l.attributes.length;_++)y[l.attributes[_].name]=l.attributes[_].value;(h||a)&&(h&&(a&&h.__html==a.__html||h.__html===l.innerHTML)||(l.innerHTML=h&&h.__html||""))}if(k(l,p,y,r,e),h)u.__k=[];else if(_=u.props.children,H(l,Array.isArray(_)?_:[_],u,i,t,r&&"foreignObject"!==d,o,f,o?o[0]:i.__k&&A(i,0),e),null!=o)for(_=o.length;_--;)null!=o[_]&&v(o[_]);e||("value"in p&&void 0!==(_=p.value)&&(_!==l.value||"progress"===d&&!_||"option"===d&&_!==y.value)&&g(l,"value",_,y.value,!1),"checked"in p&&void 0!==(_=p.checked)&&_!==l.checked&&g(l,"checked",_,y.checked,!1))}return l}function S(n,u,i){try{"function"==typeof n?n(u):n.current=u}catch(n){l.__e(n,i)}}function q(n,u,i){var t,r;if(l.unmount&&l.unmount(n),(t=n.ref)&&(t.current&&t.current!==n.__e||S(t,null,u)),null!=(t=n.__c)){if(t.componentWillUnmount)try{t.componentWillUnmount()}catch(n){l.__e(n,u)}t.base=t.__P=null,n.__c=void 0}if(t=n.__k)for(r=0;r<t.length;r++)t[r]&&q(t[r],u,i||"function"!=typeof n.type);i||null==n.__e||v(n.__e),n.__=n.__e=n.__d=void 0}function B(n,l,u){return this.constructor(n,u)}function D(u,i,t){var r,o,f;l.__&&l.__(u,i),o=(r="function"==typeof t)?null:t&&t.__k||i.__k,f=[],M(i,u=(!r&&t||i).__k=y(_,null,[u]),o||c,c,void 0!==i.ownerSVGElement,!r&&t?[t]:o?null:i.firstChild?n.call(i.childNodes):null,f,!r&&t?t:o?o.__e:i.firstChild,r),N(f,u)}function E(n,l){D(n,l,E)}function F(l,u,i){var t,r,o,f=h({},l.props);for(o in u)"key"==o?t=u[o]:"ref"==o?r=u[o]:f[o]=u[o];return arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):i),p(l.type,f,t||l.key,r||l.ref,null)}function G(n,l){var u={__c:l="__cC"+e++,__:n,Consumer:function(n,l){return n.children(l)},Provider:function(n){var u,i;return this.getChildContext||(u=[],(i={})[l]=this,this.getChildContext=function(){return i},this.shouldComponentUpdate=function(n){this.props.value!==n.value&&u.some(function(n){n.__e=!0,T(n)})},this.sub=function(n){u.push(n);var l=n.componentWillUnmount;n.componentWillUnmount=function(){u.splice(u.indexOf(n),1),l&&l.call(n)}}),n.children}};return u.Provider.__=u.Consumer.contextType=u}n=s.slice,l={__e:function(n,l,u,i){for(var t,r,o;l=l.__;)if((t=l.__c)&&!t.__)try{if((r=t.constructor)&&null!=r.getDerivedStateFromError&&(t.setState(r.getDerivedStateFromError(n)),o=t.__d),null!=t.componentDidCatch&&(t.componentDidCatch(n,i||{}),o=t.__d),o)return t.__E=t}catch(l){n=l}throw n}},u=0,i=function(n){return null!=n&&void 0===n.constructor},t=!1,x.prototype.setState=function(n,l){var u;u=null!=this.__s&&this.__s!==this.state?this.__s:this.__s=h({},this.state),"function"==typeof n&&(n=n(h({},u),this.props)),n&&h(u,n),null!=n&&this.__v&&(l&&this._sb.push(l),T(this))},x.prototype.forceUpdate=function(n){this.__v&&(this.__e=!0,n&&this.__h.push(n),T(this))},x.prototype.render=_,r=[],f="function"==typeof Promise?Promise.prototype.then.bind(Promise.resolve()):setTimeout,$.__r=0,e=0;
|
|
//# sourceMappingURL=preact.module.js.map
|
|
|
|
;// ../../node_modules/preact/hooks/dist/hooks.module.js
|
|
var hooks_module_t,hooks_module_r,hooks_module_u,hooks_module_i,hooks_module_o=0,hooks_module_f=[],hooks_module_c=[],hooks_module_e=l.__b,hooks_module_a=l.__r,hooks_module_v=l.diffed,hooks_module_l=l.__c,hooks_module_m=l.unmount;function hooks_module_d(t,u){l.__h&&l.__h(hooks_module_r,t,hooks_module_o||u),hooks_module_o=0;var i=hooks_module_r.__H||(hooks_module_r.__H={__:[],__h:[]});return t>=i.__.length&&i.__.push({__V:hooks_module_c}),i.__[t]}function hooks_module_p(n){return hooks_module_o=1,hooks_module_y(hooks_module_B,n)}function hooks_module_y(n,u,i){var o=hooks_module_d(hooks_module_t++,2);if(o.t=n,!o.__c&&(o.__=[i?i(u):hooks_module_B(void 0,u),function(n){var t=o.__N?o.__N[0]:o.__[0],r=o.t(t,n);t!==r&&(o.__N=[r,o.__[1]],o.__c.setState({}))}],o.__c=hooks_module_r,!hooks_module_r.u)){hooks_module_r.u=!0;var f=hooks_module_r.shouldComponentUpdate;hooks_module_r.shouldComponentUpdate=function(n,t,r){if(!o.__c.__H)return!0;var u=o.__c.__H.__.filter(function(n){return n.__c});if(u.every(function(n){return!n.__N}))return!f||f.call(this,n,t,r);var i=!1;return u.forEach(function(n){if(n.__N){var t=n.__[0];n.__=n.__N,n.__N=void 0,t!==n.__[0]&&(i=!0)}}),!(!i&&o.__c.props===n)&&(!f||f.call(this,n,t,r))}}return o.__N||o.__}function hooks_module_h(u,i){var o=hooks_module_d(hooks_module_t++,3);!l.__s&&hooks_module_z(o.__H,i)&&(o.__=u,o.i=i,hooks_module_r.__H.__h.push(o))}function hooks_module_s(u,i){var o=hooks_module_d(hooks_module_t++,4);!l.__s&&hooks_module_z(o.__H,i)&&(o.__=u,o.i=i,hooks_module_r.__h.push(o))}function hooks_module_(n){return hooks_module_o=5,hooks_module_F(function(){return{current:n}},[])}function hooks_module_A(n,t,r){hooks_module_o=6,hooks_module_s(function(){return"function"==typeof n?(n(t()),function(){return n(null)}):n?(n.current=t(),function(){return n.current=null}):void 0},null==r?r:r.concat(n))}function hooks_module_F(n,r){var u=hooks_module_d(hooks_module_t++,7);return hooks_module_z(u.__H,r)?(u.__V=n(),u.i=r,u.__h=n,u.__V):u.__}function hooks_module_T(n,t){return hooks_module_o=8,hooks_module_F(function(){return n},t)}function hooks_module_q(n){var u=hooks_module_r.context[n.__c],i=hooks_module_d(hooks_module_t++,9);return i.c=n,u?(null==i.__&&(i.__=!0,u.sub(hooks_module_r)),u.props.value):n.__}function hooks_module_x(t,r){l.useDebugValue&&l.useDebugValue(r?r(t):t)}function hooks_module_P(n){var u=hooks_module_d(hooks_module_t++,10),i=hooks_module_p();return u.__=n,hooks_module_r.componentDidCatch||(hooks_module_r.componentDidCatch=function(n,t){u.__&&u.__(n,t),i[1](n)}),[i[0],function(){i[1](void 0)}]}function V(){var n=hooks_module_d(hooks_module_t++,11);if(!n.__){for(var u=hooks_module_r.__v;null!==u&&!u.__m&&null!==u.__;)u=u.__;var i=u.__m||(u.__m=[0,0]);n.__="P"+i[0]+"-"+i[1]++}return n.__}function hooks_module_b(){for(var t;t=hooks_module_f.shift();)if(t.__P&&t.__H)try{t.__H.__h.forEach(hooks_module_k),t.__H.__h.forEach(hooks_module_w),t.__H.__h=[]}catch(r){t.__H.__h=[],l.__e(r,t.__v)}}l.__b=function(n){hooks_module_r=null,hooks_module_e&&hooks_module_e(n)},l.__r=function(n){hooks_module_a&&hooks_module_a(n),hooks_module_t=0;var i=(hooks_module_r=n.__c).__H;i&&(hooks_module_u===hooks_module_r?(i.__h=[],hooks_module_r.__h=[],i.__.forEach(function(n){n.__N&&(n.__=n.__N),n.__V=hooks_module_c,n.__N=n.i=void 0})):(i.__h.forEach(hooks_module_k),i.__h.forEach(hooks_module_w),i.__h=[])),hooks_module_u=hooks_module_r},l.diffed=function(t){hooks_module_v&&hooks_module_v(t);var o=t.__c;o&&o.__H&&(o.__H.__h.length&&(1!==hooks_module_f.push(o)&&hooks_module_i===l.requestAnimationFrame||((hooks_module_i=l.requestAnimationFrame)||hooks_module_j)(hooks_module_b)),o.__H.__.forEach(function(n){n.i&&(n.__H=n.i),n.__V!==hooks_module_c&&(n.__=n.__V),n.i=void 0,n.__V=hooks_module_c})),hooks_module_u=hooks_module_r=null},l.__c=function(t,r){r.some(function(t){try{t.__h.forEach(hooks_module_k),t.__h=t.__h.filter(function(n){return!n.__||hooks_module_w(n)})}catch(u){r.some(function(n){n.__h&&(n.__h=[])}),r=[],l.__e(u,t.__v)}}),hooks_module_l&&hooks_module_l(t,r)},l.unmount=function(t){hooks_module_m&&hooks_module_m(t);var r,u=t.__c;u&&u.__H&&(u.__H.__.forEach(function(n){try{hooks_module_k(n)}catch(n){r=n}}),u.__H=void 0,r&&l.__e(r,u.__v))};var hooks_module_g="function"==typeof requestAnimationFrame;function hooks_module_j(n){var t,r=function(){clearTimeout(u),hooks_module_g&&cancelAnimationFrame(t),setTimeout(n)},u=setTimeout(r,100);hooks_module_g&&(t=requestAnimationFrame(r))}function hooks_module_k(n){var t=hooks_module_r,u=n.__c;"function"==typeof u&&(n.__c=void 0,u()),hooks_module_r=t}function hooks_module_w(n){var t=hooks_module_r;n.__c=n.__(),hooks_module_r=t}function hooks_module_z(n,t){return!n||n.length!==t.length||t.some(function(t,r){return t!==n[r]})}function hooks_module_B(n,t){return"function"==typeof t?t(n):t}
|
|
//# sourceMappingURL=hooks.module.js.map
|
|
|
|
;// ../../node_modules/preact/compat/dist/compat.module.js
|
|
function compat_module_g(n,t){for(var e in t)n[e]=t[e];return n}function compat_module_C(n,t){for(var e in n)if("__source"!==e&&!(e in t))return!0;for(var r in t)if("__source"!==r&&n[r]!==t[r])return!0;return!1}function compat_module_E(n,t){return n===t&&(0!==n||1/n==1/t)||n!=n&&t!=t}function compat_module_w(n){this.props=n}function R(n,e){function r(n){var t=this.props.ref,r=t==n.ref;return!r&&t&&(t.call?t(null):t.current=null),e?!e(this.props,n)||!r:compat_module_C(this.props,n)}function u(e){return this.shouldComponentUpdate=r,y(n,e)}return u.displayName="Memo("+(n.displayName||n.name)+")",u.prototype.isReactComponent=!0,u.__f=!0,u}(compat_module_w.prototype=new x).isPureReactComponent=!0,compat_module_w.prototype.shouldComponentUpdate=function(n,t){return compat_module_C(this.props,n)||compat_module_C(this.state,t)};var compat_module_x=l.__b;l.__b=function(n){n.type&&n.type.__f&&n.ref&&(n.props.ref=n.ref,n.ref=null),compat_module_x&&compat_module_x(n)};var compat_module_N="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.forward_ref")||3911;function compat_module_k(n){function t(t){var e=compat_module_g({},t);return delete e.ref,n(e,t.ref||null)}return t.$$typeof=compat_module_N,t.render=t,t.prototype.isReactComponent=t.__f=!0,t.displayName="ForwardRef("+(n.displayName||n.name)+")",t}var compat_module_A=function(n,t){return null==n?null:j(j(n).map(t))},compat_module_O={map:compat_module_A,forEach:compat_module_A,count:function(n){return n?j(n).length:0},only:function(n){var t=j(n);if(1!==t.length)throw"Children.only";return t[0]},toArray:j},compat_module_T=l.__e;l.__e=function(n,t,e,r){if(n.then)for(var u,o=t;o=o.__;)if((u=o.__c)&&u.__c)return null==t.__e&&(t.__e=e.__e,t.__k=e.__k),u.__c(n,t);compat_module_T(n,t,e,r)};var compat_module_I=l.unmount;function compat_module_L(n,t,e){return n&&(n.__c&&n.__c.__H&&(n.__c.__H.__.forEach(function(n){"function"==typeof n.__c&&n.__c()}),n.__c.__H=null),null!=(n=compat_module_g({},n)).__c&&(n.__c.__P===e&&(n.__c.__P=t),n.__c=null),n.__k=n.__k&&n.__k.map(function(n){return compat_module_L(n,t,e)})),n}function U(n,t,e){return n&&(n.__v=null,n.__k=n.__k&&n.__k.map(function(n){return U(n,t,e)}),n.__c&&n.__c.__P===t&&(n.__e&&e.insertBefore(n.__e,n.__d),n.__c.__e=!0,n.__c.__P=e)),n}function compat_module_D(){this.__u=0,this.t=null,this.__b=null}function compat_module_F(n){var t=n.__.__c;return t&&t.__a&&t.__a(n)}function compat_module_M(n){var e,r,u;function o(o){if(e||(e=n()).then(function(n){r=n.default||n},function(n){u=n}),u)throw u;if(!r)throw e;return y(r,o)}return o.displayName="Lazy",o.__f=!0,o}function compat_module_V(){this.u=null,this.o=null}l.unmount=function(n){var t=n.__c;t&&t.__R&&t.__R(),t&&!0===n.__h&&(n.type=null),compat_module_I&&compat_module_I(n)},(compat_module_D.prototype=new x).__c=function(n,t){var e=t.__c,r=this;null==r.t&&(r.t=[]),r.t.push(e);var u=compat_module_F(r.__v),o=!1,i=function(){o||(o=!0,e.__R=null,u?u(l):l())};e.__R=i;var l=function(){if(!--r.__u){if(r.state.__a){var n=r.state.__a;r.__v.__k[0]=U(n,n.__c.__P,n.__c.__O)}var t;for(r.setState({__a:r.__b=null});t=r.t.pop();)t.forceUpdate()}},c=!0===t.__h;r.__u++||c||r.setState({__a:r.__b=r.__v.__k[0]}),n.then(i,i)},compat_module_D.prototype.componentWillUnmount=function(){this.t=[]},compat_module_D.prototype.render=function(n,e){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),o=this.__v.__k[0].__c;this.__v.__k[0]=compat_module_L(this.__b,r,o.__O=o.__P)}this.__b=null}var i=e.__a&&y(_,null,n.fallback);return i&&(i.__h=null),[y(_,null,e.__a?null:n.children),i]};var W=function(n,t,e){if(++e[1]===e[0]&&n.o.delete(t),n.props.revealOrder&&("t"!==n.props.revealOrder[0]||!n.o.size))for(e=n.u;e;){for(;e.length>3;)e.pop()();if(e[1]<e[0])break;n.u=e=e[2]}};function compat_module_P(n){return this.getChildContext=function(){return n.context},n.children}function compat_module_$(n){var e=this,r=n.i;e.componentWillUnmount=function(){D(null,e.l),e.l=null,e.i=null},e.i&&e.i!==r&&e.componentWillUnmount(),n.__v?(e.l||(e.i=r,e.l={nodeType:1,parentNode:r,childNodes:[],appendChild:function(n){this.childNodes.push(n),e.i.appendChild(n)},insertBefore:function(n,t){this.childNodes.push(n),e.i.appendChild(n)},removeChild:function(n){this.childNodes.splice(this.childNodes.indexOf(n)>>>1,1),e.i.removeChild(n)}}),D(y(compat_module_P,{context:e.context},n.__v),e.l)):e.l&&e.componentWillUnmount()}function compat_module_j(n,e){var r=y(compat_module_$,{__v:n,i:e});return r.containerInfo=e,r}(compat_module_V.prototype=new x).__a=function(n){var t=this,e=compat_module_F(t.__v),r=t.o.get(n);return r[0]++,function(u){var o=function(){t.props.revealOrder?(r.push(u),W(t,n,r)):u()};e?e(o):o()}},compat_module_V.prototype.render=function(n){this.u=null,this.o=new Map;var t=j(n.children);n.revealOrder&&"b"===n.revealOrder[0]&&t.reverse();for(var e=t.length;e--;)this.o.set(t[e],this.u=[1,0,this.u]);return n.children},compat_module_V.prototype.componentDidUpdate=compat_module_V.prototype.componentDidMount=function(){var n=this;this.o.forEach(function(t,e){W(n,e,t)})};var compat_module_z="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,compat_module_B=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,compat_module_H="undefined"!=typeof document,Z=function(n){return("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/i:/fil|che|ra/i).test(n)};function Y(n,t,e){return null==t.__k&&(t.textContent=""),D(n,t),"function"==typeof e&&e(),n?n.__c:null}function compat_module_q(n,t,e){return E(n,t),"function"==typeof e&&e(),n?n.__c:null}x.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(t){Object.defineProperty(x.prototype,t,{configurable:!0,get:function(){return this["UNSAFE_"+t]},set:function(n){Object.defineProperty(this,t,{configurable:!0,writable:!0,value:n})}})});var compat_module_G=l.event;function J(){}function K(){return this.cancelBubble}function Q(){return this.defaultPrevented}l.event=function(n){return compat_module_G&&(n=compat_module_G(n)),n.persist=J,n.isPropagationStopped=K,n.isDefaultPrevented=Q,n.nativeEvent=n};var X,nn={configurable:!0,get:function(){return this.class}},tn=l.vnode;l.vnode=function(n){var t=n.type,e=n.props,u=e;if("string"==typeof t){var o=-1===t.indexOf("-");for(var i in u={},e){var l=e[i];compat_module_H&&"children"===i&&"noscript"===t||"value"===i&&"defaultValue"in e&&null==l||("defaultValue"===i&&"value"in e&&null==e.value?i="value":"download"===i&&!0===l?l="":/ondoubleclick/i.test(i)?i="ondblclick":/^onchange(textarea|input)/i.test(i+t)&&!Z(e.type)?i="oninput":/^onfocus$/i.test(i)?i="onfocusin":/^onblur$/i.test(i)?i="onfocusout":/^on(Ani|Tra|Tou|BeforeInp|Compo)/.test(i)?i=i.toLowerCase():o&&compat_module_B.test(i)?i=i.replace(/[A-Z0-9]/g,"-$&").toLowerCase():null===l&&(l=void 0),/^oninput$/i.test(i)&&(i=i.toLowerCase(),u[i]&&(i="oninputCapture")),u[i]=l)}"select"==t&&u.multiple&&Array.isArray(u.value)&&(u.value=j(e.children).forEach(function(n){n.props.selected=-1!=u.value.indexOf(n.props.value)})),"select"==t&&null!=u.defaultValue&&(u.value=j(e.children).forEach(function(n){n.props.selected=u.multiple?-1!=u.defaultValue.indexOf(n.props.value):u.defaultValue==n.props.value})),n.props=u,e.class!=e.className&&(nn.enumerable="className"in e,null!=e.className&&(u.class=e.className),Object.defineProperty(u,"className",nn))}n.$$typeof=compat_module_z,tn&&tn(n)};var en=l.__r;l.__r=function(n){en&&en(n),X=n.__c};var rn={ReactCurrentDispatcher:{current:{readContext:function(n){return X.__n[n.__c].props.value}}}},un="17.0.2";function on(n){return y.bind(null,n)}function ln(n){return!!n&&n.$$typeof===compat_module_z}function cn(n){return ln(n)?F.apply(null,arguments):n}function fn(n){return!!n.__k&&(D(null,n),!0)}function an(n){return n&&(n.base||1===n.nodeType&&n)||null}var sn=function(n,t){return n(t)},hn=function(n,t){return n(t)},vn=_;function dn(n){n()}function pn(n){return n}function mn(){return[!1,dn]}var yn=hooks_module_s;function _n(n,t){var e=t(),r=hooks_module_p({h:{__:e,v:t}}),u=r[0].h,o=r[1];return hooks_module_s(function(){u.__=e,u.v=t,compat_module_E(u.__,t())||o({h:u})},[n,e,t]),hooks_module_h(function(){return compat_module_E(u.__,u.v())||o({h:u}),n(function(){compat_module_E(u.__,u.v())||o({h:u})})},[n]),e}var bn={useState:hooks_module_p,useId:V,useReducer:hooks_module_y,useEffect:hooks_module_h,useLayoutEffect:hooks_module_s,useInsertionEffect:yn,useTransition:mn,useDeferredValue:pn,useSyncExternalStore:_n,startTransition:dn,useRef:hooks_module_,useImperativeHandle:hooks_module_A,useMemo:hooks_module_F,useCallback:hooks_module_T,useContext:hooks_module_q,useDebugValue:hooks_module_x,version:"17.0.2",Children:compat_module_O,render:Y,hydrate:compat_module_q,unmountComponentAtNode:fn,createPortal:compat_module_j,createElement:y,createContext:G,createFactory:on,cloneElement:cn,createRef:d,Fragment:_,isValidElement:ln,findDOMNode:an,Component:x,PureComponent:compat_module_w,memo:R,forwardRef:compat_module_k,flushSync:hn,unstable_batchedUpdates:sn,StrictMode:vn,Suspense:compat_module_D,SuspenseList:compat_module_V,lazy:compat_module_M,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:rn};
|
|
//# sourceMappingURL=compat.module.js.map
|
|
|
|
;// ../../node_modules/@fullcalendar/core/internal-common.js
|
|
|
|
|
|
|
|
|
|
const styleTexts = [];
|
|
const styleEls = new Map();
|
|
function injectStyles(styleText) {
|
|
styleTexts.push(styleText);
|
|
styleEls.forEach((styleEl) => {
|
|
appendStylesTo(styleEl, styleText);
|
|
});
|
|
}
|
|
function ensureElHasStyles(el) {
|
|
if (el.isConnected && // sometimes true if SSR system simulates DOM
|
|
el.getRootNode // sometimes undefined if SSR system simulates DOM
|
|
) {
|
|
registerStylesRoot(el.getRootNode());
|
|
}
|
|
}
|
|
function registerStylesRoot(rootNode) {
|
|
let styleEl = styleEls.get(rootNode);
|
|
if (!styleEl || !styleEl.isConnected) {
|
|
styleEl = rootNode.querySelector('style[data-fullcalendar]');
|
|
if (!styleEl) {
|
|
styleEl = document.createElement('style');
|
|
styleEl.setAttribute('data-fullcalendar', '');
|
|
const nonce = getNonceValue();
|
|
if (nonce) {
|
|
styleEl.nonce = nonce;
|
|
}
|
|
const parentEl = rootNode === document ? document.head : rootNode;
|
|
const insertBefore = rootNode === document
|
|
? parentEl.querySelector('script,link[rel=stylesheet],link[as=style],style')
|
|
: parentEl.firstChild;
|
|
parentEl.insertBefore(styleEl, insertBefore);
|
|
}
|
|
styleEls.set(rootNode, styleEl);
|
|
hydrateStylesRoot(styleEl);
|
|
}
|
|
}
|
|
function hydrateStylesRoot(styleEl) {
|
|
for (const styleText of styleTexts) {
|
|
appendStylesTo(styleEl, styleText);
|
|
}
|
|
}
|
|
function appendStylesTo(styleEl, styleText) {
|
|
const { sheet } = styleEl;
|
|
const ruleCnt = sheet.cssRules.length;
|
|
styleText.split('}').forEach((styleStr, i) => {
|
|
styleStr = styleStr.trim();
|
|
if (styleStr) {
|
|
sheet.insertRule(styleStr + '}', ruleCnt + i);
|
|
}
|
|
});
|
|
}
|
|
// nonce
|
|
// -------------------------------------------------------------------------------------------------
|
|
let queriedNonceValue;
|
|
function getNonceValue() {
|
|
if (queriedNonceValue === undefined) {
|
|
queriedNonceValue = queryNonceValue();
|
|
}
|
|
return queriedNonceValue;
|
|
}
|
|
/*
|
|
TODO: discourage meta tag and instead put nonce attribute on placeholder <style> tag
|
|
*/
|
|
function queryNonceValue() {
|
|
const metaWithNonce = document.querySelector('meta[name="csp-nonce"]');
|
|
if (metaWithNonce && metaWithNonce.hasAttribute('content')) {
|
|
return metaWithNonce.getAttribute('content');
|
|
}
|
|
const elWithNonce = document.querySelector('script[nonce]');
|
|
if (elWithNonce) {
|
|
return elWithNonce.nonce || '';
|
|
}
|
|
return '';
|
|
}
|
|
// main
|
|
// -------------------------------------------------------------------------------------------------
|
|
if (typeof document !== 'undefined') {
|
|
registerStylesRoot(document);
|
|
}
|
|
|
|
var css_248z = ":root{--fc-small-font-size:.85em;--fc-page-bg-color:#fff;--fc-neutral-bg-color:hsla(0,0%,82%,.3);--fc-neutral-text-color:grey;--fc-border-color:#ddd;--fc-button-text-color:#fff;--fc-button-bg-color:#2c3e50;--fc-button-border-color:#2c3e50;--fc-button-hover-bg-color:#1e2b37;--fc-button-hover-border-color:#1a252f;--fc-button-active-bg-color:#1a252f;--fc-button-active-border-color:#151e27;--fc-event-bg-color:#3788d8;--fc-event-border-color:#3788d8;--fc-event-text-color:#fff;--fc-event-selected-overlay-color:rgba(0,0,0,.25);--fc-more-link-bg-color:#d0d0d0;--fc-more-link-text-color:inherit;--fc-event-resizer-thickness:8px;--fc-event-resizer-dot-total-width:8px;--fc-event-resizer-dot-border-width:1px;--fc-non-business-color:hsla(0,0%,84%,.3);--fc-bg-event-color:#8fdf82;--fc-bg-event-opacity:0.3;--fc-highlight-color:rgba(188,232,241,.3);--fc-today-bg-color:rgba(255,220,40,.15);--fc-now-indicator-color:red}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc{display:flex;flex-direction:column;font-size:1em}.fc,.fc *,.fc :after,.fc :before{box-sizing:border-box}.fc table{border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{padding:0;vertical-align:top}.fc a[data-navlink]{cursor:pointer}.fc a[data-navlink]:hover{text-decoration:underline}.fc-direction-ltr{direction:ltr;text-align:left}.fc-direction-rtl{direction:rtl;text-align:right}.fc-theme-standard td,.fc-theme-standard th{border:1px solid var(--fc-border-color)}.fc-liquid-hack td,.fc-liquid-hack th{position:relative}@font-face{font-family:fcicons;font-style:normal;font-weight:400;src:url(\"data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\") format(\"truetype\")}.fc-icon{speak:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;display:inline-block;font-family:fcicons!important;font-style:normal;font-variant:normal;font-weight:400;height:1em;line-height:1;text-align:center;text-transform:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:1em}.fc-icon-chevron-left:before{content:\"\\e900\"}.fc-icon-chevron-right:before{content:\"\\e901\"}.fc-icon-chevrons-left:before{content:\"\\e902\"}.fc-icon-chevrons-right:before{content:\"\\e903\"}.fc-icon-minus-square:before{content:\"\\e904\"}.fc-icon-plus-square:before{content:\"\\e905\"}.fc-icon-x:before{content:\"\\e906\"}.fc .fc-button{border-radius:0;font-family:inherit;font-size:inherit;line-height:inherit;margin:0;overflow:visible;text-transform:none}.fc .fc-button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}.fc .fc-button{-webkit-appearance:button}.fc .fc-button:not(:disabled){cursor:pointer}.fc .fc-button{background-color:transparent;border:1px solid transparent;border-radius:.25em;display:inline-block;font-size:1em;font-weight:400;line-height:1.5;padding:.4em .65em;text-align:center;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle}.fc .fc-button:hover{text-decoration:none}.fc .fc-button:focus{box-shadow:0 0 0 .2rem rgba(44,62,80,.25);outline:0}.fc .fc-button:disabled{opacity:.65}.fc .fc-button-primary{background-color:var(--fc-button-bg-color);border-color:var(--fc-button-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:hover{background-color:var(--fc-button-hover-bg-color);border-color:var(--fc-button-hover-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:disabled{background-color:var(--fc-button-bg-color);border-color:var(--fc-button-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:focus{box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc .fc-button-primary:not(:disabled).fc-button-active,.fc .fc-button-primary:not(:disabled):active{background-color:var(--fc-button-active-bg-color);border-color:var(--fc-button-active-border-color);color:var(--fc-button-text-color)}.fc .fc-button-primary:not(:disabled).fc-button-active:focus,.fc .fc-button-primary:not(:disabled):active:focus{box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc .fc-button .fc-icon{font-size:1.5em;vertical-align:middle}.fc .fc-button-group{display:inline-flex;position:relative;vertical-align:middle}.fc .fc-button-group>.fc-button{flex:1 1 auto;position:relative}.fc .fc-button-group>.fc-button.fc-button-active,.fc .fc-button-group>.fc-button:active,.fc .fc-button-group>.fc-button:focus,.fc .fc-button-group>.fc-button:hover{z-index:1}.fc-direction-ltr .fc-button-group>.fc-button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0;margin-left:-1px}.fc-direction-ltr .fc-button-group>.fc-button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.fc-direction-rtl .fc-button-group>.fc-button:not(:first-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.fc-direction-rtl .fc-button-group>.fc-button:not(:last-child){border-bottom-left-radius:0;border-top-left-radius:0}.fc .fc-toolbar{align-items:center;display:flex;justify-content:space-between}.fc .fc-toolbar.fc-header-toolbar{margin-bottom:1.5em}.fc .fc-toolbar.fc-footer-toolbar{margin-top:1.5em}.fc .fc-toolbar-title{font-size:1.75em;margin:0}.fc-direction-ltr .fc-toolbar>*>:not(:first-child){margin-left:.75em}.fc-direction-rtl .fc-toolbar>*>:not(:first-child){margin-right:.75em}.fc-direction-rtl .fc-toolbar-ltr{flex-direction:row-reverse}.fc .fc-scroller{-webkit-overflow-scrolling:touch;position:relative}.fc .fc-scroller-liquid{height:100%}.fc .fc-scroller-liquid-absolute{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-scroller-harness{direction:ltr;overflow:hidden;position:relative}.fc .fc-scroller-harness-liquid{height:100%}.fc-direction-rtl .fc-scroller-harness>.fc-scroller{direction:rtl}.fc-theme-standard .fc-scrollgrid{border:1px solid var(--fc-border-color)}.fc .fc-scrollgrid,.fc .fc-scrollgrid table{table-layout:fixed;width:100%}.fc .fc-scrollgrid table{border-left-style:hidden;border-right-style:hidden;border-top-style:hidden}.fc .fc-scrollgrid{border-bottom-width:0;border-collapse:separate;border-right-width:0}.fc .fc-scrollgrid-liquid{height:100%}.fc .fc-scrollgrid-section,.fc .fc-scrollgrid-section table,.fc .fc-scrollgrid-section>td{height:1px}.fc .fc-scrollgrid-section-liquid>td{height:100%}.fc .fc-scrollgrid-section>*{border-left-width:0;border-top-width:0}.fc .fc-scrollgrid-section-footer>*,.fc .fc-scrollgrid-section-header>*{border-bottom-width:0}.fc .fc-scrollgrid-section-body table,.fc .fc-scrollgrid-section-footer table{border-bottom-style:hidden}.fc .fc-scrollgrid-section-sticky>*{background:var(--fc-page-bg-color);position:sticky;z-index:3}.fc .fc-scrollgrid-section-header.fc-scrollgrid-section-sticky>*{top:0}.fc .fc-scrollgrid-section-footer.fc-scrollgrid-section-sticky>*{bottom:0}.fc .fc-scrollgrid-sticky-shim{height:1px;margin-bottom:-1px}.fc-sticky{position:sticky}.fc .fc-view-harness{flex-grow:1;position:relative}.fc .fc-view-harness-active>.fc-view{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-col-header-cell-cushion{display:inline-block;padding:2px 4px}.fc .fc-bg-event,.fc .fc-highlight,.fc .fc-non-business{bottom:0;left:0;position:absolute;right:0;top:0}.fc .fc-non-business{background:var(--fc-non-business-color)}.fc .fc-bg-event{background:var(--fc-bg-event-color);opacity:var(--fc-bg-event-opacity)}.fc .fc-bg-event .fc-event-title{font-size:var(--fc-small-font-size);font-style:italic;margin:.5em}.fc .fc-highlight{background:var(--fc-highlight-color)}.fc .fc-cell-shaded,.fc .fc-day-disabled{background:var(--fc-neutral-bg-color)}a.fc-event,a.fc-event:hover{text-decoration:none}.fc-event.fc-event-draggable,.fc-event[href]{cursor:pointer}.fc-event .fc-event-main{position:relative;z-index:2}.fc-event-dragging:not(.fc-event-selected){opacity:.75}.fc-event-dragging.fc-event-selected{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-event .fc-event-resizer{display:none;position:absolute;z-index:4}.fc-event-selected .fc-event-resizer,.fc-event:hover .fc-event-resizer{display:block}.fc-event-selected .fc-event-resizer{background:var(--fc-page-bg-color);border-color:inherit;border-radius:calc(var(--fc-event-resizer-dot-total-width)/2);border-style:solid;border-width:var(--fc-event-resizer-dot-border-width);height:var(--fc-event-resizer-dot-total-width);width:var(--fc-event-resizer-dot-total-width)}.fc-event-selected .fc-event-resizer:before{bottom:-20px;content:\"\";left:-20px;position:absolute;right:-20px;top:-20px}.fc-event-selected,.fc-event:focus{box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event-selected:before,.fc-event:focus:before{bottom:0;content:\"\";left:0;position:absolute;right:0;top:0;z-index:3}.fc-event-selected:after,.fc-event:focus:after{background:var(--fc-event-selected-overlay-color);bottom:-1px;content:\"\";left:-1px;position:absolute;right:-1px;top:-1px;z-index:1}.fc-h-event{background-color:var(--fc-event-bg-color);border:1px solid var(--fc-event-border-color);display:block}.fc-h-event .fc-event-main{color:var(--fc-event-text-color)}.fc-h-event .fc-event-main-frame{display:flex}.fc-h-event .fc-event-time{max-width:100%;overflow:hidden}.fc-h-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-width:0}.fc-h-event .fc-event-title{display:inline-block;left:0;max-width:100%;overflow:hidden;right:0;vertical-align:top}.fc-h-event.fc-event-selected:before{bottom:-10px;top:-10px}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-start),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-end){border-bottom-left-radius:0;border-left-width:0;border-top-left-radius:0}.fc-direction-ltr .fc-daygrid-block-event:not(.fc-event-end),.fc-direction-rtl .fc-daygrid-block-event:not(.fc-event-start){border-bottom-right-radius:0;border-right-width:0;border-top-right-radius:0}.fc-h-event:not(.fc-event-selected) .fc-event-resizer{bottom:0;top:0;width:var(--fc-event-resizer-thickness)}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end{cursor:w-resize;left:calc(var(--fc-event-resizer-thickness)*-.5)}.fc-direction-ltr .fc-h-event:not(.fc-event-selected) .fc-event-resizer-end,.fc-direction-rtl .fc-h-event:not(.fc-event-selected) .fc-event-resizer-start{cursor:e-resize;right:calc(var(--fc-event-resizer-thickness)*-.5)}.fc-h-event.fc-event-selected .fc-event-resizer{margin-top:calc(var(--fc-event-resizer-dot-total-width)*-.5);top:50%}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-start,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-end{left:calc(var(--fc-event-resizer-dot-total-width)*-.5)}.fc-direction-ltr .fc-h-event.fc-event-selected .fc-event-resizer-end,.fc-direction-rtl .fc-h-event.fc-event-selected .fc-event-resizer-start{right:calc(var(--fc-event-resizer-dot-total-width)*-.5)}.fc .fc-popover{box-shadow:0 2px 6px rgba(0,0,0,.15);position:absolute;z-index:9999}.fc .fc-popover-header{align-items:center;display:flex;flex-direction:row;justify-content:space-between;padding:3px 4px}.fc .fc-popover-title{margin:0 2px}.fc .fc-popover-close{cursor:pointer;font-size:1.1em;opacity:.65}.fc-theme-standard .fc-popover{background:var(--fc-page-bg-color);border:1px solid var(--fc-border-color)}.fc-theme-standard .fc-popover-header{background:var(--fc-neutral-bg-color)}";
|
|
injectStyles(css_248z);
|
|
|
|
class DelayedRunner {
|
|
constructor(drainedOption) {
|
|
this.drainedOption = drainedOption;
|
|
this.isRunning = false;
|
|
this.isDirty = false;
|
|
this.pauseDepths = {};
|
|
this.timeoutId = 0;
|
|
}
|
|
request(delay) {
|
|
this.isDirty = true;
|
|
if (!this.isPaused()) {
|
|
this.clearTimeout();
|
|
if (delay == null) {
|
|
this.tryDrain();
|
|
}
|
|
else {
|
|
this.timeoutId = setTimeout(// NOT OPTIMAL! TODO: look at debounce
|
|
this.tryDrain.bind(this), delay);
|
|
}
|
|
}
|
|
}
|
|
pause(scope = '') {
|
|
let { pauseDepths } = this;
|
|
pauseDepths[scope] = (pauseDepths[scope] || 0) + 1;
|
|
this.clearTimeout();
|
|
}
|
|
resume(scope = '', force) {
|
|
let { pauseDepths } = this;
|
|
if (scope in pauseDepths) {
|
|
if (force) {
|
|
delete pauseDepths[scope];
|
|
}
|
|
else {
|
|
pauseDepths[scope] -= 1;
|
|
let depth = pauseDepths[scope];
|
|
if (depth <= 0) {
|
|
delete pauseDepths[scope];
|
|
}
|
|
}
|
|
this.tryDrain();
|
|
}
|
|
}
|
|
isPaused() {
|
|
return Object.keys(this.pauseDepths).length;
|
|
}
|
|
tryDrain() {
|
|
if (!this.isRunning && !this.isPaused()) {
|
|
this.isRunning = true;
|
|
while (this.isDirty) {
|
|
this.isDirty = false;
|
|
this.drained(); // might set isDirty to true again
|
|
}
|
|
this.isRunning = false;
|
|
}
|
|
}
|
|
clear() {
|
|
this.clearTimeout();
|
|
this.isDirty = false;
|
|
this.pauseDepths = {};
|
|
}
|
|
clearTimeout() {
|
|
if (this.timeoutId) {
|
|
clearTimeout(this.timeoutId);
|
|
this.timeoutId = 0;
|
|
}
|
|
}
|
|
drained() {
|
|
if (this.drainedOption) {
|
|
this.drainedOption();
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeElement(el) {
|
|
if (el.parentNode) {
|
|
el.parentNode.removeChild(el);
|
|
}
|
|
}
|
|
// Querying
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
function elementClosest(el, selector) {
|
|
if (el.closest) {
|
|
return el.closest(selector);
|
|
// really bad fallback for IE
|
|
// from https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
|
}
|
|
if (!document.documentElement.contains(el)) {
|
|
return null;
|
|
}
|
|
do {
|
|
if (elementMatches(el, selector)) {
|
|
return el;
|
|
}
|
|
el = (el.parentElement || el.parentNode);
|
|
} while (el !== null && el.nodeType === 1);
|
|
return null;
|
|
}
|
|
function elementMatches(el, selector) {
|
|
let method = el.matches || el.matchesSelector || el.msMatchesSelector;
|
|
return method.call(el, selector);
|
|
}
|
|
// accepts multiple subject els
|
|
// returns a real array. good for methods like forEach
|
|
// TODO: accept the document
|
|
function findElements(container, selector) {
|
|
let containers = container instanceof HTMLElement ? [container] : container;
|
|
let allMatches = [];
|
|
for (let i = 0; i < containers.length; i += 1) {
|
|
let matches = containers[i].querySelectorAll(selector);
|
|
for (let j = 0; j < matches.length; j += 1) {
|
|
allMatches.push(matches[j]);
|
|
}
|
|
}
|
|
return allMatches;
|
|
}
|
|
// accepts multiple subject els
|
|
// only queries direct child elements // TODO: rename to findDirectChildren!
|
|
function findDirectChildren(parent, selector) {
|
|
let parents = parent instanceof HTMLElement ? [parent] : parent;
|
|
let allMatches = [];
|
|
for (let i = 0; i < parents.length; i += 1) {
|
|
let childNodes = parents[i].children; // only ever elements
|
|
for (let j = 0; j < childNodes.length; j += 1) {
|
|
let childNode = childNodes[j];
|
|
if (!selector || elementMatches(childNode, selector)) {
|
|
allMatches.push(childNode);
|
|
}
|
|
}
|
|
}
|
|
return allMatches;
|
|
}
|
|
// Style
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
const PIXEL_PROP_RE = /(top|left|right|bottom|width|height)$/i;
|
|
function applyStyle(el, props) {
|
|
for (let propName in props) {
|
|
applyStyleProp(el, propName, props[propName]);
|
|
}
|
|
}
|
|
function applyStyleProp(el, name, val) {
|
|
if (val == null) {
|
|
el.style[name] = '';
|
|
}
|
|
else if (typeof val === 'number' && PIXEL_PROP_RE.test(name)) {
|
|
el.style[name] = `${val}px`;
|
|
}
|
|
else {
|
|
el.style[name] = val;
|
|
}
|
|
}
|
|
// Event Handling
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
// if intercepting bubbled events at the document/window/body level,
|
|
// and want to see originating element (the 'target'), use this util instead
|
|
// of `ev.target` because it goes within web-component boundaries.
|
|
function getEventTargetViaRoot(ev) {
|
|
var _a, _b;
|
|
return (_b = (_a = ev.composedPath) === null || _a === void 0 ? void 0 : _a.call(ev)[0]) !== null && _b !== void 0 ? _b : ev.target;
|
|
}
|
|
// Unique ID for DOM attribute
|
|
let guid$1 = 0;
|
|
function getUniqueDomId() {
|
|
guid$1 += 1;
|
|
return 'fc-dom-' + guid$1;
|
|
}
|
|
|
|
// Stops a mouse/touch event from doing it's native browser action
|
|
function preventDefault(ev) {
|
|
ev.preventDefault();
|
|
}
|
|
// Event Delegation
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
function buildDelegationHandler(selector, handler) {
|
|
return (ev) => {
|
|
let matchedChild = elementClosest(ev.target, selector);
|
|
if (matchedChild) {
|
|
handler.call(matchedChild, ev, matchedChild);
|
|
}
|
|
};
|
|
}
|
|
function listenBySelector(container, eventType, selector, handler) {
|
|
let attachedHandler = buildDelegationHandler(selector, handler);
|
|
container.addEventListener(eventType, attachedHandler);
|
|
return () => {
|
|
container.removeEventListener(eventType, attachedHandler);
|
|
};
|
|
}
|
|
function listenToHoverBySelector(container, selector, onMouseEnter, onMouseLeave) {
|
|
let currentMatchedChild;
|
|
return listenBySelector(container, 'mouseover', selector, (mouseOverEv, matchedChild) => {
|
|
if (matchedChild !== currentMatchedChild) {
|
|
currentMatchedChild = matchedChild;
|
|
onMouseEnter(mouseOverEv, matchedChild);
|
|
let realOnMouseLeave = (mouseLeaveEv) => {
|
|
currentMatchedChild = null;
|
|
onMouseLeave(mouseLeaveEv, matchedChild);
|
|
matchedChild.removeEventListener('mouseleave', realOnMouseLeave);
|
|
};
|
|
// listen to the next mouseleave, and then unattach
|
|
matchedChild.addEventListener('mouseleave', realOnMouseLeave);
|
|
}
|
|
});
|
|
}
|
|
// Animation
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
const transitionEventNames = [
|
|
'webkitTransitionEnd',
|
|
'otransitionend',
|
|
'oTransitionEnd',
|
|
'msTransitionEnd',
|
|
'transitionend',
|
|
];
|
|
// triggered only when the next single subsequent transition finishes
|
|
function whenTransitionDone(el, callback) {
|
|
let realCallback = (ev) => {
|
|
callback(ev);
|
|
transitionEventNames.forEach((eventName) => {
|
|
el.removeEventListener(eventName, realCallback);
|
|
});
|
|
};
|
|
transitionEventNames.forEach((eventName) => {
|
|
el.addEventListener(eventName, realCallback); // cross-browser way to determine when the transition finishes
|
|
});
|
|
}
|
|
// ARIA workarounds
|
|
// ----------------------------------------------------------------------------------------------------------------
|
|
function createAriaClickAttrs(handler) {
|
|
return Object.assign({ onClick: handler }, createAriaKeyboardAttrs(handler));
|
|
}
|
|
function createAriaKeyboardAttrs(handler) {
|
|
return {
|
|
tabIndex: 0,
|
|
onKeyDown(ev) {
|
|
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
handler(ev);
|
|
ev.preventDefault(); // if space, don't scroll down page
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
let guidNumber = 0;
|
|
function guid() {
|
|
guidNumber += 1;
|
|
return String(guidNumber);
|
|
}
|
|
/* FullCalendar-specific DOM Utilities
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
// Make the mouse cursor express that an event is not allowed in the current area
|
|
function disableCursor() {
|
|
document.body.classList.add('fc-not-allowed');
|
|
}
|
|
// Returns the mouse cursor to its original look
|
|
function enableCursor() {
|
|
document.body.classList.remove('fc-not-allowed');
|
|
}
|
|
/* Selection
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
function preventSelection(el) {
|
|
el.style.userSelect = 'none';
|
|
el.style.webkitUserSelect = 'none';
|
|
el.addEventListener('selectstart', preventDefault);
|
|
}
|
|
function allowSelection(el) {
|
|
el.style.userSelect = '';
|
|
el.style.webkitUserSelect = '';
|
|
el.removeEventListener('selectstart', preventDefault);
|
|
}
|
|
/* Context Menu
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
function preventContextMenu(el) {
|
|
el.addEventListener('contextmenu', preventDefault);
|
|
}
|
|
function allowContextMenu(el) {
|
|
el.removeEventListener('contextmenu', preventDefault);
|
|
}
|
|
function parseFieldSpecs(input) {
|
|
let specs = [];
|
|
let tokens = [];
|
|
let i;
|
|
let token;
|
|
if (typeof input === 'string') {
|
|
tokens = input.split(/\s*,\s*/);
|
|
}
|
|
else if (typeof input === 'function') {
|
|
tokens = [input];
|
|
}
|
|
else if (Array.isArray(input)) {
|
|
tokens = input;
|
|
}
|
|
for (i = 0; i < tokens.length; i += 1) {
|
|
token = tokens[i];
|
|
if (typeof token === 'string') {
|
|
specs.push(token.charAt(0) === '-' ?
|
|
{ field: token.substring(1), order: -1 } :
|
|
{ field: token, order: 1 });
|
|
}
|
|
else if (typeof token === 'function') {
|
|
specs.push({ func: token });
|
|
}
|
|
}
|
|
return specs;
|
|
}
|
|
function compareByFieldSpecs(obj0, obj1, fieldSpecs) {
|
|
let i;
|
|
let cmp;
|
|
for (i = 0; i < fieldSpecs.length; i += 1) {
|
|
cmp = compareByFieldSpec(obj0, obj1, fieldSpecs[i]);
|
|
if (cmp) {
|
|
return cmp;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
function compareByFieldSpec(obj0, obj1, fieldSpec) {
|
|
if (fieldSpec.func) {
|
|
return fieldSpec.func(obj0, obj1);
|
|
}
|
|
return flexibleCompare(obj0[fieldSpec.field], obj1[fieldSpec.field])
|
|
* (fieldSpec.order || 1);
|
|
}
|
|
function flexibleCompare(a, b) {
|
|
if (!a && !b) {
|
|
return 0;
|
|
}
|
|
if (b == null) {
|
|
return -1;
|
|
}
|
|
if (a == null) {
|
|
return 1;
|
|
}
|
|
if (typeof a === 'string' || typeof b === 'string') {
|
|
return String(a).localeCompare(String(b));
|
|
}
|
|
return a - b;
|
|
}
|
|
/* String Utilities
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
function padStart(val, len) {
|
|
let s = String(val);
|
|
return '000'.substr(0, len - s.length) + s;
|
|
}
|
|
function formatWithOrdinals(formatter, args, fallbackText) {
|
|
if (typeof formatter === 'function') {
|
|
return formatter(...args);
|
|
}
|
|
if (typeof formatter === 'string') { // non-blank string
|
|
return args.reduce((str, arg, index) => (str.replace('$' + index, arg || '')), formatter);
|
|
}
|
|
return fallbackText;
|
|
}
|
|
/* Number Utilities
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
function compareNumbers(a, b) {
|
|
return a - b;
|
|
}
|
|
function isInt(n) {
|
|
return n % 1 === 0;
|
|
}
|
|
/* FC-specific DOM dimension stuff
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
function computeSmallestCellWidth(cellEl) {
|
|
let allWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-frame');
|
|
let contentWidthEl = cellEl.querySelector('.fc-scrollgrid-shrink-cushion');
|
|
if (!allWidthEl) {
|
|
throw new Error('needs fc-scrollgrid-shrink-frame className'); // TODO: use const
|
|
}
|
|
if (!contentWidthEl) {
|
|
throw new Error('needs fc-scrollgrid-shrink-cushion className');
|
|
}
|
|
return cellEl.getBoundingClientRect().width - allWidthEl.getBoundingClientRect().width + // the cell padding+border
|
|
contentWidthEl.getBoundingClientRect().width;
|
|
}
|
|
|
|
const INTERNAL_UNITS = ['years', 'months', 'days', 'milliseconds'];
|
|
const PARSE_RE = /^(-?)(?:(\d+)\.)?(\d+):(\d\d)(?::(\d\d)(?:\.(\d\d\d))?)?/;
|
|
// Parsing and Creation
|
|
function createDuration(input, unit) {
|
|
if (typeof input === 'string') {
|
|
return parseString(input);
|
|
}
|
|
if (typeof input === 'object' && input) { // non-null object
|
|
return parseObject(input);
|
|
}
|
|
if (typeof input === 'number') {
|
|
return parseObject({ [unit || 'milliseconds']: input });
|
|
}
|
|
return null;
|
|
}
|
|
function parseString(s) {
|
|
let m = PARSE_RE.exec(s);
|
|
if (m) {
|
|
let sign = m[1] ? -1 : 1;
|
|
return {
|
|
years: 0,
|
|
months: 0,
|
|
days: sign * (m[2] ? parseInt(m[2], 10) : 0),
|
|
milliseconds: sign * ((m[3] ? parseInt(m[3], 10) : 0) * 60 * 60 * 1000 + // hours
|
|
(m[4] ? parseInt(m[4], 10) : 0) * 60 * 1000 + // minutes
|
|
(m[5] ? parseInt(m[5], 10) : 0) * 1000 + // seconds
|
|
(m[6] ? parseInt(m[6], 10) : 0) // ms
|
|
),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
function parseObject(obj) {
|
|
let duration = {
|
|
years: obj.years || obj.year || 0,
|
|
months: obj.months || obj.month || 0,
|
|
days: obj.days || obj.day || 0,
|
|
milliseconds: (obj.hours || obj.hour || 0) * 60 * 60 * 1000 + // hours
|
|
(obj.minutes || obj.minute || 0) * 60 * 1000 + // minutes
|
|
(obj.seconds || obj.second || 0) * 1000 + // seconds
|
|
(obj.milliseconds || obj.millisecond || obj.ms || 0), // ms
|
|
};
|
|
let weeks = obj.weeks || obj.week;
|
|
if (weeks) {
|
|
duration.days += weeks * 7;
|
|
duration.specifiedWeeks = true;
|
|
}
|
|
return duration;
|
|
}
|
|
// Equality
|
|
function durationsEqual(d0, d1) {
|
|
return d0.years === d1.years &&
|
|
d0.months === d1.months &&
|
|
d0.days === d1.days &&
|
|
d0.milliseconds === d1.milliseconds;
|
|
}
|
|
function asCleanDays(dur) {
|
|
if (!dur.years && !dur.months && !dur.milliseconds) {
|
|
return dur.days;
|
|
}
|
|
return 0;
|
|
}
|
|
// Simple Math
|
|
function addDurations(d0, d1) {
|
|
return {
|
|
years: d0.years + d1.years,
|
|
months: d0.months + d1.months,
|
|
days: d0.days + d1.days,
|
|
milliseconds: d0.milliseconds + d1.milliseconds,
|
|
};
|
|
}
|
|
function subtractDurations(d1, d0) {
|
|
return {
|
|
years: d1.years - d0.years,
|
|
months: d1.months - d0.months,
|
|
days: d1.days - d0.days,
|
|
milliseconds: d1.milliseconds - d0.milliseconds,
|
|
};
|
|
}
|
|
function multiplyDuration(d, n) {
|
|
return {
|
|
years: d.years * n,
|
|
months: d.months * n,
|
|
days: d.days * n,
|
|
milliseconds: d.milliseconds * n,
|
|
};
|
|
}
|
|
// Conversions
|
|
// "Rough" because they are based on average-case Gregorian months/years
|
|
function asRoughYears(dur) {
|
|
return asRoughDays(dur) / 365;
|
|
}
|
|
function asRoughMonths(dur) {
|
|
return asRoughDays(dur) / 30;
|
|
}
|
|
function asRoughDays(dur) {
|
|
return asRoughMs(dur) / 864e5;
|
|
}
|
|
function asRoughMinutes(dur) {
|
|
return asRoughMs(dur) / (1000 * 60);
|
|
}
|
|
function asRoughSeconds(dur) {
|
|
return asRoughMs(dur) / 1000;
|
|
}
|
|
function asRoughMs(dur) {
|
|
return dur.years * (365 * 864e5) +
|
|
dur.months * (30 * 864e5) +
|
|
dur.days * 864e5 +
|
|
dur.milliseconds;
|
|
}
|
|
// Advanced Math
|
|
function wholeDivideDurations(numerator, denominator) {
|
|
let res = null;
|
|
for (let i = 0; i < INTERNAL_UNITS.length; i += 1) {
|
|
let unit = INTERNAL_UNITS[i];
|
|
if (denominator[unit]) {
|
|
let localRes = numerator[unit] / denominator[unit];
|
|
if (!isInt(localRes) || (res !== null && res !== localRes)) {
|
|
return null;
|
|
}
|
|
res = localRes;
|
|
}
|
|
else if (numerator[unit]) {
|
|
// needs to divide by something but can't!
|
|
return null;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
function greatestDurationDenominator(dur) {
|
|
let ms = dur.milliseconds;
|
|
if (ms) {
|
|
if (ms % 1000 !== 0) {
|
|
return { unit: 'millisecond', value: ms };
|
|
}
|
|
if (ms % (1000 * 60) !== 0) {
|
|
return { unit: 'second', value: ms / 1000 };
|
|
}
|
|
if (ms % (1000 * 60 * 60) !== 0) {
|
|
return { unit: 'minute', value: ms / (1000 * 60) };
|
|
}
|
|
if (ms) {
|
|
return { unit: 'hour', value: ms / (1000 * 60 * 60) };
|
|
}
|
|
}
|
|
if (dur.days) {
|
|
if (dur.specifiedWeeks && dur.days % 7 === 0) {
|
|
return { unit: 'week', value: dur.days / 7 };
|
|
}
|
|
return { unit: 'day', value: dur.days };
|
|
}
|
|
if (dur.months) {
|
|
return { unit: 'month', value: dur.months };
|
|
}
|
|
if (dur.years) {
|
|
return { unit: 'year', value: dur.years };
|
|
}
|
|
return { unit: 'millisecond', value: 0 };
|
|
}
|
|
|
|
// TODO: new util arrayify?
|
|
function removeExact(array, exactVal) {
|
|
let removeCnt = 0;
|
|
let i = 0;
|
|
while (i < array.length) {
|
|
if (array[i] === exactVal) {
|
|
array.splice(i, 1);
|
|
removeCnt += 1;
|
|
}
|
|
else {
|
|
i += 1;
|
|
}
|
|
}
|
|
return removeCnt;
|
|
}
|
|
function isArraysEqual(a0, a1, equalityFunc) {
|
|
if (a0 === a1) {
|
|
return true;
|
|
}
|
|
let len = a0.length;
|
|
let i;
|
|
if (len !== a1.length) { // not array? or not same length?
|
|
return false;
|
|
}
|
|
for (i = 0; i < len; i += 1) {
|
|
if (!(equalityFunc ? equalityFunc(a0[i], a1[i]) : a0[i] === a1[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const DAY_IDS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
// Adding
|
|
function addWeeks(m, n) {
|
|
let a = dateToUtcArray(m);
|
|
a[2] += n * 7;
|
|
return arrayToUtcDate(a);
|
|
}
|
|
function addDays(m, n) {
|
|
let a = dateToUtcArray(m);
|
|
a[2] += n;
|
|
return arrayToUtcDate(a);
|
|
}
|
|
function addMs(m, n) {
|
|
let a = dateToUtcArray(m);
|
|
a[6] += n;
|
|
return arrayToUtcDate(a);
|
|
}
|
|
// Diffing (all return floats)
|
|
// TODO: why not use ranges?
|
|
function diffWeeks(m0, m1) {
|
|
return diffDays(m0, m1) / 7;
|
|
}
|
|
function diffDays(m0, m1) {
|
|
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60 * 24);
|
|
}
|
|
function diffHours(m0, m1) {
|
|
return (m1.valueOf() - m0.valueOf()) / (1000 * 60 * 60);
|
|
}
|
|
function diffMinutes(m0, m1) {
|
|
return (m1.valueOf() - m0.valueOf()) / (1000 * 60);
|
|
}
|
|
function diffSeconds(m0, m1) {
|
|
return (m1.valueOf() - m0.valueOf()) / 1000;
|
|
}
|
|
function diffDayAndTime(m0, m1) {
|
|
let m0day = startOfDay(m0);
|
|
let m1day = startOfDay(m1);
|
|
return {
|
|
years: 0,
|
|
months: 0,
|
|
days: Math.round(diffDays(m0day, m1day)),
|
|
milliseconds: (m1.valueOf() - m1day.valueOf()) - (m0.valueOf() - m0day.valueOf()),
|
|
};
|
|
}
|
|
// Diffing Whole Units
|
|
function diffWholeWeeks(m0, m1) {
|
|
let d = diffWholeDays(m0, m1);
|
|
if (d !== null && d % 7 === 0) {
|
|
return d / 7;
|
|
}
|
|
return null;
|
|
}
|
|
function diffWholeDays(m0, m1) {
|
|
if (timeAsMs(m0) === timeAsMs(m1)) {
|
|
return Math.round(diffDays(m0, m1));
|
|
}
|
|
return null;
|
|
}
|
|
// Start-Of
|
|
function startOfDay(m) {
|
|
return arrayToUtcDate([
|
|
m.getUTCFullYear(),
|
|
m.getUTCMonth(),
|
|
m.getUTCDate(),
|
|
]);
|
|
}
|
|
function startOfHour(m) {
|
|
return arrayToUtcDate([
|
|
m.getUTCFullYear(),
|
|
m.getUTCMonth(),
|
|
m.getUTCDate(),
|
|
m.getUTCHours(),
|
|
]);
|
|
}
|
|
function startOfMinute(m) {
|
|
return arrayToUtcDate([
|
|
m.getUTCFullYear(),
|
|
m.getUTCMonth(),
|
|
m.getUTCDate(),
|
|
m.getUTCHours(),
|
|
m.getUTCMinutes(),
|
|
]);
|
|
}
|
|
function startOfSecond(m) {
|
|
return arrayToUtcDate([
|
|
m.getUTCFullYear(),
|
|
m.getUTCMonth(),
|
|
m.getUTCDate(),
|
|
m.getUTCHours(),
|
|
m.getUTCMinutes(),
|
|
m.getUTCSeconds(),
|
|
]);
|
|
}
|
|
// Week Computation
|
|
function weekOfYear(marker, dow, doy) {
|
|
let y = marker.getUTCFullYear();
|
|
let w = weekOfGivenYear(marker, y, dow, doy);
|
|
if (w < 1) {
|
|
return weekOfGivenYear(marker, y - 1, dow, doy);
|
|
}
|
|
let nextW = weekOfGivenYear(marker, y + 1, dow, doy);
|
|
if (nextW >= 1) {
|
|
return Math.min(w, nextW);
|
|
}
|
|
return w;
|
|
}
|
|
function weekOfGivenYear(marker, year, dow, doy) {
|
|
let firstWeekStart = arrayToUtcDate([year, 0, 1 + firstWeekOffset(year, dow, doy)]);
|
|
let dayStart = startOfDay(marker);
|
|
let days = Math.round(diffDays(firstWeekStart, dayStart));
|
|
return Math.floor(days / 7) + 1; // zero-indexed
|
|
}
|
|
// start-of-first-week - start-of-year
|
|
function firstWeekOffset(year, dow, doy) {
|
|
// first-week day -- which january is always in the first week (4 for iso, 1 for other)
|
|
let fwd = 7 + dow - doy;
|
|
// first-week day local weekday -- which local weekday is fwd
|
|
let fwdlw = (7 + arrayToUtcDate([year, 0, fwd]).getUTCDay() - dow) % 7;
|
|
return -fwdlw + fwd - 1;
|
|
}
|
|
// Array Conversion
|
|
function dateToLocalArray(date) {
|
|
return [
|
|
date.getFullYear(),
|
|
date.getMonth(),
|
|
date.getDate(),
|
|
date.getHours(),
|
|
date.getMinutes(),
|
|
date.getSeconds(),
|
|
date.getMilliseconds(),
|
|
];
|
|
}
|
|
function arrayToLocalDate(a) {
|
|
return new Date(a[0], a[1] || 0, a[2] == null ? 1 : a[2], // day of month
|
|
a[3] || 0, a[4] || 0, a[5] || 0);
|
|
}
|
|
function dateToUtcArray(date) {
|
|
return [
|
|
date.getUTCFullYear(),
|
|
date.getUTCMonth(),
|
|
date.getUTCDate(),
|
|
date.getUTCHours(),
|
|
date.getUTCMinutes(),
|
|
date.getUTCSeconds(),
|
|
date.getUTCMilliseconds(),
|
|
];
|
|
}
|
|
function arrayToUtcDate(a) {
|
|
// according to web standards (and Safari), a month index is required.
|
|
// massage if only given a year.
|
|
if (a.length === 1) {
|
|
a = a.concat([0]);
|
|
}
|
|
return new Date(Date.UTC(...a));
|
|
}
|
|
// Other Utils
|
|
function isValidDate(m) {
|
|
return !isNaN(m.valueOf());
|
|
}
|
|
function timeAsMs(m) {
|
|
return m.getUTCHours() * 1000 * 60 * 60 +
|
|
m.getUTCMinutes() * 1000 * 60 +
|
|
m.getUTCSeconds() * 1000 +
|
|
m.getUTCMilliseconds();
|
|
}
|
|
|
|
// timeZoneOffset is in minutes
|
|
function buildIsoString(marker, timeZoneOffset, stripZeroTime = false) {
|
|
let s = marker.toISOString();
|
|
s = s.replace('.000', '');
|
|
if (stripZeroTime) {
|
|
s = s.replace('T00:00:00Z', '');
|
|
}
|
|
if (s.length > 10) { // time part wasn't stripped, can add timezone info
|
|
if (timeZoneOffset == null) {
|
|
s = s.replace('Z', '');
|
|
}
|
|
else if (timeZoneOffset !== 0) {
|
|
s = s.replace('Z', formatTimeZoneOffset(timeZoneOffset, true));
|
|
}
|
|
// otherwise, its UTC-0 and we want to keep the Z
|
|
}
|
|
return s;
|
|
}
|
|
// formats the date, but with no time part
|
|
// TODO: somehow merge with buildIsoString and stripZeroTime
|
|
// TODO: rename. omit "string"
|
|
function formatDayString(marker) {
|
|
return marker.toISOString().replace(/T.*$/, '');
|
|
}
|
|
function formatIsoMonthStr(marker) {
|
|
return marker.toISOString().match(/^\d{4}-\d{2}/)[0];
|
|
}
|
|
// TODO: use Date::toISOString and use everything after the T?
|
|
function formatIsoTimeString(marker) {
|
|
return padStart(marker.getUTCHours(), 2) + ':' +
|
|
padStart(marker.getUTCMinutes(), 2) + ':' +
|
|
padStart(marker.getUTCSeconds(), 2);
|
|
}
|
|
function formatTimeZoneOffset(minutes, doIso = false) {
|
|
let sign = minutes < 0 ? '-' : '+';
|
|
let abs = Math.abs(minutes);
|
|
let hours = Math.floor(abs / 60);
|
|
let mins = Math.round(abs % 60);
|
|
if (doIso) {
|
|
return `${sign + padStart(hours, 2)}:${padStart(mins, 2)}`;
|
|
}
|
|
return `GMT${sign}${hours}${mins ? `:${padStart(mins, 2)}` : ''}`;
|
|
}
|
|
|
|
function memoize(workerFunc, resEquality, teardownFunc) {
|
|
let currentArgs;
|
|
let currentRes;
|
|
return function (...newArgs) {
|
|
if (!currentArgs) {
|
|
currentRes = workerFunc.apply(this, newArgs);
|
|
}
|
|
else if (!isArraysEqual(currentArgs, newArgs)) {
|
|
if (teardownFunc) {
|
|
teardownFunc(currentRes);
|
|
}
|
|
let res = workerFunc.apply(this, newArgs);
|
|
if (!resEquality || !resEquality(res, currentRes)) {
|
|
currentRes = res;
|
|
}
|
|
}
|
|
currentArgs = newArgs;
|
|
return currentRes;
|
|
};
|
|
}
|
|
function memoizeObjArg(workerFunc, resEquality, teardownFunc) {
|
|
let currentArg;
|
|
let currentRes;
|
|
return (newArg) => {
|
|
if (!currentArg) {
|
|
currentRes = workerFunc.call(this, newArg);
|
|
}
|
|
else if (!isPropsEqual(currentArg, newArg)) {
|
|
if (teardownFunc) {
|
|
teardownFunc(currentRes);
|
|
}
|
|
let res = workerFunc.call(this, newArg);
|
|
if (!resEquality || !resEquality(res, currentRes)) {
|
|
currentRes = res;
|
|
}
|
|
}
|
|
currentArg = newArg;
|
|
return currentRes;
|
|
};
|
|
}
|
|
function memoizeArraylike(// used at all?
|
|
workerFunc, resEquality, teardownFunc) {
|
|
let currentArgSets = [];
|
|
let currentResults = [];
|
|
return (newArgSets) => {
|
|
let currentLen = currentArgSets.length;
|
|
let newLen = newArgSets.length;
|
|
let i = 0;
|
|
for (; i < currentLen; i += 1) {
|
|
if (!newArgSets[i]) { // one of the old sets no longer exists
|
|
if (teardownFunc) {
|
|
teardownFunc(currentResults[i]);
|
|
}
|
|
}
|
|
else if (!isArraysEqual(currentArgSets[i], newArgSets[i])) {
|
|
if (teardownFunc) {
|
|
teardownFunc(currentResults[i]);
|
|
}
|
|
let res = workerFunc.apply(this, newArgSets[i]);
|
|
if (!resEquality || !resEquality(res, currentResults[i])) {
|
|
currentResults[i] = res;
|
|
}
|
|
}
|
|
}
|
|
for (; i < newLen; i += 1) {
|
|
currentResults[i] = workerFunc.apply(this, newArgSets[i]);
|
|
}
|
|
currentArgSets = newArgSets;
|
|
currentResults.splice(newLen); // remove excess
|
|
return currentResults;
|
|
};
|
|
}
|
|
function memoizeHashlike(workerFunc, resEquality, teardownFunc) {
|
|
let currentArgHash = {};
|
|
let currentResHash = {};
|
|
return (newArgHash) => {
|
|
let newResHash = {};
|
|
for (let key in newArgHash) {
|
|
if (!currentResHash[key]) {
|
|
newResHash[key] = workerFunc.apply(this, newArgHash[key]);
|
|
}
|
|
else if (!isArraysEqual(currentArgHash[key], newArgHash[key])) {
|
|
if (teardownFunc) {
|
|
teardownFunc(currentResHash[key]);
|
|
}
|
|
let res = workerFunc.apply(this, newArgHash[key]);
|
|
newResHash[key] = (resEquality && resEquality(res, currentResHash[key]))
|
|
? currentResHash[key]
|
|
: res;
|
|
}
|
|
else {
|
|
newResHash[key] = currentResHash[key];
|
|
}
|
|
}
|
|
currentArgHash = newArgHash;
|
|
currentResHash = newResHash;
|
|
return newResHash;
|
|
};
|
|
}
|
|
|
|
const EXTENDED_SETTINGS_AND_SEVERITIES = {
|
|
week: 3,
|
|
separator: 9,
|
|
omitZeroMinute: 9,
|
|
meridiem: 9,
|
|
omitCommas: 9,
|
|
};
|
|
const STANDARD_DATE_PROP_SEVERITIES = {
|
|
timeZoneName: 7,
|
|
era: 6,
|
|
year: 5,
|
|
month: 4,
|
|
day: 2,
|
|
weekday: 2,
|
|
hour: 1,
|
|
minute: 1,
|
|
second: 1,
|
|
};
|
|
const MERIDIEM_RE = /\s*([ap])\.?m\.?/i; // eats up leading spaces too
|
|
const COMMA_RE = /,/g; // we need re for globalness
|
|
const MULTI_SPACE_RE = /\s+/g;
|
|
const LTR_RE = /\u200e/g; // control character
|
|
const UTC_RE = /UTC|GMT/;
|
|
class NativeFormatter {
|
|
constructor(formatSettings) {
|
|
let standardDateProps = {};
|
|
let extendedSettings = {};
|
|
let smallestUnitNum = 9; // the smallest unit in the formatter (9 is a sentinel, beyond max)
|
|
for (let name in formatSettings) {
|
|
if (name in EXTENDED_SETTINGS_AND_SEVERITIES) {
|
|
extendedSettings[name] = formatSettings[name];
|
|
const severity = EXTENDED_SETTINGS_AND_SEVERITIES[name];
|
|
if (severity < 9) {
|
|
smallestUnitNum = Math.min(EXTENDED_SETTINGS_AND_SEVERITIES[name], smallestUnitNum);
|
|
}
|
|
}
|
|
else {
|
|
standardDateProps[name] = formatSettings[name];
|
|
if (name in STANDARD_DATE_PROP_SEVERITIES) { // TODO: what about hour12? no severity
|
|
smallestUnitNum = Math.min(STANDARD_DATE_PROP_SEVERITIES[name], smallestUnitNum);
|
|
}
|
|
}
|
|
}
|
|
this.standardDateProps = standardDateProps;
|
|
this.extendedSettings = extendedSettings;
|
|
this.smallestUnitNum = smallestUnitNum;
|
|
this.buildFormattingFunc = memoize(buildFormattingFunc);
|
|
}
|
|
format(date, context) {
|
|
return this.buildFormattingFunc(this.standardDateProps, this.extendedSettings, context)(date);
|
|
}
|
|
formatRange(start, end, context, betterDefaultSeparator) {
|
|
let { standardDateProps, extendedSettings } = this;
|
|
let diffSeverity = computeMarkerDiffSeverity(start.marker, end.marker, context.calendarSystem);
|
|
if (!diffSeverity) {
|
|
return this.format(start, context);
|
|
}
|
|
let biggestUnitForPartial = diffSeverity;
|
|
if (biggestUnitForPartial > 1 && // the two dates are different in a way that's larger scale than time
|
|
(standardDateProps.year === 'numeric' || standardDateProps.year === '2-digit') &&
|
|
(standardDateProps.month === 'numeric' || standardDateProps.month === '2-digit') &&
|
|
(standardDateProps.day === 'numeric' || standardDateProps.day === '2-digit')) {
|
|
biggestUnitForPartial = 1; // make it look like the dates are only different in terms of time
|
|
}
|
|
let full0 = this.format(start, context);
|
|
let full1 = this.format(end, context);
|
|
if (full0 === full1) {
|
|
return full0;
|
|
}
|
|
let partialDateProps = computePartialFormattingOptions(standardDateProps, biggestUnitForPartial);
|
|
let partialFormattingFunc = buildFormattingFunc(partialDateProps, extendedSettings, context);
|
|
let partial0 = partialFormattingFunc(start);
|
|
let partial1 = partialFormattingFunc(end);
|
|
let insertion = findCommonInsertion(full0, partial0, full1, partial1);
|
|
let separator = extendedSettings.separator || betterDefaultSeparator || context.defaultSeparator || '';
|
|
if (insertion) {
|
|
return insertion.before + partial0 + separator + partial1 + insertion.after;
|
|
}
|
|
return full0 + separator + full1;
|
|
}
|
|
getSmallestUnit() {
|
|
switch (this.smallestUnitNum) {
|
|
case 7:
|
|
case 6:
|
|
case 5:
|
|
return 'year';
|
|
case 4:
|
|
return 'month';
|
|
case 3:
|
|
return 'week';
|
|
case 2:
|
|
return 'day';
|
|
default:
|
|
return 'time'; // really?
|
|
}
|
|
}
|
|
}
|
|
function buildFormattingFunc(standardDateProps, extendedSettings, context) {
|
|
let standardDatePropCnt = Object.keys(standardDateProps).length;
|
|
if (standardDatePropCnt === 1 && standardDateProps.timeZoneName === 'short') {
|
|
return (date) => (formatTimeZoneOffset(date.timeZoneOffset));
|
|
}
|
|
if (standardDatePropCnt === 0 && extendedSettings.week) {
|
|
return (date) => (formatWeekNumber(context.computeWeekNumber(date.marker), context.weekText, context.weekTextLong, context.locale, extendedSettings.week));
|
|
}
|
|
return buildNativeFormattingFunc(standardDateProps, extendedSettings, context);
|
|
}
|
|
function buildNativeFormattingFunc(standardDateProps, extendedSettings, context) {
|
|
standardDateProps = Object.assign({}, standardDateProps); // copy
|
|
extendedSettings = Object.assign({}, extendedSettings); // copy
|
|
sanitizeSettings(standardDateProps, extendedSettings);
|
|
standardDateProps.timeZone = 'UTC'; // we leverage the only guaranteed timeZone for our UTC markers
|
|
let normalFormat = new Intl.DateTimeFormat(context.locale.codes, standardDateProps);
|
|
let zeroFormat; // needed?
|
|
if (extendedSettings.omitZeroMinute) {
|
|
let zeroProps = Object.assign({}, standardDateProps);
|
|
delete zeroProps.minute; // seconds and ms were already considered in sanitizeSettings
|
|
zeroFormat = new Intl.DateTimeFormat(context.locale.codes, zeroProps);
|
|
}
|
|
return (date) => {
|
|
let { marker } = date;
|
|
let format;
|
|
if (zeroFormat && !marker.getUTCMinutes()) {
|
|
format = zeroFormat;
|
|
}
|
|
else {
|
|
format = normalFormat;
|
|
}
|
|
let s = format.format(marker);
|
|
return postProcess(s, date, standardDateProps, extendedSettings, context);
|
|
};
|
|
}
|
|
function sanitizeSettings(standardDateProps, extendedSettings) {
|
|
// deal with a browser inconsistency where formatting the timezone
|
|
// requires that the hour/minute be present.
|
|
if (standardDateProps.timeZoneName) {
|
|
if (!standardDateProps.hour) {
|
|
standardDateProps.hour = '2-digit';
|
|
}
|
|
if (!standardDateProps.minute) {
|
|
standardDateProps.minute = '2-digit';
|
|
}
|
|
}
|
|
// only support short timezone names
|
|
if (standardDateProps.timeZoneName === 'long') {
|
|
standardDateProps.timeZoneName = 'short';
|
|
}
|
|
// if requesting to display seconds, MUST display minutes
|
|
if (extendedSettings.omitZeroMinute && (standardDateProps.second || standardDateProps.millisecond)) {
|
|
delete extendedSettings.omitZeroMinute;
|
|
}
|
|
}
|
|
function postProcess(s, date, standardDateProps, extendedSettings, context) {
|
|
s = s.replace(LTR_RE, ''); // remove left-to-right control chars. do first. good for other regexes
|
|
if (standardDateProps.timeZoneName === 'short') {
|
|
s = injectTzoStr(s, (context.timeZone === 'UTC' || date.timeZoneOffset == null) ?
|
|
'UTC' : // important to normalize for IE, which does "GMT"
|
|
formatTimeZoneOffset(date.timeZoneOffset));
|
|
}
|
|
if (extendedSettings.omitCommas) {
|
|
s = s.replace(COMMA_RE, '').trim();
|
|
}
|
|
if (extendedSettings.omitZeroMinute) {
|
|
s = s.replace(':00', ''); // zeroFormat doesn't always achieve this
|
|
}
|
|
// ^ do anything that might create adjacent spaces before this point,
|
|
// because MERIDIEM_RE likes to eat up loading spaces
|
|
if (extendedSettings.meridiem === false) {
|
|
s = s.replace(MERIDIEM_RE, '').trim();
|
|
}
|
|
else if (extendedSettings.meridiem === 'narrow') { // a/p
|
|
s = s.replace(MERIDIEM_RE, (m0, m1) => m1.toLocaleLowerCase());
|
|
}
|
|
else if (extendedSettings.meridiem === 'short') { // am/pm
|
|
s = s.replace(MERIDIEM_RE, (m0, m1) => `${m1.toLocaleLowerCase()}m`);
|
|
}
|
|
else if (extendedSettings.meridiem === 'lowercase') { // other meridiem transformers already converted to lowercase
|
|
s = s.replace(MERIDIEM_RE, (m0) => m0.toLocaleLowerCase());
|
|
}
|
|
s = s.replace(MULTI_SPACE_RE, ' ');
|
|
s = s.trim();
|
|
return s;
|
|
}
|
|
function injectTzoStr(s, tzoStr) {
|
|
let replaced = false;
|
|
s = s.replace(UTC_RE, () => {
|
|
replaced = true;
|
|
return tzoStr;
|
|
});
|
|
// IE11 doesn't include UTC/GMT in the original string, so append to end
|
|
if (!replaced) {
|
|
s += ` ${tzoStr}`;
|
|
}
|
|
return s;
|
|
}
|
|
function formatWeekNumber(num, weekText, weekTextLong, locale, display) {
|
|
let parts = [];
|
|
if (display === 'long') {
|
|
parts.push(weekTextLong);
|
|
}
|
|
else if (display === 'short' || display === 'narrow') {
|
|
parts.push(weekText);
|
|
}
|
|
if (display === 'long' || display === 'short') {
|
|
parts.push(' ');
|
|
}
|
|
parts.push(locale.simpleNumberFormat.format(num));
|
|
if (locale.options.direction === 'rtl') { // TODO: use control characters instead?
|
|
parts.reverse();
|
|
}
|
|
return parts.join('');
|
|
}
|
|
// Range Formatting Utils
|
|
// 0 = exactly the same
|
|
// 1 = different by time
|
|
// and bigger
|
|
function computeMarkerDiffSeverity(d0, d1, ca) {
|
|
if (ca.getMarkerYear(d0) !== ca.getMarkerYear(d1)) {
|
|
return 5;
|
|
}
|
|
if (ca.getMarkerMonth(d0) !== ca.getMarkerMonth(d1)) {
|
|
return 4;
|
|
}
|
|
if (ca.getMarkerDay(d0) !== ca.getMarkerDay(d1)) {
|
|
return 2;
|
|
}
|
|
if (timeAsMs(d0) !== timeAsMs(d1)) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
function computePartialFormattingOptions(options, biggestUnit) {
|
|
let partialOptions = {};
|
|
for (let name in options) {
|
|
if (!(name in STANDARD_DATE_PROP_SEVERITIES) || // not a date part prop (like timeZone)
|
|
STANDARD_DATE_PROP_SEVERITIES[name] <= biggestUnit) {
|
|
partialOptions[name] = options[name];
|
|
}
|
|
}
|
|
return partialOptions;
|
|
}
|
|
function findCommonInsertion(full0, partial0, full1, partial1) {
|
|
let i0 = 0;
|
|
while (i0 < full0.length) {
|
|
let found0 = full0.indexOf(partial0, i0);
|
|
if (found0 === -1) {
|
|
break;
|
|
}
|
|
let before0 = full0.substr(0, found0);
|
|
i0 = found0 + partial0.length;
|
|
let after0 = full0.substr(i0);
|
|
let i1 = 0;
|
|
while (i1 < full1.length) {
|
|
let found1 = full1.indexOf(partial1, i1);
|
|
if (found1 === -1) {
|
|
break;
|
|
}
|
|
let before1 = full1.substr(0, found1);
|
|
i1 = found1 + partial1.length;
|
|
let after1 = full1.substr(i1);
|
|
if (before0 === before1 && after0 === after1) {
|
|
return {
|
|
before: before0,
|
|
after: after0,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function expandZonedMarker(dateInfo, calendarSystem) {
|
|
let a = calendarSystem.markerToArray(dateInfo.marker);
|
|
return {
|
|
marker: dateInfo.marker,
|
|
timeZoneOffset: dateInfo.timeZoneOffset,
|
|
array: a,
|
|
year: a[0],
|
|
month: a[1],
|
|
day: a[2],
|
|
hour: a[3],
|
|
minute: a[4],
|
|
second: a[5],
|
|
millisecond: a[6],
|
|
};
|
|
}
|
|
|
|
function createVerboseFormattingArg(start, end, context, betterDefaultSeparator) {
|
|
let startInfo = expandZonedMarker(start, context.calendarSystem);
|
|
let endInfo = end ? expandZonedMarker(end, context.calendarSystem) : null;
|
|
return {
|
|
date: startInfo,
|
|
start: startInfo,
|
|
end: endInfo,
|
|
timeZone: context.timeZone,
|
|
localeCodes: context.locale.codes,
|
|
defaultSeparator: betterDefaultSeparator || context.defaultSeparator,
|
|
};
|
|
}
|
|
|
|
/*
|
|
TODO: fix the terminology of "formatter" vs "formatting func"
|
|
*/
|
|
/*
|
|
At the time of instantiation, this object does not know which cmd-formatting system it will use.
|
|
It receives this at the time of formatting, as a setting.
|
|
*/
|
|
class CmdFormatter {
|
|
constructor(cmdStr) {
|
|
this.cmdStr = cmdStr;
|
|
}
|
|
format(date, context, betterDefaultSeparator) {
|
|
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(date, null, context, betterDefaultSeparator));
|
|
}
|
|
formatRange(start, end, context, betterDefaultSeparator) {
|
|
return context.cmdFormatter(this.cmdStr, createVerboseFormattingArg(start, end, context, betterDefaultSeparator));
|
|
}
|
|
}
|
|
|
|
class FuncFormatter {
|
|
constructor(func) {
|
|
this.func = func;
|
|
}
|
|
format(date, context, betterDefaultSeparator) {
|
|
return this.func(createVerboseFormattingArg(date, null, context, betterDefaultSeparator));
|
|
}
|
|
formatRange(start, end, context, betterDefaultSeparator) {
|
|
return this.func(createVerboseFormattingArg(start, end, context, betterDefaultSeparator));
|
|
}
|
|
}
|
|
|
|
function createFormatter(input) {
|
|
if (typeof input === 'object' && input) { // non-null object
|
|
return new NativeFormatter(input);
|
|
}
|
|
if (typeof input === 'string') {
|
|
return new CmdFormatter(input);
|
|
}
|
|
if (typeof input === 'function') {
|
|
return new FuncFormatter(input);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// base options
|
|
// ------------
|
|
const BASE_OPTION_REFINERS = {
|
|
navLinkDayClick: identity,
|
|
navLinkWeekClick: identity,
|
|
duration: createDuration,
|
|
bootstrapFontAwesome: identity,
|
|
buttonIcons: identity,
|
|
customButtons: identity,
|
|
defaultAllDayEventDuration: createDuration,
|
|
defaultTimedEventDuration: createDuration,
|
|
nextDayThreshold: createDuration,
|
|
scrollTime: createDuration,
|
|
scrollTimeReset: Boolean,
|
|
slotMinTime: createDuration,
|
|
slotMaxTime: createDuration,
|
|
dayPopoverFormat: createFormatter,
|
|
slotDuration: createDuration,
|
|
snapDuration: createDuration,
|
|
headerToolbar: identity,
|
|
footerToolbar: identity,
|
|
defaultRangeSeparator: String,
|
|
titleRangeSeparator: String,
|
|
forceEventDuration: Boolean,
|
|
dayHeaders: Boolean,
|
|
dayHeaderFormat: createFormatter,
|
|
dayHeaderClassNames: identity,
|
|
dayHeaderContent: identity,
|
|
dayHeaderDidMount: identity,
|
|
dayHeaderWillUnmount: identity,
|
|
dayCellClassNames: identity,
|
|
dayCellContent: identity,
|
|
dayCellDidMount: identity,
|
|
dayCellWillUnmount: identity,
|
|
initialView: String,
|
|
aspectRatio: Number,
|
|
weekends: Boolean,
|
|
weekNumberCalculation: identity,
|
|
weekNumbers: Boolean,
|
|
weekNumberClassNames: identity,
|
|
weekNumberContent: identity,
|
|
weekNumberDidMount: identity,
|
|
weekNumberWillUnmount: identity,
|
|
editable: Boolean,
|
|
viewClassNames: identity,
|
|
viewDidMount: identity,
|
|
viewWillUnmount: identity,
|
|
nowIndicator: Boolean,
|
|
nowIndicatorSnap: identity,
|
|
nowIndicatorClassNames: identity,
|
|
nowIndicatorContent: identity,
|
|
nowIndicatorDidMount: identity,
|
|
nowIndicatorWillUnmount: identity,
|
|
showNonCurrentDates: Boolean,
|
|
lazyFetching: Boolean,
|
|
startParam: String,
|
|
endParam: String,
|
|
timeZoneParam: String,
|
|
timeZone: String,
|
|
locales: identity,
|
|
locale: identity,
|
|
themeSystem: String,
|
|
dragRevertDuration: Number,
|
|
dragScroll: Boolean,
|
|
allDayMaintainDuration: Boolean,
|
|
unselectAuto: Boolean,
|
|
dropAccept: identity,
|
|
eventOrder: parseFieldSpecs,
|
|
eventOrderStrict: Boolean,
|
|
handleWindowResize: Boolean,
|
|
windowResizeDelay: Number,
|
|
longPressDelay: Number,
|
|
eventDragMinDistance: Number,
|
|
expandRows: Boolean,
|
|
height: identity,
|
|
contentHeight: identity,
|
|
direction: String,
|
|
weekNumberFormat: createFormatter,
|
|
eventResizableFromStart: Boolean,
|
|
displayEventTime: Boolean,
|
|
displayEventEnd: Boolean,
|
|
weekText: String,
|
|
weekTextLong: String,
|
|
progressiveEventRendering: Boolean,
|
|
businessHours: identity,
|
|
initialDate: identity,
|
|
now: identity,
|
|
eventDataTransform: identity,
|
|
stickyHeaderDates: identity,
|
|
stickyFooterScrollbar: identity,
|
|
viewHeight: identity,
|
|
defaultAllDay: Boolean,
|
|
eventSourceFailure: identity,
|
|
eventSourceSuccess: identity,
|
|
eventDisplay: String,
|
|
eventStartEditable: Boolean,
|
|
eventDurationEditable: Boolean,
|
|
eventOverlap: identity,
|
|
eventConstraint: identity,
|
|
eventAllow: identity,
|
|
eventBackgroundColor: String,
|
|
eventBorderColor: String,
|
|
eventTextColor: String,
|
|
eventColor: String,
|
|
eventClassNames: identity,
|
|
eventContent: identity,
|
|
eventDidMount: identity,
|
|
eventWillUnmount: identity,
|
|
selectConstraint: identity,
|
|
selectOverlap: identity,
|
|
selectAllow: identity,
|
|
droppable: Boolean,
|
|
unselectCancel: String,
|
|
slotLabelFormat: identity,
|
|
slotLaneClassNames: identity,
|
|
slotLaneContent: identity,
|
|
slotLaneDidMount: identity,
|
|
slotLaneWillUnmount: identity,
|
|
slotLabelClassNames: identity,
|
|
slotLabelContent: identity,
|
|
slotLabelDidMount: identity,
|
|
slotLabelWillUnmount: identity,
|
|
dayMaxEvents: identity,
|
|
dayMaxEventRows: identity,
|
|
dayMinWidth: Number,
|
|
slotLabelInterval: createDuration,
|
|
allDayText: String,
|
|
allDayClassNames: identity,
|
|
allDayContent: identity,
|
|
allDayDidMount: identity,
|
|
allDayWillUnmount: identity,
|
|
slotMinWidth: Number,
|
|
navLinks: Boolean,
|
|
eventTimeFormat: createFormatter,
|
|
rerenderDelay: Number,
|
|
moreLinkText: identity,
|
|
moreLinkHint: identity,
|
|
selectMinDistance: Number,
|
|
selectable: Boolean,
|
|
selectLongPressDelay: Number,
|
|
eventLongPressDelay: Number,
|
|
selectMirror: Boolean,
|
|
eventMaxStack: Number,
|
|
eventMinHeight: Number,
|
|
eventMinWidth: Number,
|
|
eventShortHeight: Number,
|
|
slotEventOverlap: Boolean,
|
|
plugins: identity,
|
|
firstDay: Number,
|
|
dayCount: Number,
|
|
dateAlignment: String,
|
|
dateIncrement: createDuration,
|
|
hiddenDays: identity,
|
|
fixedWeekCount: Boolean,
|
|
validRange: identity,
|
|
visibleRange: identity,
|
|
titleFormat: identity,
|
|
eventInteractive: Boolean,
|
|
// only used by list-view, but languages define the value, so we need it in base options
|
|
noEventsText: String,
|
|
viewHint: identity,
|
|
navLinkHint: identity,
|
|
closeHint: String,
|
|
timeHint: String,
|
|
eventHint: String,
|
|
moreLinkClick: identity,
|
|
moreLinkClassNames: identity,
|
|
moreLinkContent: identity,
|
|
moreLinkDidMount: identity,
|
|
moreLinkWillUnmount: identity,
|
|
monthStartFormat: createFormatter,
|
|
// for connectors
|
|
// (can't be part of plugin system b/c must be provided at runtime)
|
|
handleCustomRendering: identity,
|
|
customRenderingMetaMap: identity,
|
|
customRenderingReplaces: Boolean,
|
|
};
|
|
// do NOT give a type here. need `typeof BASE_OPTION_DEFAULTS` to give real results.
|
|
// raw values.
|
|
const BASE_OPTION_DEFAULTS = {
|
|
eventDisplay: 'auto',
|
|
defaultRangeSeparator: ' - ',
|
|
titleRangeSeparator: ' \u2013 ',
|
|
defaultTimedEventDuration: '01:00:00',
|
|
defaultAllDayEventDuration: { day: 1 },
|
|
forceEventDuration: false,
|
|
nextDayThreshold: '00:00:00',
|
|
dayHeaders: true,
|
|
initialView: '',
|
|
aspectRatio: 1.35,
|
|
headerToolbar: {
|
|
start: 'title',
|
|
center: '',
|
|
end: 'today prev,next',
|
|
},
|
|
weekends: true,
|
|
weekNumbers: false,
|
|
weekNumberCalculation: 'local',
|
|
editable: false,
|
|
nowIndicator: false,
|
|
scrollTime: '06:00:00',
|
|
scrollTimeReset: true,
|
|
slotMinTime: '00:00:00',
|
|
slotMaxTime: '24:00:00',
|
|
showNonCurrentDates: true,
|
|
lazyFetching: true,
|
|
startParam: 'start',
|
|
endParam: 'end',
|
|
timeZoneParam: 'timeZone',
|
|
timeZone: 'local',
|
|
locales: [],
|
|
locale: '',
|
|
themeSystem: 'standard',
|
|
dragRevertDuration: 500,
|
|
dragScroll: true,
|
|
allDayMaintainDuration: false,
|
|
unselectAuto: true,
|
|
dropAccept: '*',
|
|
eventOrder: 'start,-duration,allDay,title',
|
|
dayPopoverFormat: { month: 'long', day: 'numeric', year: 'numeric' },
|
|
handleWindowResize: true,
|
|
windowResizeDelay: 100,
|
|
longPressDelay: 1000,
|
|
eventDragMinDistance: 5,
|
|
expandRows: false,
|
|
navLinks: false,
|
|
selectable: false,
|
|
eventMinHeight: 15,
|
|
eventMinWidth: 30,
|
|
eventShortHeight: 30,
|
|
monthStartFormat: { month: 'long', day: 'numeric' },
|
|
nowIndicatorSnap: 'auto',
|
|
};
|
|
// calendar listeners
|
|
// ------------------
|
|
const CALENDAR_LISTENER_REFINERS = {
|
|
datesSet: identity,
|
|
eventsSet: identity,
|
|
eventAdd: identity,
|
|
eventChange: identity,
|
|
eventRemove: identity,
|
|
windowResize: identity,
|
|
eventClick: identity,
|
|
eventMouseEnter: identity,
|
|
eventMouseLeave: identity,
|
|
select: identity,
|
|
unselect: identity,
|
|
loading: identity,
|
|
// internal
|
|
_unmount: identity,
|
|
_beforeprint: identity,
|
|
_afterprint: identity,
|
|
_noEventDrop: identity,
|
|
_noEventResize: identity,
|
|
_resize: identity,
|
|
_scrollRequest: identity,
|
|
};
|
|
// calendar-specific options
|
|
// -------------------------
|
|
const CALENDAR_OPTION_REFINERS = {
|
|
buttonText: identity,
|
|
buttonHints: identity,
|
|
views: identity,
|
|
plugins: identity,
|
|
initialEvents: identity,
|
|
events: identity,
|
|
eventSources: identity,
|
|
};
|
|
const COMPLEX_OPTION_COMPARATORS = {
|
|
headerToolbar: isMaybeObjectsEqual,
|
|
footerToolbar: isMaybeObjectsEqual,
|
|
buttonText: isMaybeObjectsEqual,
|
|
buttonHints: isMaybeObjectsEqual,
|
|
buttonIcons: isMaybeObjectsEqual,
|
|
dateIncrement: isMaybeObjectsEqual,
|
|
plugins: isMaybeArraysEqual,
|
|
events: isMaybeArraysEqual,
|
|
eventSources: isMaybeArraysEqual,
|
|
['resources']: isMaybeArraysEqual,
|
|
};
|
|
function isMaybeObjectsEqual(a, b) {
|
|
if (typeof a === 'object' && typeof b === 'object' && a && b) { // both non-null objects
|
|
return isPropsEqual(a, b);
|
|
}
|
|
return a === b;
|
|
}
|
|
function isMaybeArraysEqual(a, b) {
|
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
return isArraysEqual(a, b);
|
|
}
|
|
return a === b;
|
|
}
|
|
// view-specific options
|
|
// ---------------------
|
|
const VIEW_OPTION_REFINERS = {
|
|
type: String,
|
|
component: identity,
|
|
buttonText: String,
|
|
buttonTextKey: String,
|
|
dateProfileGeneratorClass: identity,
|
|
usesMinMaxTime: Boolean,
|
|
classNames: identity,
|
|
content: identity,
|
|
didMount: identity,
|
|
willUnmount: identity,
|
|
};
|
|
// util funcs
|
|
// ----------------------------------------------------------------------------------------------------
|
|
function mergeRawOptions(optionSets) {
|
|
return mergeProps(optionSets, COMPLEX_OPTION_COMPARATORS);
|
|
}
|
|
function refineProps(input, refiners) {
|
|
let refined = {};
|
|
let extra = {};
|
|
for (let propName in refiners) {
|
|
if (propName in input) {
|
|
refined[propName] = refiners[propName](input[propName]);
|
|
}
|
|
}
|
|
for (let propName in input) {
|
|
if (!(propName in refiners)) {
|
|
extra[propName] = input[propName];
|
|
}
|
|
}
|
|
return { refined, extra };
|
|
}
|
|
function identity(raw) {
|
|
return raw;
|
|
}
|
|
|
|
const { hasOwnProperty: internal_common_hasOwnProperty } = Object.prototype;
|
|
// Merges an array of objects into a single object.
|
|
// The second argument allows for an array of property names who's object values will be merged together.
|
|
function mergeProps(propObjs, complexPropsMap) {
|
|
let dest = {};
|
|
if (complexPropsMap) {
|
|
for (let name in complexPropsMap) {
|
|
if (complexPropsMap[name] === isMaybeObjectsEqual) { // implies that it's object-mergeable
|
|
let complexObjs = [];
|
|
// collect the trailing object values, stopping when a non-object is discovered
|
|
for (let i = propObjs.length - 1; i >= 0; i -= 1) {
|
|
let val = propObjs[i][name];
|
|
if (typeof val === 'object' && val) { // non-null object
|
|
complexObjs.unshift(val);
|
|
}
|
|
else if (val !== undefined) {
|
|
dest[name] = val; // if there were no objects, this value will be used
|
|
break;
|
|
}
|
|
}
|
|
// if the trailing values were objects, use the merged value
|
|
if (complexObjs.length) {
|
|
dest[name] = mergeProps(complexObjs);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// copy values into the destination, going from last to first
|
|
for (let i = propObjs.length - 1; i >= 0; i -= 1) {
|
|
let props = propObjs[i];
|
|
for (let name in props) {
|
|
if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
|
|
dest[name] = props[name];
|
|
}
|
|
}
|
|
}
|
|
return dest;
|
|
}
|
|
function filterHash(hash, func) {
|
|
let filtered = {};
|
|
for (let key in hash) {
|
|
if (func(hash[key], key)) {
|
|
filtered[key] = hash[key];
|
|
}
|
|
}
|
|
return filtered;
|
|
}
|
|
function mapHash(hash, func) {
|
|
let newHash = {};
|
|
for (let key in hash) {
|
|
newHash[key] = func(hash[key], key);
|
|
}
|
|
return newHash;
|
|
}
|
|
function arrayToHash(a) {
|
|
let hash = {};
|
|
for (let item of a) {
|
|
hash[item] = true;
|
|
}
|
|
return hash;
|
|
}
|
|
// TODO: reassess browser support
|
|
// https://caniuse.com/?search=object.values
|
|
function hashValuesToArray(obj) {
|
|
let a = [];
|
|
for (let key in obj) {
|
|
a.push(obj[key]);
|
|
}
|
|
return a;
|
|
}
|
|
function isPropsEqual(obj0, obj1) {
|
|
if (obj0 === obj1) {
|
|
return true;
|
|
}
|
|
for (let key in obj0) {
|
|
if (internal_common_hasOwnProperty.call(obj0, key)) {
|
|
if (!(key in obj1)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
for (let key in obj1) {
|
|
if (internal_common_hasOwnProperty.call(obj1, key)) {
|
|
if (obj0[key] !== obj1[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
const HANDLER_RE = /^on[A-Z]/;
|
|
function isNonHandlerPropsEqual(obj0, obj1) {
|
|
const keys = getUnequalProps(obj0, obj1);
|
|
for (let key of keys) {
|
|
if (!HANDLER_RE.test(key)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function getUnequalProps(obj0, obj1) {
|
|
let keys = [];
|
|
for (let key in obj0) {
|
|
if (internal_common_hasOwnProperty.call(obj0, key)) {
|
|
if (!(key in obj1)) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
}
|
|
for (let key in obj1) {
|
|
if (internal_common_hasOwnProperty.call(obj1, key)) {
|
|
if (obj0[key] !== obj1[key]) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
function compareObjs(oldProps, newProps, equalityFuncs = {}) {
|
|
if (oldProps === newProps) {
|
|
return true;
|
|
}
|
|
// if (debug) {
|
|
// for (let key in newProps) {
|
|
// if (key in oldProps && isObjValsEqual(oldProps[key], newProps[key], equalityFuncs[key])) {
|
|
// // equal
|
|
// } else {
|
|
// if (debug) {
|
|
// console.log('prop difference', key, oldProps[key], newProps[key])
|
|
// }
|
|
// }
|
|
// }
|
|
// // check for props that were omitted in the new
|
|
// for (let key in oldProps) {
|
|
// if (!(key in newProps)) {
|
|
// if (debug) {
|
|
// console.log('prop absent', key)
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
for (let key in newProps) {
|
|
if (key in oldProps && isObjValsEqual(oldProps[key], newProps[key], equalityFuncs[key])) ;
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
// check for props that were omitted in the new
|
|
for (let key in oldProps) {
|
|
if (!(key in newProps)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
/*
|
|
assumed "true" equality for handler names like "onReceiveSomething"
|
|
*/
|
|
function isObjValsEqual(val0, val1, comparator) {
|
|
if (val0 === val1 || comparator === true) {
|
|
return true;
|
|
}
|
|
if (comparator) {
|
|
return comparator(val0, val1);
|
|
}
|
|
return false;
|
|
}
|
|
function collectFromHash(hash, startIndex = 0, endIndex, step = 1) {
|
|
let res = [];
|
|
if (endIndex == null) {
|
|
endIndex = Object.keys(hash).length;
|
|
}
|
|
for (let i = startIndex; i < endIndex; i += step) {
|
|
let val = hash[i];
|
|
if (val !== undefined) { // will disregard undefined for sparse arrays
|
|
res.push(val);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
let calendarSystemClassMap = {};
|
|
function registerCalendarSystem(name, theClass) {
|
|
calendarSystemClassMap[name] = theClass;
|
|
}
|
|
function createCalendarSystem(name) {
|
|
return new calendarSystemClassMap[name]();
|
|
}
|
|
class GregorianCalendarSystem {
|
|
getMarkerYear(d) {
|
|
return d.getUTCFullYear();
|
|
}
|
|
getMarkerMonth(d) {
|
|
return d.getUTCMonth();
|
|
}
|
|
getMarkerDay(d) {
|
|
return d.getUTCDate();
|
|
}
|
|
arrayToMarker(arr) {
|
|
return arrayToUtcDate(arr);
|
|
}
|
|
markerToArray(marker) {
|
|
return dateToUtcArray(marker);
|
|
}
|
|
}
|
|
registerCalendarSystem('gregory', GregorianCalendarSystem);
|
|
|
|
const ISO_RE = /^\s*(\d{4})(-?(\d{2})(-?(\d{2})([T ](\d{2}):?(\d{2})(:?(\d{2})(\.(\d+))?)?(Z|(([-+])(\d{2})(:?(\d{2}))?))?)?)?)?$/;
|
|
function internal_common_parse(str) {
|
|
let m = ISO_RE.exec(str);
|
|
if (m) {
|
|
let marker = new Date(Date.UTC(Number(m[1]), m[3] ? Number(m[3]) - 1 : 0, Number(m[5] || 1), Number(m[7] || 0), Number(m[8] || 0), Number(m[10] || 0), m[12] ? Number(`0.${m[12]}`) * 1000 : 0));
|
|
if (isValidDate(marker)) {
|
|
let timeZoneOffset = null;
|
|
if (m[13]) {
|
|
timeZoneOffset = (m[15] === '-' ? -1 : 1) * (Number(m[16] || 0) * 60 +
|
|
Number(m[18] || 0));
|
|
}
|
|
return {
|
|
marker,
|
|
isTimeUnspecified: !m[6],
|
|
timeZoneOffset,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class DateEnv {
|
|
constructor(settings) {
|
|
let timeZone = this.timeZone = settings.timeZone;
|
|
let isNamedTimeZone = timeZone !== 'local' && timeZone !== 'UTC';
|
|
if (settings.namedTimeZoneImpl && isNamedTimeZone) {
|
|
this.namedTimeZoneImpl = new settings.namedTimeZoneImpl(timeZone);
|
|
}
|
|
this.canComputeOffset = Boolean(!isNamedTimeZone || this.namedTimeZoneImpl);
|
|
this.calendarSystem = createCalendarSystem(settings.calendarSystem);
|
|
this.locale = settings.locale;
|
|
this.weekDow = settings.locale.week.dow;
|
|
this.weekDoy = settings.locale.week.doy;
|
|
if (settings.weekNumberCalculation === 'ISO') {
|
|
this.weekDow = 1;
|
|
this.weekDoy = 4;
|
|
}
|
|
if (typeof settings.firstDay === 'number') {
|
|
this.weekDow = settings.firstDay;
|
|
}
|
|
if (typeof settings.weekNumberCalculation === 'function') {
|
|
this.weekNumberFunc = settings.weekNumberCalculation;
|
|
}
|
|
this.weekText = settings.weekText != null ? settings.weekText : settings.locale.options.weekText;
|
|
this.weekTextLong = (settings.weekTextLong != null ? settings.weekTextLong : settings.locale.options.weekTextLong) || this.weekText;
|
|
this.cmdFormatter = settings.cmdFormatter;
|
|
this.defaultSeparator = settings.defaultSeparator;
|
|
}
|
|
// Creating / Parsing
|
|
createMarker(input) {
|
|
let meta = this.createMarkerMeta(input);
|
|
if (meta === null) {
|
|
return null;
|
|
}
|
|
return meta.marker;
|
|
}
|
|
createNowMarker() {
|
|
if (this.canComputeOffset) {
|
|
return this.timestampToMarker(new Date().valueOf());
|
|
}
|
|
// if we can't compute the current date val for a timezone,
|
|
// better to give the current local date vals than UTC
|
|
return arrayToUtcDate(dateToLocalArray(new Date()));
|
|
}
|
|
createMarkerMeta(input) {
|
|
if (typeof input === 'string') {
|
|
return this.parse(input);
|
|
}
|
|
let marker = null;
|
|
if (typeof input === 'number') {
|
|
marker = this.timestampToMarker(input);
|
|
}
|
|
else if (input instanceof Date) {
|
|
input = input.valueOf();
|
|
if (!isNaN(input)) {
|
|
marker = this.timestampToMarker(input);
|
|
}
|
|
}
|
|
else if (Array.isArray(input)) {
|
|
marker = arrayToUtcDate(input);
|
|
}
|
|
if (marker === null || !isValidDate(marker)) {
|
|
return null;
|
|
}
|
|
return { marker, isTimeUnspecified: false, forcedTzo: null };
|
|
}
|
|
parse(s) {
|
|
let parts = internal_common_parse(s);
|
|
if (parts === null) {
|
|
return null;
|
|
}
|
|
let { marker } = parts;
|
|
let forcedTzo = null;
|
|
if (parts.timeZoneOffset !== null) {
|
|
if (this.canComputeOffset) {
|
|
marker = this.timestampToMarker(marker.valueOf() - parts.timeZoneOffset * 60 * 1000);
|
|
}
|
|
else {
|
|
forcedTzo = parts.timeZoneOffset;
|
|
}
|
|
}
|
|
return { marker, isTimeUnspecified: parts.isTimeUnspecified, forcedTzo };
|
|
}
|
|
// Accessors
|
|
getYear(marker) {
|
|
return this.calendarSystem.getMarkerYear(marker);
|
|
}
|
|
getMonth(marker) {
|
|
return this.calendarSystem.getMarkerMonth(marker);
|
|
}
|
|
getDay(marker) {
|
|
return this.calendarSystem.getMarkerDay(marker);
|
|
}
|
|
// Adding / Subtracting
|
|
add(marker, dur) {
|
|
let a = this.calendarSystem.markerToArray(marker);
|
|
a[0] += dur.years;
|
|
a[1] += dur.months;
|
|
a[2] += dur.days;
|
|
a[6] += dur.milliseconds;
|
|
return this.calendarSystem.arrayToMarker(a);
|
|
}
|
|
subtract(marker, dur) {
|
|
let a = this.calendarSystem.markerToArray(marker);
|
|
a[0] -= dur.years;
|
|
a[1] -= dur.months;
|
|
a[2] -= dur.days;
|
|
a[6] -= dur.milliseconds;
|
|
return this.calendarSystem.arrayToMarker(a);
|
|
}
|
|
addYears(marker, n) {
|
|
let a = this.calendarSystem.markerToArray(marker);
|
|
a[0] += n;
|
|
return this.calendarSystem.arrayToMarker(a);
|
|
}
|
|
addMonths(marker, n) {
|
|
let a = this.calendarSystem.markerToArray(marker);
|
|
a[1] += n;
|
|
return this.calendarSystem.arrayToMarker(a);
|
|
}
|
|
// Diffing Whole Units
|
|
diffWholeYears(m0, m1) {
|
|
let { calendarSystem } = this;
|
|
if (timeAsMs(m0) === timeAsMs(m1) &&
|
|
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1) &&
|
|
calendarSystem.getMarkerMonth(m0) === calendarSystem.getMarkerMonth(m1)) {
|
|
return calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0);
|
|
}
|
|
return null;
|
|
}
|
|
diffWholeMonths(m0, m1) {
|
|
let { calendarSystem } = this;
|
|
if (timeAsMs(m0) === timeAsMs(m1) &&
|
|
calendarSystem.getMarkerDay(m0) === calendarSystem.getMarkerDay(m1)) {
|
|
return (calendarSystem.getMarkerMonth(m1) - calendarSystem.getMarkerMonth(m0)) +
|
|
(calendarSystem.getMarkerYear(m1) - calendarSystem.getMarkerYear(m0)) * 12;
|
|
}
|
|
return null;
|
|
}
|
|
// Range / Duration
|
|
greatestWholeUnit(m0, m1) {
|
|
let n = this.diffWholeYears(m0, m1);
|
|
if (n !== null) {
|
|
return { unit: 'year', value: n };
|
|
}
|
|
n = this.diffWholeMonths(m0, m1);
|
|
if (n !== null) {
|
|
return { unit: 'month', value: n };
|
|
}
|
|
n = diffWholeWeeks(m0, m1);
|
|
if (n !== null) {
|
|
return { unit: 'week', value: n };
|
|
}
|
|
n = diffWholeDays(m0, m1);
|
|
if (n !== null) {
|
|
return { unit: 'day', value: n };
|
|
}
|
|
n = diffHours(m0, m1);
|
|
if (isInt(n)) {
|
|
return { unit: 'hour', value: n };
|
|
}
|
|
n = diffMinutes(m0, m1);
|
|
if (isInt(n)) {
|
|
return { unit: 'minute', value: n };
|
|
}
|
|
n = diffSeconds(m0, m1);
|
|
if (isInt(n)) {
|
|
return { unit: 'second', value: n };
|
|
}
|
|
return { unit: 'millisecond', value: m1.valueOf() - m0.valueOf() };
|
|
}
|
|
countDurationsBetween(m0, m1, d) {
|
|
// TODO: can use greatestWholeUnit
|
|
let diff;
|
|
if (d.years) {
|
|
diff = this.diffWholeYears(m0, m1);
|
|
if (diff !== null) {
|
|
return diff / asRoughYears(d);
|
|
}
|
|
}
|
|
if (d.months) {
|
|
diff = this.diffWholeMonths(m0, m1);
|
|
if (diff !== null) {
|
|
return diff / asRoughMonths(d);
|
|
}
|
|
}
|
|
if (d.days) {
|
|
diff = diffWholeDays(m0, m1);
|
|
if (diff !== null) {
|
|
return diff / asRoughDays(d);
|
|
}
|
|
}
|
|
return (m1.valueOf() - m0.valueOf()) / asRoughMs(d);
|
|
}
|
|
// Start-Of
|
|
// these DON'T return zoned-dates. only UTC start-of dates
|
|
startOf(m, unit) {
|
|
if (unit === 'year') {
|
|
return this.startOfYear(m);
|
|
}
|
|
if (unit === 'month') {
|
|
return this.startOfMonth(m);
|
|
}
|
|
if (unit === 'week') {
|
|
return this.startOfWeek(m);
|
|
}
|
|
if (unit === 'day') {
|
|
return startOfDay(m);
|
|
}
|
|
if (unit === 'hour') {
|
|
return startOfHour(m);
|
|
}
|
|
if (unit === 'minute') {
|
|
return startOfMinute(m);
|
|
}
|
|
if (unit === 'second') {
|
|
return startOfSecond(m);
|
|
}
|
|
return null;
|
|
}
|
|
startOfYear(m) {
|
|
return this.calendarSystem.arrayToMarker([
|
|
this.calendarSystem.getMarkerYear(m),
|
|
]);
|
|
}
|
|
startOfMonth(m) {
|
|
return this.calendarSystem.arrayToMarker([
|
|
this.calendarSystem.getMarkerYear(m),
|
|
this.calendarSystem.getMarkerMonth(m),
|
|
]);
|
|
}
|
|
startOfWeek(m) {
|
|
return this.calendarSystem.arrayToMarker([
|
|
this.calendarSystem.getMarkerYear(m),
|
|
this.calendarSystem.getMarkerMonth(m),
|
|
m.getUTCDate() - ((m.getUTCDay() - this.weekDow + 7) % 7),
|
|
]);
|
|
}
|
|
// Week Number
|
|
computeWeekNumber(marker) {
|
|
if (this.weekNumberFunc) {
|
|
return this.weekNumberFunc(this.toDate(marker));
|
|
}
|
|
return weekOfYear(marker, this.weekDow, this.weekDoy);
|
|
}
|
|
// TODO: choke on timeZoneName: long
|
|
format(marker, formatter, dateOptions = {}) {
|
|
return formatter.format({
|
|
marker,
|
|
timeZoneOffset: dateOptions.forcedTzo != null ?
|
|
dateOptions.forcedTzo :
|
|
this.offsetForMarker(marker),
|
|
}, this);
|
|
}
|
|
formatRange(start, end, formatter, dateOptions = {}) {
|
|
if (dateOptions.isEndExclusive) {
|
|
end = addMs(end, -1);
|
|
}
|
|
return formatter.formatRange({
|
|
marker: start,
|
|
timeZoneOffset: dateOptions.forcedStartTzo != null ?
|
|
dateOptions.forcedStartTzo :
|
|
this.offsetForMarker(start),
|
|
}, {
|
|
marker: end,
|
|
timeZoneOffset: dateOptions.forcedEndTzo != null ?
|
|
dateOptions.forcedEndTzo :
|
|
this.offsetForMarker(end),
|
|
}, this, dateOptions.defaultSeparator);
|
|
}
|
|
/*
|
|
DUMB: the omitTime arg is dumb. if we omit the time, we want to omit the timezone offset. and if we do that,
|
|
might as well use buildIsoString or some other util directly
|
|
*/
|
|
formatIso(marker, extraOptions = {}) {
|
|
let timeZoneOffset = null;
|
|
if (!extraOptions.omitTimeZoneOffset) {
|
|
if (extraOptions.forcedTzo != null) {
|
|
timeZoneOffset = extraOptions.forcedTzo;
|
|
}
|
|
else {
|
|
timeZoneOffset = this.offsetForMarker(marker);
|
|
}
|
|
}
|
|
return buildIsoString(marker, timeZoneOffset, extraOptions.omitTime);
|
|
}
|
|
// TimeZone
|
|
timestampToMarker(ms) {
|
|
if (this.timeZone === 'local') {
|
|
return arrayToUtcDate(dateToLocalArray(new Date(ms)));
|
|
}
|
|
if (this.timeZone === 'UTC' || !this.namedTimeZoneImpl) {
|
|
return new Date(ms);
|
|
}
|
|
return arrayToUtcDate(this.namedTimeZoneImpl.timestampToArray(ms));
|
|
}
|
|
offsetForMarker(m) {
|
|
if (this.timeZone === 'local') {
|
|
return -arrayToLocalDate(dateToUtcArray(m)).getTimezoneOffset(); // convert "inverse" offset to "normal" offset
|
|
}
|
|
if (this.timeZone === 'UTC') {
|
|
return 0;
|
|
}
|
|
if (this.namedTimeZoneImpl) {
|
|
return this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m));
|
|
}
|
|
return null;
|
|
}
|
|
// Conversion
|
|
toDate(m, forcedTzo) {
|
|
if (this.timeZone === 'local') {
|
|
return arrayToLocalDate(dateToUtcArray(m));
|
|
}
|
|
if (this.timeZone === 'UTC') {
|
|
return new Date(m.valueOf()); // make sure it's a copy
|
|
}
|
|
if (!this.namedTimeZoneImpl) {
|
|
return new Date(m.valueOf() - (forcedTzo || 0));
|
|
}
|
|
return new Date(m.valueOf() -
|
|
this.namedTimeZoneImpl.offsetForArray(dateToUtcArray(m)) * 1000 * 60);
|
|
}
|
|
}
|
|
|
|
class internal_common_Theme {
|
|
constructor(calendarOptions) {
|
|
if (this.iconOverrideOption) {
|
|
this.setIconOverride(calendarOptions[this.iconOverrideOption]);
|
|
}
|
|
}
|
|
setIconOverride(iconOverrideHash) {
|
|
let iconClassesCopy;
|
|
let buttonName;
|
|
if (typeof iconOverrideHash === 'object' && iconOverrideHash) { // non-null object
|
|
iconClassesCopy = Object.assign({}, this.iconClasses);
|
|
for (buttonName in iconOverrideHash) {
|
|
iconClassesCopy[buttonName] = this.applyIconOverridePrefix(iconOverrideHash[buttonName]);
|
|
}
|
|
this.iconClasses = iconClassesCopy;
|
|
}
|
|
else if (iconOverrideHash === false) {
|
|
this.iconClasses = {};
|
|
}
|
|
}
|
|
applyIconOverridePrefix(className) {
|
|
let prefix = this.iconOverridePrefix;
|
|
if (prefix && className.indexOf(prefix) !== 0) { // if not already present
|
|
className = prefix + className;
|
|
}
|
|
return className;
|
|
}
|
|
getClass(key) {
|
|
return this.classes[key] || '';
|
|
}
|
|
getIconClass(buttonName, isRtl) {
|
|
let className;
|
|
if (isRtl && this.rtlIconClasses) {
|
|
className = this.rtlIconClasses[buttonName] || this.iconClasses[buttonName];
|
|
}
|
|
else {
|
|
className = this.iconClasses[buttonName];
|
|
}
|
|
if (className) {
|
|
return `${this.baseIconClass} ${className}`;
|
|
}
|
|
return '';
|
|
}
|
|
getCustomButtonIconClass(customButtonProps) {
|
|
let className;
|
|
if (this.iconOverrideCustomButtonOption) {
|
|
className = customButtonProps[this.iconOverrideCustomButtonOption];
|
|
if (className) {
|
|
return `${this.baseIconClass} ${this.applyIconOverridePrefix(className)}`;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
}
|
|
internal_common_Theme.prototype.classes = {};
|
|
internal_common_Theme.prototype.iconClasses = {};
|
|
internal_common_Theme.prototype.baseIconClass = '';
|
|
internal_common_Theme.prototype.iconOverridePrefix = '';
|
|
|
|
/*
|
|
NOTE: this can be a public API, especially createElement for hooks.
|
|
See examples/typescript-scheduler/src/index.ts
|
|
*/
|
|
function flushSync(runBeforeFlush) {
|
|
runBeforeFlush();
|
|
let oldDebounceRendering = l.debounceRendering; // orig
|
|
let callbackQ = [];
|
|
function execCallbackSync(callback) {
|
|
callbackQ.push(callback);
|
|
}
|
|
l.debounceRendering = execCallbackSync;
|
|
D(y(FakeComponent, {}), document.createElement('div'));
|
|
while (callbackQ.length) {
|
|
callbackQ.shift()();
|
|
}
|
|
l.debounceRendering = oldDebounceRendering;
|
|
}
|
|
class FakeComponent extends x {
|
|
render() { return y('div', {}); }
|
|
componentDidMount() { this.setState({}); }
|
|
}
|
|
// TODO: use preact/compat instead?
|
|
function internal_common_createContext(defaultValue) {
|
|
let ContextType = G(defaultValue);
|
|
let origProvider = ContextType.Provider;
|
|
ContextType.Provider = function () {
|
|
let isNew = !this.getChildContext;
|
|
let children = origProvider.apply(this, arguments); // eslint-disable-line prefer-rest-params
|
|
if (isNew) {
|
|
let subs = [];
|
|
this.shouldComponentUpdate = (_props) => {
|
|
if (this.props.value !== _props.value) {
|
|
subs.forEach((c) => {
|
|
c.context = _props.value;
|
|
c.forceUpdate();
|
|
});
|
|
}
|
|
};
|
|
this.sub = (c) => {
|
|
subs.push(c);
|
|
let old = c.componentWillUnmount;
|
|
c.componentWillUnmount = () => {
|
|
subs.splice(subs.indexOf(c), 1);
|
|
old && old.call(c);
|
|
};
|
|
};
|
|
}
|
|
return children;
|
|
};
|
|
return ContextType;
|
|
}
|
|
|
|
class ScrollResponder {
|
|
constructor(execFunc, emitter, scrollTime, scrollTimeReset) {
|
|
this.execFunc = execFunc;
|
|
this.emitter = emitter;
|
|
this.scrollTime = scrollTime;
|
|
this.scrollTimeReset = scrollTimeReset;
|
|
this.handleScrollRequest = (request) => {
|
|
this.queuedRequest = Object.assign({}, this.queuedRequest || {}, request);
|
|
this.drain();
|
|
};
|
|
emitter.on('_scrollRequest', this.handleScrollRequest);
|
|
this.fireInitialScroll();
|
|
}
|
|
detach() {
|
|
this.emitter.off('_scrollRequest', this.handleScrollRequest);
|
|
}
|
|
update(isDatesNew) {
|
|
if (isDatesNew && this.scrollTimeReset) {
|
|
this.fireInitialScroll(); // will drain
|
|
}
|
|
else {
|
|
this.drain();
|
|
}
|
|
}
|
|
fireInitialScroll() {
|
|
this.handleScrollRequest({
|
|
time: this.scrollTime,
|
|
});
|
|
}
|
|
drain() {
|
|
if (this.queuedRequest && this.execFunc(this.queuedRequest)) {
|
|
this.queuedRequest = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ViewContextType = internal_common_createContext({}); // for Components
|
|
function buildViewContext(viewSpec, viewApi, viewOptions, dateProfileGenerator, dateEnv, nowManager, theme, pluginHooks, dispatch, getCurrentData, emitter, calendarApi, registerInteractiveComponent, unregisterInteractiveComponent) {
|
|
return {
|
|
dateEnv,
|
|
nowManager,
|
|
options: viewOptions,
|
|
pluginHooks,
|
|
emitter,
|
|
dispatch,
|
|
getCurrentData,
|
|
calendarApi,
|
|
viewSpec,
|
|
viewApi,
|
|
dateProfileGenerator,
|
|
theme,
|
|
isRtl: viewOptions.direction === 'rtl',
|
|
addResizeHandler(handler) {
|
|
emitter.on('_resize', handler);
|
|
},
|
|
removeResizeHandler(handler) {
|
|
emitter.off('_resize', handler);
|
|
},
|
|
createScrollResponder(execFunc) {
|
|
return new ScrollResponder(execFunc, emitter, createDuration(viewOptions.scrollTime), viewOptions.scrollTimeReset);
|
|
},
|
|
registerInteractiveComponent,
|
|
unregisterInteractiveComponent,
|
|
};
|
|
}
|
|
|
|
/* eslint max-classes-per-file: off */
|
|
class PureComponent extends x {
|
|
// debug: boolean
|
|
shouldComponentUpdate(nextProps, nextState) {
|
|
const shouldUpdate = !compareObjs(this.props, nextProps, this.propEquality /*, this.debug */) ||
|
|
!compareObjs(this.state, nextState, this.stateEquality /*, this.debug */);
|
|
// if (this.debug && shouldUpdate) {
|
|
// console.log('shouldUpdate!')
|
|
// }
|
|
return shouldUpdate;
|
|
}
|
|
// HACK for freakin' React StrictMode
|
|
safeSetState(newState) {
|
|
if (!compareObjs(this.state, Object.assign(Object.assign({}, this.state), newState), this.stateEquality)) {
|
|
this.setState(newState);
|
|
}
|
|
}
|
|
}
|
|
PureComponent.addPropsEquality = addPropsEquality;
|
|
PureComponent.addStateEquality = addStateEquality;
|
|
PureComponent.contextType = ViewContextType;
|
|
PureComponent.prototype.propEquality = {};
|
|
PureComponent.prototype.stateEquality = {};
|
|
class BaseComponent extends PureComponent {
|
|
}
|
|
BaseComponent.contextType = ViewContextType;
|
|
function addPropsEquality(propEquality) {
|
|
let hash = Object.create(this.prototype.propEquality);
|
|
Object.assign(hash, propEquality);
|
|
this.prototype.propEquality = hash;
|
|
}
|
|
function addStateEquality(stateEquality) {
|
|
let hash = Object.create(this.prototype.stateEquality);
|
|
Object.assign(hash, stateEquality);
|
|
this.prototype.stateEquality = hash;
|
|
}
|
|
// use other one
|
|
function setRef(ref, current) {
|
|
if (typeof ref === 'function') {
|
|
ref(current);
|
|
}
|
|
else if (ref) {
|
|
// see https://github.com/facebook/react/issues/13029
|
|
ref.current = current;
|
|
}
|
|
}
|
|
|
|
class ContentInjector extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.id = guid();
|
|
this.queuedDomNodes = [];
|
|
this.currentDomNodes = [];
|
|
this.handleEl = (el) => {
|
|
const { options } = this.context;
|
|
const { generatorName } = this.props;
|
|
if (!options.customRenderingReplaces || !hasCustomRenderingHandler(generatorName, options)) {
|
|
this.updateElRef(el);
|
|
}
|
|
};
|
|
this.updateElRef = (el) => {
|
|
if (this.props.elRef) {
|
|
setRef(this.props.elRef, el);
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
const { props, context } = this;
|
|
const { options } = context;
|
|
const { customGenerator, defaultGenerator, renderProps } = props;
|
|
const attrs = buildElAttrs(props, [], this.handleEl);
|
|
let useDefault = false;
|
|
let innerContent;
|
|
let queuedDomNodes = [];
|
|
let currentGeneratorMeta;
|
|
if (customGenerator != null) {
|
|
const customGeneratorRes = typeof customGenerator === 'function' ?
|
|
customGenerator(renderProps, y) :
|
|
customGenerator;
|
|
if (customGeneratorRes === true) {
|
|
useDefault = true;
|
|
}
|
|
else {
|
|
const isObject = customGeneratorRes && typeof customGeneratorRes === 'object'; // non-null
|
|
if (isObject && ('html' in customGeneratorRes)) {
|
|
attrs.dangerouslySetInnerHTML = { __html: customGeneratorRes.html };
|
|
}
|
|
else if (isObject && ('domNodes' in customGeneratorRes)) {
|
|
queuedDomNodes = Array.prototype.slice.call(customGeneratorRes.domNodes);
|
|
}
|
|
else if (isObject
|
|
? i(customGeneratorRes) // vdom node
|
|
: typeof customGeneratorRes !== 'function' // primitive value (like string or number)
|
|
) {
|
|
// use in vdom
|
|
innerContent = customGeneratorRes;
|
|
}
|
|
else {
|
|
// an exotic object for handleCustomRendering
|
|
currentGeneratorMeta = customGeneratorRes;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
useDefault = !hasCustomRenderingHandler(props.generatorName, options);
|
|
}
|
|
if (useDefault && defaultGenerator) {
|
|
innerContent = defaultGenerator(renderProps);
|
|
}
|
|
this.queuedDomNodes = queuedDomNodes;
|
|
this.currentGeneratorMeta = currentGeneratorMeta;
|
|
return y(props.elTag, attrs, innerContent);
|
|
}
|
|
componentDidMount() {
|
|
this.applyQueueudDomNodes();
|
|
this.triggerCustomRendering(true);
|
|
}
|
|
componentDidUpdate() {
|
|
this.applyQueueudDomNodes();
|
|
this.triggerCustomRendering(true);
|
|
}
|
|
componentWillUnmount() {
|
|
this.triggerCustomRendering(false); // TODO: different API for removal?
|
|
}
|
|
triggerCustomRendering(isActive) {
|
|
var _a;
|
|
const { props, context } = this;
|
|
const { handleCustomRendering, customRenderingMetaMap } = context.options;
|
|
if (handleCustomRendering) {
|
|
const generatorMeta = (_a = this.currentGeneratorMeta) !== null && _a !== void 0 ? _a : customRenderingMetaMap === null || customRenderingMetaMap === void 0 ? void 0 : customRenderingMetaMap[props.generatorName];
|
|
if (generatorMeta) {
|
|
handleCustomRendering(Object.assign(Object.assign({ id: this.id, isActive, containerEl: this.base, reportNewContainerEl: this.updateElRef, // front-end framework tells us about new container els
|
|
generatorMeta }, props), { elClasses: (props.elClasses || []).filter(isTruthy) }));
|
|
}
|
|
}
|
|
}
|
|
applyQueueudDomNodes() {
|
|
const { queuedDomNodes, currentDomNodes } = this;
|
|
const el = this.base;
|
|
if (!isArraysEqual(queuedDomNodes, currentDomNodes)) {
|
|
currentDomNodes.forEach(removeElement);
|
|
for (let newNode of queuedDomNodes) {
|
|
el.appendChild(newNode);
|
|
}
|
|
this.currentDomNodes = queuedDomNodes;
|
|
}
|
|
}
|
|
}
|
|
ContentInjector.addPropsEquality({
|
|
elClasses: isArraysEqual,
|
|
elStyle: isPropsEqual,
|
|
elAttrs: isNonHandlerPropsEqual,
|
|
renderProps: isPropsEqual,
|
|
});
|
|
// Util
|
|
/*
|
|
Does UI-framework provide custom way of rendering that does not use Preact VDOM
|
|
AND does the calendar's options define custom rendering?
|
|
AKA. Should we NOT render the default content?
|
|
*/
|
|
function hasCustomRenderingHandler(generatorName, options) {
|
|
var _a;
|
|
return Boolean(options.handleCustomRendering &&
|
|
generatorName &&
|
|
((_a = options.customRenderingMetaMap) === null || _a === void 0 ? void 0 : _a[generatorName]));
|
|
}
|
|
function buildElAttrs(props, extraClassNames, elRef) {
|
|
const attrs = Object.assign(Object.assign({}, props.elAttrs), { ref: elRef });
|
|
if (props.elClasses || extraClassNames) {
|
|
attrs.className = (props.elClasses || [])
|
|
.concat(extraClassNames || [])
|
|
.concat(attrs.className || [])
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
}
|
|
if (props.elStyle) {
|
|
attrs.style = props.elStyle;
|
|
}
|
|
return attrs;
|
|
}
|
|
function isTruthy(val) {
|
|
return Boolean(val);
|
|
}
|
|
|
|
const RenderId = internal_common_createContext(0);
|
|
|
|
class ContentContainer extends x {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.InnerContent = InnerContentInjector.bind(undefined, this);
|
|
this.handleEl = (el) => {
|
|
this.el = el;
|
|
if (this.props.elRef) {
|
|
setRef(this.props.elRef, el);
|
|
if (el && this.didMountMisfire) {
|
|
this.componentDidMount();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
const { props } = this;
|
|
const generatedClassNames = generateClassNames(props.classNameGenerator, props.renderProps);
|
|
if (props.children) {
|
|
const elAttrs = buildElAttrs(props, generatedClassNames, this.handleEl);
|
|
const children = props.children(this.InnerContent, props.renderProps, elAttrs);
|
|
if (props.elTag) {
|
|
return y(props.elTag, elAttrs, children);
|
|
}
|
|
else {
|
|
return children;
|
|
}
|
|
}
|
|
else {
|
|
return y((ContentInjector), Object.assign(Object.assign({}, props), { elRef: this.handleEl, elTag: props.elTag || 'div', elClasses: (props.elClasses || []).concat(generatedClassNames), renderId: this.context }));
|
|
}
|
|
}
|
|
componentDidMount() {
|
|
var _a, _b;
|
|
if (this.el) {
|
|
(_b = (_a = this.props).didMount) === null || _b === void 0 ? void 0 : _b.call(_a, Object.assign(Object.assign({}, this.props.renderProps), { el: this.el }));
|
|
}
|
|
else {
|
|
this.didMountMisfire = true;
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
var _a, _b;
|
|
(_b = (_a = this.props).willUnmount) === null || _b === void 0 ? void 0 : _b.call(_a, Object.assign(Object.assign({}, this.props.renderProps), { el: this.el }));
|
|
}
|
|
}
|
|
ContentContainer.contextType = RenderId;
|
|
function InnerContentInjector(containerComponent, props) {
|
|
const parentProps = containerComponent.props;
|
|
return y((ContentInjector), Object.assign({ renderProps: parentProps.renderProps, generatorName: parentProps.generatorName, customGenerator: parentProps.customGenerator, defaultGenerator: parentProps.defaultGenerator, renderId: containerComponent.context }, props));
|
|
}
|
|
// Utils
|
|
function generateClassNames(classNameGenerator, renderProps) {
|
|
const classNames = typeof classNameGenerator === 'function' ?
|
|
classNameGenerator(renderProps) :
|
|
classNameGenerator || [];
|
|
return typeof classNames === 'string' ? [classNames] : classNames;
|
|
}
|
|
|
|
class ViewContainer extends BaseComponent {
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let renderProps = { view: context.viewApi };
|
|
return (y(ContentContainer, { elRef: props.elRef, elTag: props.elTag || 'div', elAttrs: props.elAttrs, elClasses: [
|
|
...buildViewClassNames(props.viewSpec),
|
|
...(props.elClasses || []),
|
|
], elStyle: props.elStyle, renderProps: renderProps, classNameGenerator: options.viewClassNames, generatorName: undefined, didMount: options.viewDidMount, willUnmount: options.viewWillUnmount }, () => props.children));
|
|
}
|
|
}
|
|
function buildViewClassNames(viewSpec) {
|
|
return [
|
|
`fc-${viewSpec.type}-view`,
|
|
'fc-view',
|
|
];
|
|
}
|
|
|
|
function parseRange(input, dateEnv) {
|
|
let start = null;
|
|
let end = null;
|
|
if (input.start) {
|
|
start = dateEnv.createMarker(input.start);
|
|
}
|
|
if (input.end) {
|
|
end = dateEnv.createMarker(input.end);
|
|
}
|
|
if (!start && !end) {
|
|
return null;
|
|
}
|
|
if (start && end && end < start) {
|
|
return null;
|
|
}
|
|
return { start, end };
|
|
}
|
|
// SIDE-EFFECT: will mutate ranges.
|
|
// Will return a new array result.
|
|
function invertRanges(ranges, constraintRange) {
|
|
let invertedRanges = [];
|
|
let { start } = constraintRange; // the end of the previous range. the start of the new range
|
|
let i;
|
|
let dateRange;
|
|
// ranges need to be in order. required for our date-walking algorithm
|
|
ranges.sort(compareRanges);
|
|
for (i = 0; i < ranges.length; i += 1) {
|
|
dateRange = ranges[i];
|
|
// add the span of time before the event (if there is any)
|
|
if (dateRange.start > start) { // compare millisecond time (skip any ambig logic)
|
|
invertedRanges.push({ start, end: dateRange.start });
|
|
}
|
|
if (dateRange.end > start) {
|
|
start = dateRange.end;
|
|
}
|
|
}
|
|
// add the span of time after the last event (if there is any)
|
|
if (start < constraintRange.end) { // compare millisecond time (skip any ambig logic)
|
|
invertedRanges.push({ start, end: constraintRange.end });
|
|
}
|
|
return invertedRanges;
|
|
}
|
|
function compareRanges(range0, range1) {
|
|
return range0.start.valueOf() - range1.start.valueOf(); // earlier ranges go first
|
|
}
|
|
function intersectRanges(range0, range1) {
|
|
let { start, end } = range0;
|
|
let newRange = null;
|
|
if (range1.start !== null) {
|
|
if (start === null) {
|
|
start = range1.start;
|
|
}
|
|
else {
|
|
start = new Date(Math.max(start.valueOf(), range1.start.valueOf()));
|
|
}
|
|
}
|
|
if (range1.end != null) {
|
|
if (end === null) {
|
|
end = range1.end;
|
|
}
|
|
else {
|
|
end = new Date(Math.min(end.valueOf(), range1.end.valueOf()));
|
|
}
|
|
}
|
|
if (start === null || end === null || start < end) {
|
|
newRange = { start, end };
|
|
}
|
|
return newRange;
|
|
}
|
|
function rangesEqual(range0, range1) {
|
|
return (range0.start === null ? null : range0.start.valueOf()) === (range1.start === null ? null : range1.start.valueOf()) &&
|
|
(range0.end === null ? null : range0.end.valueOf()) === (range1.end === null ? null : range1.end.valueOf());
|
|
}
|
|
function rangesIntersect(range0, range1) {
|
|
return (range0.end === null || range1.start === null || range0.end > range1.start) &&
|
|
(range0.start === null || range1.end === null || range0.start < range1.end);
|
|
}
|
|
function rangeContainsRange(outerRange, innerRange) {
|
|
return (outerRange.start === null || (innerRange.start !== null && innerRange.start >= outerRange.start)) &&
|
|
(outerRange.end === null || (innerRange.end !== null && innerRange.end <= outerRange.end));
|
|
}
|
|
function rangeContainsMarker(range, date) {
|
|
return (range.start === null || date >= range.start) &&
|
|
(range.end === null || date < range.end);
|
|
}
|
|
// If the given date is not within the given range, move it inside.
|
|
// (If it's past the end, make it one millisecond before the end).
|
|
function constrainMarkerToRange(date, range) {
|
|
if (range.start != null && date < range.start) {
|
|
return range.start;
|
|
}
|
|
if (range.end != null && date >= range.end) {
|
|
return new Date(range.end.valueOf() - 1);
|
|
}
|
|
return date;
|
|
}
|
|
|
|
/* Date stuff that doesn't belong in datelib core
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
// given a timed range, computes an all-day range that has the same exact duration,
|
|
// but whose start time is aligned with the start of the day.
|
|
function computeAlignedDayRange(timedRange) {
|
|
let dayCnt = Math.floor(diffDays(timedRange.start, timedRange.end)) || 1;
|
|
let start = startOfDay(timedRange.start);
|
|
let end = addDays(start, dayCnt);
|
|
return { start, end };
|
|
}
|
|
// given a timed range, computes an all-day range based on how for the end date bleeds into the next day
|
|
// TODO: give nextDayThreshold a default arg
|
|
function computeVisibleDayRange(timedRange, nextDayThreshold = createDuration(0)) {
|
|
let startDay = null;
|
|
let endDay = null;
|
|
if (timedRange.end) {
|
|
endDay = startOfDay(timedRange.end);
|
|
let endTimeMS = timedRange.end.valueOf() - endDay.valueOf(); // # of milliseconds into `endDay`
|
|
// If the end time is actually inclusively part of the next day and is equal to or
|
|
// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
|
|
// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
|
|
if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
|
|
endDay = addDays(endDay, 1);
|
|
}
|
|
}
|
|
if (timedRange.start) {
|
|
startDay = startOfDay(timedRange.start); // the beginning of the day the range starts
|
|
// If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
|
|
if (endDay && endDay <= startDay) {
|
|
endDay = addDays(startDay, 1);
|
|
}
|
|
}
|
|
return { start: startDay, end: endDay };
|
|
}
|
|
// spans from one day into another?
|
|
function isMultiDayRange(range) {
|
|
let visibleRange = computeVisibleDayRange(range);
|
|
return diffDays(visibleRange.start, visibleRange.end) > 1;
|
|
}
|
|
function diffDates(date0, date1, dateEnv, largeUnit) {
|
|
if (largeUnit === 'year') {
|
|
return createDuration(dateEnv.diffWholeYears(date0, date1), 'year');
|
|
}
|
|
if (largeUnit === 'month') {
|
|
return createDuration(dateEnv.diffWholeMonths(date0, date1), 'month');
|
|
}
|
|
return diffDayAndTime(date0, date1); // returns a duration
|
|
}
|
|
|
|
class DateProfileGenerator {
|
|
constructor(props) {
|
|
this.props = props;
|
|
this.initHiddenDays();
|
|
}
|
|
/* Date Range Computation
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
// Builds a structure with info about what the dates/ranges will be for the "prev" view.
|
|
buildPrev(currentDateProfile, currentDate, forceToValid) {
|
|
let { dateEnv } = this.props;
|
|
let prevDate = dateEnv.subtract(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
|
|
currentDateProfile.dateIncrement);
|
|
return this.build(prevDate, -1, forceToValid);
|
|
}
|
|
// Builds a structure with info about what the dates/ranges will be for the "next" view.
|
|
buildNext(currentDateProfile, currentDate, forceToValid) {
|
|
let { dateEnv } = this.props;
|
|
let nextDate = dateEnv.add(dateEnv.startOf(currentDate, currentDateProfile.currentRangeUnit), // important for start-of-month
|
|
currentDateProfile.dateIncrement);
|
|
return this.build(nextDate, 1, forceToValid);
|
|
}
|
|
// Builds a structure holding dates/ranges for rendering around the given date.
|
|
// Optional direction param indicates whether the date is being incremented/decremented
|
|
// from its previous value. decremented = -1, incremented = 1 (default).
|
|
build(currentDate, direction, forceToValid = true) {
|
|
let { props } = this;
|
|
let validRange;
|
|
let currentInfo;
|
|
let isRangeAllDay;
|
|
let renderRange;
|
|
let activeRange;
|
|
let isValid;
|
|
validRange = this.buildValidRange();
|
|
validRange = this.trimHiddenDays(validRange);
|
|
if (forceToValid) {
|
|
currentDate = constrainMarkerToRange(currentDate, validRange);
|
|
}
|
|
currentInfo = this.buildCurrentRangeInfo(currentDate, direction);
|
|
isRangeAllDay = /^(year|month|week|day)$/.test(currentInfo.unit);
|
|
renderRange = this.buildRenderRange(this.trimHiddenDays(currentInfo.range), currentInfo.unit, isRangeAllDay);
|
|
renderRange = this.trimHiddenDays(renderRange);
|
|
activeRange = renderRange;
|
|
if (!props.showNonCurrentDates) {
|
|
activeRange = intersectRanges(activeRange, currentInfo.range);
|
|
}
|
|
activeRange = this.adjustActiveRange(activeRange);
|
|
activeRange = intersectRanges(activeRange, validRange); // might return null
|
|
// it's invalid if the originally requested date is not contained,
|
|
// or if the range is completely outside of the valid range.
|
|
isValid = rangesIntersect(currentInfo.range, validRange);
|
|
// HACK: constrain to render-range so `currentDate` is more useful to view rendering
|
|
if (!rangeContainsMarker(renderRange, currentDate)) {
|
|
currentDate = renderRange.start;
|
|
}
|
|
return {
|
|
currentDate,
|
|
// constraint for where prev/next operations can go and where events can be dragged/resized to.
|
|
// an object with optional start and end properties.
|
|
validRange,
|
|
// range the view is formally responsible for.
|
|
// for example, a month view might have 1st-31st, excluding padded dates
|
|
currentRange: currentInfo.range,
|
|
// name of largest unit being displayed, like "month" or "week"
|
|
currentRangeUnit: currentInfo.unit,
|
|
isRangeAllDay,
|
|
// dates that display events and accept drag-n-drop
|
|
// will be `null` if no dates accept events
|
|
activeRange,
|
|
// date range with a rendered skeleton
|
|
// includes not-active days that need some sort of DOM
|
|
renderRange,
|
|
// Duration object that denotes the first visible time of any given day
|
|
slotMinTime: props.slotMinTime,
|
|
// Duration object that denotes the exclusive visible end time of any given day
|
|
slotMaxTime: props.slotMaxTime,
|
|
isValid,
|
|
// how far the current date will move for a prev/next operation
|
|
dateIncrement: this.buildDateIncrement(currentInfo.duration),
|
|
// pass a fallback (might be null) ^
|
|
};
|
|
}
|
|
// Builds an object with optional start/end properties.
|
|
// Indicates the minimum/maximum dates to display.
|
|
// not responsible for trimming hidden days.
|
|
buildValidRange() {
|
|
let input = this.props.validRangeInput;
|
|
let simpleInput = typeof input === 'function'
|
|
? input.call(this.props.calendarApi, this.props.dateEnv.toDate(this.props.nowManager.getDateMarker()))
|
|
: input;
|
|
return this.refineRange(simpleInput) ||
|
|
{ start: null, end: null }; // completely open-ended
|
|
}
|
|
// Builds a structure with info about the "current" range, the range that is
|
|
// highlighted as being the current month for example.
|
|
// See build() for a description of `direction`.
|
|
// Guaranteed to have `range` and `unit` properties. `duration` is optional.
|
|
buildCurrentRangeInfo(date, direction) {
|
|
let { props } = this;
|
|
let duration = null;
|
|
let unit = null;
|
|
let range = null;
|
|
let dayCount;
|
|
if (props.duration) {
|
|
duration = props.duration;
|
|
unit = props.durationUnit;
|
|
range = this.buildRangeFromDuration(date, direction, duration, unit);
|
|
}
|
|
else if ((dayCount = this.props.dayCount)) {
|
|
unit = 'day';
|
|
range = this.buildRangeFromDayCount(date, direction, dayCount);
|
|
}
|
|
else if ((range = this.buildCustomVisibleRange(date))) {
|
|
unit = props.dateEnv.greatestWholeUnit(range.start, range.end).unit;
|
|
}
|
|
else {
|
|
duration = this.getFallbackDuration();
|
|
unit = greatestDurationDenominator(duration).unit;
|
|
range = this.buildRangeFromDuration(date, direction, duration, unit);
|
|
}
|
|
return { duration, unit, range };
|
|
}
|
|
getFallbackDuration() {
|
|
return createDuration({ day: 1 });
|
|
}
|
|
// Returns a new activeRange to have time values (un-ambiguate)
|
|
// slotMinTime or slotMaxTime causes the range to expand.
|
|
adjustActiveRange(range) {
|
|
let { dateEnv, usesMinMaxTime, slotMinTime, slotMaxTime } = this.props;
|
|
let { start, end } = range;
|
|
if (usesMinMaxTime) {
|
|
// expand active range if slotMinTime is negative (why not when positive?)
|
|
if (asRoughDays(slotMinTime) < 0) {
|
|
start = startOfDay(start); // necessary?
|
|
start = dateEnv.add(start, slotMinTime);
|
|
}
|
|
// expand active range if slotMaxTime is beyond one day (why not when negative?)
|
|
if (asRoughDays(slotMaxTime) > 1) {
|
|
end = startOfDay(end); // necessary?
|
|
end = addDays(end, -1);
|
|
end = dateEnv.add(end, slotMaxTime);
|
|
}
|
|
}
|
|
return { start, end };
|
|
}
|
|
// Builds the "current" range when it is specified as an explicit duration.
|
|
// `unit` is the already-computed greatestDurationDenominator unit of duration.
|
|
buildRangeFromDuration(date, direction, duration, unit) {
|
|
let { dateEnv, dateAlignment } = this.props;
|
|
let start;
|
|
let end;
|
|
let res;
|
|
// compute what the alignment should be
|
|
if (!dateAlignment) {
|
|
let { dateIncrement } = this.props;
|
|
if (dateIncrement) {
|
|
// use the smaller of the two units
|
|
if (asRoughMs(dateIncrement) < asRoughMs(duration)) {
|
|
dateAlignment = greatestDurationDenominator(dateIncrement).unit;
|
|
}
|
|
else {
|
|
dateAlignment = unit;
|
|
}
|
|
}
|
|
else {
|
|
dateAlignment = unit;
|
|
}
|
|
}
|
|
// if the view displays a single day or smaller
|
|
if (asRoughDays(duration) <= 1) {
|
|
if (this.isHiddenDay(start)) {
|
|
start = this.skipHiddenDays(start, direction);
|
|
start = startOfDay(start);
|
|
}
|
|
}
|
|
function computeRes() {
|
|
start = dateEnv.startOf(date, dateAlignment);
|
|
end = dateEnv.add(start, duration);
|
|
res = { start, end };
|
|
}
|
|
computeRes();
|
|
// if range is completely enveloped by hidden days, go past the hidden days
|
|
if (!this.trimHiddenDays(res)) {
|
|
date = this.skipHiddenDays(date, direction);
|
|
computeRes();
|
|
}
|
|
return res;
|
|
}
|
|
// Builds the "current" range when a dayCount is specified.
|
|
buildRangeFromDayCount(date, direction, dayCount) {
|
|
let { dateEnv, dateAlignment } = this.props;
|
|
let runningCount = 0;
|
|
let start = date;
|
|
let end;
|
|
if (dateAlignment) {
|
|
start = dateEnv.startOf(start, dateAlignment);
|
|
}
|
|
start = startOfDay(start);
|
|
start = this.skipHiddenDays(start, direction);
|
|
end = start;
|
|
do {
|
|
end = addDays(end, 1);
|
|
if (!this.isHiddenDay(end)) {
|
|
runningCount += 1;
|
|
}
|
|
} while (runningCount < dayCount);
|
|
return { start, end };
|
|
}
|
|
// Builds a normalized range object for the "visible" range,
|
|
// which is a way to define the currentRange and activeRange at the same time.
|
|
buildCustomVisibleRange(date) {
|
|
let { props } = this;
|
|
let input = props.visibleRangeInput;
|
|
let simpleInput = typeof input === 'function'
|
|
? input.call(props.calendarApi, props.dateEnv.toDate(date))
|
|
: input;
|
|
let range = this.refineRange(simpleInput);
|
|
if (range && (range.start == null || range.end == null)) {
|
|
return null;
|
|
}
|
|
return range;
|
|
}
|
|
// Computes the range that will represent the element/cells for *rendering*,
|
|
// but which may have voided days/times.
|
|
// not responsible for trimming hidden days.
|
|
buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) {
|
|
return currentRange;
|
|
}
|
|
// Compute the duration value that should be added/substracted to the current date
|
|
// when a prev/next operation happens.
|
|
buildDateIncrement(fallback) {
|
|
let { dateIncrement } = this.props;
|
|
let customAlignment;
|
|
if (dateIncrement) {
|
|
return dateIncrement;
|
|
}
|
|
if ((customAlignment = this.props.dateAlignment)) {
|
|
return createDuration(1, customAlignment);
|
|
}
|
|
if (fallback) {
|
|
return fallback;
|
|
}
|
|
return createDuration({ days: 1 });
|
|
}
|
|
refineRange(rangeInput) {
|
|
if (rangeInput) {
|
|
let range = parseRange(rangeInput, this.props.dateEnv);
|
|
if (range) {
|
|
range = computeVisibleDayRange(range);
|
|
}
|
|
return range;
|
|
}
|
|
return null;
|
|
}
|
|
/* Hidden Days
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
// Initializes internal variables related to calculating hidden days-of-week
|
|
initHiddenDays() {
|
|
let hiddenDays = this.props.hiddenDays || []; // array of day-of-week indices that are hidden
|
|
let isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
|
|
let dayCnt = 0;
|
|
let i;
|
|
if (this.props.weekends === false) {
|
|
hiddenDays.push(0, 6); // 0=sunday, 6=saturday
|
|
}
|
|
for (i = 0; i < 7; i += 1) {
|
|
if (!(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)) {
|
|
dayCnt += 1;
|
|
}
|
|
}
|
|
if (!dayCnt) {
|
|
throw new Error('invalid hiddenDays'); // all days were hidden? bad.
|
|
}
|
|
this.isHiddenDayHash = isHiddenDayHash;
|
|
}
|
|
// Remove days from the beginning and end of the range that are computed as hidden.
|
|
// If the whole range is trimmed off, returns null
|
|
trimHiddenDays(range) {
|
|
let { start, end } = range;
|
|
if (start) {
|
|
start = this.skipHiddenDays(start);
|
|
}
|
|
if (end) {
|
|
end = this.skipHiddenDays(end, -1, true);
|
|
}
|
|
if (start == null || end == null || start < end) {
|
|
return { start, end };
|
|
}
|
|
return null;
|
|
}
|
|
// Is the current day hidden?
|
|
// `day` is a day-of-week index (0-6), or a Date (used for UTC)
|
|
isHiddenDay(day) {
|
|
if (day instanceof Date) {
|
|
day = day.getUTCDay();
|
|
}
|
|
return this.isHiddenDayHash[day];
|
|
}
|
|
// Incrementing the current day until it is no longer a hidden day, returning a copy.
|
|
// DOES NOT CONSIDER validRange!
|
|
// If the initial value of `date` is not a hidden day, don't do anything.
|
|
// Pass `isExclusive` as `true` if you are dealing with an end date.
|
|
// `inc` defaults to `1` (increment one day forward each time)
|
|
skipHiddenDays(date, inc = 1, isExclusive = false) {
|
|
while (this.isHiddenDayHash[(date.getUTCDay() + (isExclusive ? inc : 0) + 7) % 7]) {
|
|
date = addDays(date, inc);
|
|
}
|
|
return date;
|
|
}
|
|
}
|
|
|
|
function createEventInstance(defId, range, forcedStartTzo, forcedEndTzo) {
|
|
return {
|
|
instanceId: guid(),
|
|
defId,
|
|
range,
|
|
forcedStartTzo: forcedStartTzo == null ? null : forcedStartTzo,
|
|
forcedEndTzo: forcedEndTzo == null ? null : forcedEndTzo,
|
|
};
|
|
}
|
|
|
|
function parseRecurring(refined, defaultAllDay, dateEnv, recurringTypes) {
|
|
for (let i = 0; i < recurringTypes.length; i += 1) {
|
|
let parsed = recurringTypes[i].parse(refined, dateEnv);
|
|
if (parsed) {
|
|
let { allDay } = refined;
|
|
if (allDay == null) {
|
|
allDay = defaultAllDay;
|
|
if (allDay == null) {
|
|
allDay = parsed.allDayGuess;
|
|
if (allDay == null) {
|
|
allDay = false;
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
allDay,
|
|
duration: parsed.duration,
|
|
typeData: parsed.typeData,
|
|
typeId: i,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function expandRecurring(eventStore, framingRange, context) {
|
|
let { dateEnv, pluginHooks, options } = context;
|
|
let { defs, instances } = eventStore;
|
|
// remove existing recurring instances
|
|
// TODO: bad. always expand events as a second step
|
|
instances = filterHash(instances, (instance) => !defs[instance.defId].recurringDef);
|
|
for (let defId in defs) {
|
|
let def = defs[defId];
|
|
if (def.recurringDef) {
|
|
let { duration } = def.recurringDef;
|
|
if (!duration) {
|
|
duration = def.allDay ?
|
|
options.defaultAllDayEventDuration :
|
|
options.defaultTimedEventDuration;
|
|
}
|
|
let starts = expandRecurringRanges(def, duration, framingRange, dateEnv, pluginHooks.recurringTypes);
|
|
for (let start of starts) {
|
|
let instance = createEventInstance(defId, {
|
|
start,
|
|
end: dateEnv.add(start, duration),
|
|
});
|
|
instances[instance.instanceId] = instance;
|
|
}
|
|
}
|
|
}
|
|
return { defs, instances };
|
|
}
|
|
/*
|
|
Event MUST have a recurringDef
|
|
*/
|
|
function expandRecurringRanges(eventDef, duration, framingRange, dateEnv, recurringTypes) {
|
|
let typeDef = recurringTypes[eventDef.recurringDef.typeId];
|
|
let markers = typeDef.expand(eventDef.recurringDef.typeData, {
|
|
start: dateEnv.subtract(framingRange.start, duration),
|
|
end: framingRange.end,
|
|
}, dateEnv);
|
|
// the recurrence plugins don't guarantee that all-day events are start-of-day, so we have to
|
|
if (eventDef.allDay) {
|
|
markers = markers.map(startOfDay);
|
|
}
|
|
return markers;
|
|
}
|
|
|
|
const EVENT_NON_DATE_REFINERS = {
|
|
id: String,
|
|
groupId: String,
|
|
title: String,
|
|
url: String,
|
|
interactive: Boolean,
|
|
};
|
|
const EVENT_DATE_REFINERS = {
|
|
start: identity,
|
|
end: identity,
|
|
date: identity,
|
|
allDay: Boolean,
|
|
};
|
|
const EVENT_REFINERS = Object.assign(Object.assign(Object.assign({}, EVENT_NON_DATE_REFINERS), EVENT_DATE_REFINERS), { extendedProps: identity });
|
|
function parseEvent(raw, eventSource, context, allowOpenRange, refiners = buildEventRefiners(context), defIdMap, instanceIdMap) {
|
|
let { refined, extra } = refineEventDef(raw, context, refiners);
|
|
let defaultAllDay = computeIsDefaultAllDay(eventSource, context);
|
|
let recurringRes = parseRecurring(refined, defaultAllDay, context.dateEnv, context.pluginHooks.recurringTypes);
|
|
if (recurringRes) {
|
|
let def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', recurringRes.allDay, Boolean(recurringRes.duration), context, defIdMap);
|
|
def.recurringDef = {
|
|
typeId: recurringRes.typeId,
|
|
typeData: recurringRes.typeData,
|
|
duration: recurringRes.duration,
|
|
};
|
|
return { def, instance: null };
|
|
}
|
|
let singleRes = parseSingle(refined, defaultAllDay, context, allowOpenRange);
|
|
if (singleRes) {
|
|
let def = parseEventDef(refined, extra, eventSource ? eventSource.sourceId : '', singleRes.allDay, singleRes.hasEnd, context, defIdMap);
|
|
let instance = createEventInstance(def.defId, singleRes.range, singleRes.forcedStartTzo, singleRes.forcedEndTzo);
|
|
if (instanceIdMap && def.publicId && instanceIdMap[def.publicId]) {
|
|
instance.instanceId = instanceIdMap[def.publicId];
|
|
}
|
|
return { def, instance };
|
|
}
|
|
return null;
|
|
}
|
|
function refineEventDef(raw, context, refiners = buildEventRefiners(context)) {
|
|
return refineProps(raw, refiners);
|
|
}
|
|
function buildEventRefiners(context) {
|
|
return Object.assign(Object.assign(Object.assign({}, EVENT_UI_REFINERS), EVENT_REFINERS), context.pluginHooks.eventRefiners);
|
|
}
|
|
/*
|
|
Will NOT populate extendedProps with the leftover properties.
|
|
Will NOT populate date-related props.
|
|
*/
|
|
function parseEventDef(refined, extra, sourceId, allDay, hasEnd, context, defIdMap) {
|
|
let def = {
|
|
title: refined.title || '',
|
|
groupId: refined.groupId || '',
|
|
publicId: refined.id || '',
|
|
url: refined.url || '',
|
|
recurringDef: null,
|
|
defId: ((defIdMap && refined.id) ? defIdMap[refined.id] : '') || guid(),
|
|
sourceId,
|
|
allDay,
|
|
hasEnd,
|
|
interactive: refined.interactive,
|
|
ui: createEventUi(refined, context),
|
|
extendedProps: Object.assign(Object.assign({}, (refined.extendedProps || {})), extra),
|
|
};
|
|
for (let memberAdder of context.pluginHooks.eventDefMemberAdders) {
|
|
Object.assign(def, memberAdder(refined));
|
|
}
|
|
// help out EventImpl from having user modify props
|
|
Object.freeze(def.ui.classNames);
|
|
Object.freeze(def.extendedProps);
|
|
return def;
|
|
}
|
|
function parseSingle(refined, defaultAllDay, context, allowOpenRange) {
|
|
let { allDay } = refined;
|
|
let startMeta;
|
|
let startMarker = null;
|
|
let hasEnd = false;
|
|
let endMeta;
|
|
let endMarker = null;
|
|
let startInput = refined.start != null ? refined.start : refined.date;
|
|
startMeta = context.dateEnv.createMarkerMeta(startInput);
|
|
if (startMeta) {
|
|
startMarker = startMeta.marker;
|
|
}
|
|
else if (!allowOpenRange) {
|
|
return null;
|
|
}
|
|
if (refined.end != null) {
|
|
endMeta = context.dateEnv.createMarkerMeta(refined.end);
|
|
}
|
|
if (allDay == null) {
|
|
if (defaultAllDay != null) {
|
|
allDay = defaultAllDay;
|
|
}
|
|
else {
|
|
// fall back to the date props LAST
|
|
allDay = (!startMeta || startMeta.isTimeUnspecified) &&
|
|
(!endMeta || endMeta.isTimeUnspecified);
|
|
}
|
|
}
|
|
if (allDay && startMarker) {
|
|
startMarker = startOfDay(startMarker);
|
|
}
|
|
if (endMeta) {
|
|
endMarker = endMeta.marker;
|
|
if (allDay) {
|
|
endMarker = startOfDay(endMarker);
|
|
}
|
|
if (startMarker && endMarker <= startMarker) {
|
|
endMarker = null;
|
|
}
|
|
}
|
|
if (endMarker) {
|
|
hasEnd = true;
|
|
}
|
|
else if (!allowOpenRange) {
|
|
hasEnd = context.options.forceEventDuration || false;
|
|
endMarker = context.dateEnv.add(startMarker, allDay ?
|
|
context.options.defaultAllDayEventDuration :
|
|
context.options.defaultTimedEventDuration);
|
|
}
|
|
return {
|
|
allDay,
|
|
hasEnd,
|
|
range: { start: startMarker, end: endMarker },
|
|
forcedStartTzo: startMeta ? startMeta.forcedTzo : null,
|
|
forcedEndTzo: endMeta ? endMeta.forcedTzo : null,
|
|
};
|
|
}
|
|
function computeIsDefaultAllDay(eventSource, context) {
|
|
let res = null;
|
|
if (eventSource) {
|
|
res = eventSource.defaultAllDay;
|
|
}
|
|
if (res == null) {
|
|
res = context.options.defaultAllDay;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function parseEvents(rawEvents, eventSource, context, allowOpenRange, defIdMap, instanceIdMap) {
|
|
let eventStore = createEmptyEventStore();
|
|
let eventRefiners = buildEventRefiners(context);
|
|
for (let rawEvent of rawEvents) {
|
|
let tuple = parseEvent(rawEvent, eventSource, context, allowOpenRange, eventRefiners, defIdMap, instanceIdMap);
|
|
if (tuple) {
|
|
eventTupleToStore(tuple, eventStore);
|
|
}
|
|
}
|
|
return eventStore;
|
|
}
|
|
function eventTupleToStore(tuple, eventStore = createEmptyEventStore()) {
|
|
eventStore.defs[tuple.def.defId] = tuple.def;
|
|
if (tuple.instance) {
|
|
eventStore.instances[tuple.instance.instanceId] = tuple.instance;
|
|
}
|
|
return eventStore;
|
|
}
|
|
// retrieves events that have the same groupId as the instance specified by `instanceId`
|
|
// or they are the same as the instance.
|
|
// why might instanceId not be in the store? an event from another calendar?
|
|
function getRelevantEvents(eventStore, instanceId) {
|
|
let instance = eventStore.instances[instanceId];
|
|
if (instance) {
|
|
let def = eventStore.defs[instance.defId];
|
|
// get events/instances with same group
|
|
let newStore = filterEventStoreDefs(eventStore, (lookDef) => isEventDefsGrouped(def, lookDef));
|
|
// add the original
|
|
// TODO: wish we could use eventTupleToStore or something like it
|
|
newStore.defs[def.defId] = def;
|
|
newStore.instances[instance.instanceId] = instance;
|
|
return newStore;
|
|
}
|
|
return createEmptyEventStore();
|
|
}
|
|
function isEventDefsGrouped(def0, def1) {
|
|
return Boolean(def0.groupId && def0.groupId === def1.groupId);
|
|
}
|
|
function createEmptyEventStore() {
|
|
return { defs: {}, instances: {} };
|
|
}
|
|
function mergeEventStores(store0, store1) {
|
|
return {
|
|
defs: Object.assign(Object.assign({}, store0.defs), store1.defs),
|
|
instances: Object.assign(Object.assign({}, store0.instances), store1.instances),
|
|
};
|
|
}
|
|
function filterEventStoreDefs(eventStore, filterFunc) {
|
|
let defs = filterHash(eventStore.defs, filterFunc);
|
|
let instances = filterHash(eventStore.instances, (instance) => (defs[instance.defId] // still exists?
|
|
));
|
|
return { defs, instances };
|
|
}
|
|
function excludeSubEventStore(master, sub) {
|
|
let { defs, instances } = master;
|
|
let filteredDefs = {};
|
|
let filteredInstances = {};
|
|
for (let defId in defs) {
|
|
if (!sub.defs[defId]) { // not explicitly excluded
|
|
filteredDefs[defId] = defs[defId];
|
|
}
|
|
}
|
|
for (let instanceId in instances) {
|
|
if (!sub.instances[instanceId] && // not explicitly excluded
|
|
filteredDefs[instances[instanceId].defId] // def wasn't filtered away
|
|
) {
|
|
filteredInstances[instanceId] = instances[instanceId];
|
|
}
|
|
}
|
|
return {
|
|
defs: filteredDefs,
|
|
instances: filteredInstances,
|
|
};
|
|
}
|
|
|
|
function normalizeConstraint(input, context) {
|
|
if (Array.isArray(input)) {
|
|
return parseEvents(input, null, context, true); // allowOpenRange=true
|
|
}
|
|
if (typeof input === 'object' && input) { // non-null object
|
|
return parseEvents([input], null, context, true); // allowOpenRange=true
|
|
}
|
|
if (input != null) {
|
|
return String(input);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseClassNames(raw) {
|
|
if (Array.isArray(raw)) {
|
|
return raw;
|
|
}
|
|
if (typeof raw === 'string') {
|
|
return raw.split(/\s+/);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// TODO: better called "EventSettings" or "EventConfig"
|
|
// TODO: move this file into structs
|
|
// TODO: separate constraint/overlap/allow, because selection uses only that, not other props
|
|
const EVENT_UI_REFINERS = {
|
|
display: String,
|
|
editable: Boolean,
|
|
startEditable: Boolean,
|
|
durationEditable: Boolean,
|
|
constraint: identity,
|
|
overlap: identity,
|
|
allow: identity,
|
|
className: parseClassNames,
|
|
classNames: parseClassNames,
|
|
color: String,
|
|
backgroundColor: String,
|
|
borderColor: String,
|
|
textColor: String,
|
|
};
|
|
const EMPTY_EVENT_UI = {
|
|
display: null,
|
|
startEditable: null,
|
|
durationEditable: null,
|
|
constraints: [],
|
|
overlap: null,
|
|
allows: [],
|
|
backgroundColor: '',
|
|
borderColor: '',
|
|
textColor: '',
|
|
classNames: [],
|
|
};
|
|
function createEventUi(refined, context) {
|
|
let constraint = normalizeConstraint(refined.constraint, context);
|
|
return {
|
|
display: refined.display || null,
|
|
startEditable: refined.startEditable != null ? refined.startEditable : refined.editable,
|
|
durationEditable: refined.durationEditable != null ? refined.durationEditable : refined.editable,
|
|
constraints: constraint != null ? [constraint] : [],
|
|
overlap: refined.overlap != null ? refined.overlap : null,
|
|
allows: refined.allow != null ? [refined.allow] : [],
|
|
backgroundColor: refined.backgroundColor || refined.color || '',
|
|
borderColor: refined.borderColor || refined.color || '',
|
|
textColor: refined.textColor || '',
|
|
classNames: (refined.className || []).concat(refined.classNames || []), // join singular and plural
|
|
};
|
|
}
|
|
// TODO: prevent against problems with <2 args!
|
|
function combineEventUis(uis) {
|
|
return uis.reduce(combineTwoEventUis, EMPTY_EVENT_UI);
|
|
}
|
|
function combineTwoEventUis(item0, item1) {
|
|
return {
|
|
display: item1.display != null ? item1.display : item0.display,
|
|
startEditable: item1.startEditable != null ? item1.startEditable : item0.startEditable,
|
|
durationEditable: item1.durationEditable != null ? item1.durationEditable : item0.durationEditable,
|
|
constraints: item0.constraints.concat(item1.constraints),
|
|
overlap: typeof item1.overlap === 'boolean' ? item1.overlap : item0.overlap,
|
|
allows: item0.allows.concat(item1.allows),
|
|
backgroundColor: item1.backgroundColor || item0.backgroundColor,
|
|
borderColor: item1.borderColor || item0.borderColor,
|
|
textColor: item1.textColor || item0.textColor,
|
|
classNames: item0.classNames.concat(item1.classNames),
|
|
};
|
|
}
|
|
|
|
const EVENT_SOURCE_REFINERS = {
|
|
id: String,
|
|
defaultAllDay: Boolean,
|
|
url: String,
|
|
format: String,
|
|
events: identity,
|
|
eventDataTransform: identity,
|
|
// for any network-related sources
|
|
success: identity,
|
|
failure: identity,
|
|
};
|
|
function parseEventSource(raw, context, refiners = buildEventSourceRefiners(context)) {
|
|
let rawObj;
|
|
if (typeof raw === 'string') {
|
|
rawObj = { url: raw };
|
|
}
|
|
else if (typeof raw === 'function' || Array.isArray(raw)) {
|
|
rawObj = { events: raw };
|
|
}
|
|
else if (typeof raw === 'object' && raw) { // not null
|
|
rawObj = raw;
|
|
}
|
|
if (rawObj) {
|
|
let { refined, extra } = refineProps(rawObj, refiners);
|
|
let metaRes = buildEventSourceMeta(refined, context);
|
|
if (metaRes) {
|
|
return {
|
|
_raw: raw,
|
|
isFetching: false,
|
|
latestFetchId: '',
|
|
fetchRange: null,
|
|
defaultAllDay: refined.defaultAllDay,
|
|
eventDataTransform: refined.eventDataTransform,
|
|
success: refined.success,
|
|
failure: refined.failure,
|
|
publicId: refined.id || '',
|
|
sourceId: guid(),
|
|
sourceDefId: metaRes.sourceDefId,
|
|
meta: metaRes.meta,
|
|
ui: createEventUi(refined, context),
|
|
extendedProps: extra,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function buildEventSourceRefiners(context) {
|
|
return Object.assign(Object.assign(Object.assign({}, EVENT_UI_REFINERS), EVENT_SOURCE_REFINERS), context.pluginHooks.eventSourceRefiners);
|
|
}
|
|
function buildEventSourceMeta(raw, context) {
|
|
let defs = context.pluginHooks.eventSourceDefs;
|
|
for (let i = defs.length - 1; i >= 0; i -= 1) { // later-added plugins take precedence
|
|
let def = defs[i];
|
|
let meta = def.parseMeta(raw);
|
|
if (meta) {
|
|
return { sourceDefId: i, meta };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function reduceEventStore(eventStore, action, eventSources, dateProfile, context) {
|
|
switch (action.type) {
|
|
case 'RECEIVE_EVENTS': // raw
|
|
return receiveRawEvents(eventStore, eventSources[action.sourceId], action.fetchId, action.fetchRange, action.rawEvents, context);
|
|
case 'RESET_RAW_EVENTS':
|
|
return resetRawEvents(eventStore, eventSources[action.sourceId], action.rawEvents, dateProfile.activeRange, context);
|
|
case 'ADD_EVENTS': // already parsed, but not expanded
|
|
return addEvent(eventStore, action.eventStore, // new ones
|
|
dateProfile ? dateProfile.activeRange : null, context);
|
|
case 'RESET_EVENTS':
|
|
return action.eventStore;
|
|
case 'MERGE_EVENTS': // already parsed and expanded
|
|
return mergeEventStores(eventStore, action.eventStore);
|
|
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
|
|
case 'NEXT':
|
|
case 'CHANGE_DATE':
|
|
case 'CHANGE_VIEW_TYPE':
|
|
if (dateProfile) {
|
|
return expandRecurring(eventStore, dateProfile.activeRange, context);
|
|
}
|
|
return eventStore;
|
|
case 'REMOVE_EVENTS':
|
|
return excludeSubEventStore(eventStore, action.eventStore);
|
|
case 'REMOVE_EVENT_SOURCE':
|
|
return excludeEventsBySourceId(eventStore, action.sourceId);
|
|
case 'REMOVE_ALL_EVENT_SOURCES':
|
|
return filterEventStoreDefs(eventStore, (eventDef) => (!eventDef.sourceId // only keep events with no source id
|
|
));
|
|
case 'REMOVE_ALL_EVENTS':
|
|
return createEmptyEventStore();
|
|
default:
|
|
return eventStore;
|
|
}
|
|
}
|
|
function receiveRawEvents(eventStore, eventSource, fetchId, fetchRange, rawEvents, context) {
|
|
if (eventSource && // not already removed
|
|
fetchId === eventSource.latestFetchId // TODO: wish this logic was always in event-sources
|
|
) {
|
|
let subset = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context);
|
|
if (fetchRange) {
|
|
subset = expandRecurring(subset, fetchRange, context);
|
|
}
|
|
return mergeEventStores(excludeEventsBySourceId(eventStore, eventSource.sourceId), subset);
|
|
}
|
|
return eventStore;
|
|
}
|
|
function resetRawEvents(existingEventStore, eventSource, rawEvents, activeRange, context) {
|
|
const { defIdMap, instanceIdMap } = buildPublicIdMaps(existingEventStore);
|
|
let newEventStore = parseEvents(transformRawEvents(rawEvents, eventSource, context), eventSource, context, false, defIdMap, instanceIdMap);
|
|
return expandRecurring(newEventStore, activeRange, context);
|
|
}
|
|
function transformRawEvents(rawEvents, eventSource, context) {
|
|
let calEachTransform = context.options.eventDataTransform;
|
|
let sourceEachTransform = eventSource ? eventSource.eventDataTransform : null;
|
|
if (sourceEachTransform) {
|
|
rawEvents = transformEachRawEvent(rawEvents, sourceEachTransform);
|
|
}
|
|
if (calEachTransform) {
|
|
rawEvents = transformEachRawEvent(rawEvents, calEachTransform);
|
|
}
|
|
return rawEvents;
|
|
}
|
|
function transformEachRawEvent(rawEvents, func) {
|
|
let refinedEvents;
|
|
if (!func) {
|
|
refinedEvents = rawEvents;
|
|
}
|
|
else {
|
|
refinedEvents = [];
|
|
for (let rawEvent of rawEvents) {
|
|
let refinedEvent = func(rawEvent);
|
|
if (refinedEvent) {
|
|
refinedEvents.push(refinedEvent);
|
|
}
|
|
else if (refinedEvent == null) {
|
|
refinedEvents.push(rawEvent);
|
|
} // if a different falsy value, do nothing
|
|
}
|
|
}
|
|
return refinedEvents;
|
|
}
|
|
function addEvent(eventStore, subset, expandRange, context) {
|
|
if (expandRange) {
|
|
subset = expandRecurring(subset, expandRange, context);
|
|
}
|
|
return mergeEventStores(eventStore, subset);
|
|
}
|
|
function rezoneEventStoreDates(eventStore, oldDateEnv, newDateEnv) {
|
|
let { defs } = eventStore;
|
|
let instances = mapHash(eventStore.instances, (instance) => {
|
|
let def = defs[instance.defId];
|
|
if (def.allDay) {
|
|
return instance; // isn't dependent on timezone
|
|
}
|
|
return Object.assign(Object.assign({}, instance), { range: {
|
|
start: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.start, instance.forcedStartTzo)),
|
|
end: newDateEnv.createMarker(oldDateEnv.toDate(instance.range.end, instance.forcedEndTzo)),
|
|
}, forcedStartTzo: newDateEnv.canComputeOffset ? null : instance.forcedStartTzo, forcedEndTzo: newDateEnv.canComputeOffset ? null : instance.forcedEndTzo });
|
|
});
|
|
return { defs, instances };
|
|
}
|
|
function excludeEventsBySourceId(eventStore, sourceId) {
|
|
return filterEventStoreDefs(eventStore, (eventDef) => eventDef.sourceId !== sourceId);
|
|
}
|
|
// QUESTION: why not just return instances? do a general object-property-exclusion util
|
|
function excludeInstances(eventStore, removals) {
|
|
return {
|
|
defs: eventStore.defs,
|
|
instances: filterHash(eventStore.instances, (instance) => !removals[instance.instanceId]),
|
|
};
|
|
}
|
|
function buildPublicIdMaps(eventStore) {
|
|
const { defs, instances } = eventStore;
|
|
const defIdMap = {};
|
|
const instanceIdMap = {};
|
|
for (let defId in defs) {
|
|
const def = defs[defId];
|
|
const { publicId } = def;
|
|
if (publicId) {
|
|
defIdMap[publicId] = defId;
|
|
}
|
|
}
|
|
for (let instanceId in instances) {
|
|
const instance = instances[instanceId];
|
|
const def = defs[instance.defId];
|
|
const { publicId } = def;
|
|
if (publicId) {
|
|
instanceIdMap[publicId] = instanceId;
|
|
}
|
|
}
|
|
return { defIdMap, instanceIdMap };
|
|
}
|
|
|
|
class Emitter {
|
|
constructor() {
|
|
this.handlers = {};
|
|
this.thisContext = null;
|
|
}
|
|
setThisContext(thisContext) {
|
|
this.thisContext = thisContext;
|
|
}
|
|
setOptions(options) {
|
|
this.options = options;
|
|
}
|
|
on(type, handler) {
|
|
addToHash(this.handlers, type, handler);
|
|
}
|
|
off(type, handler) {
|
|
removeFromHash(this.handlers, type, handler);
|
|
}
|
|
trigger(type, ...args) {
|
|
let attachedHandlers = this.handlers[type] || [];
|
|
let optionHandler = this.options && this.options[type];
|
|
let handlers = [].concat(optionHandler || [], attachedHandlers);
|
|
for (let handler of handlers) {
|
|
handler.apply(this.thisContext, args);
|
|
}
|
|
}
|
|
hasHandlers(type) {
|
|
return Boolean((this.handlers[type] && this.handlers[type].length) ||
|
|
(this.options && this.options[type]));
|
|
}
|
|
}
|
|
function addToHash(hash, type, handler) {
|
|
(hash[type] || (hash[type] = []))
|
|
.push(handler);
|
|
}
|
|
function removeFromHash(hash, type, handler) {
|
|
if (handler) {
|
|
if (hash[type]) {
|
|
hash[type] = hash[type].filter((func) => func !== handler);
|
|
}
|
|
}
|
|
else {
|
|
delete hash[type]; // remove all handler funcs for this type
|
|
}
|
|
}
|
|
|
|
const DEF_DEFAULTS = {
|
|
startTime: '09:00',
|
|
endTime: '17:00',
|
|
daysOfWeek: [1, 2, 3, 4, 5],
|
|
display: 'inverse-background',
|
|
classNames: 'fc-non-business',
|
|
groupId: '_businessHours', // so multiple defs get grouped
|
|
};
|
|
/*
|
|
TODO: pass around as EventDefHash!!!
|
|
*/
|
|
function parseBusinessHours(input, context) {
|
|
return parseEvents(refineInputs(input), null, context);
|
|
}
|
|
function refineInputs(input) {
|
|
let rawDefs;
|
|
if (input === true) {
|
|
rawDefs = [{}]; // will get DEF_DEFAULTS verbatim
|
|
}
|
|
else if (Array.isArray(input)) {
|
|
// if specifying an array, every sub-definition NEEDS a day-of-week
|
|
rawDefs = input.filter((rawDef) => rawDef.daysOfWeek);
|
|
}
|
|
else if (typeof input === 'object' && input) { // non-null object
|
|
rawDefs = [input];
|
|
}
|
|
else { // is probably false
|
|
rawDefs = [];
|
|
}
|
|
rawDefs = rawDefs.map((rawDef) => (Object.assign(Object.assign({}, DEF_DEFAULTS), rawDef)));
|
|
return rawDefs;
|
|
}
|
|
|
|
function triggerDateSelect(selection, pev, context) {
|
|
context.emitter.trigger('select', Object.assign(Object.assign({}, buildDateSpanApiWithContext(selection, context)), { jsEvent: pev ? pev.origEvent : null, view: context.viewApi || context.calendarApi.view }));
|
|
}
|
|
function triggerDateUnselect(pev, context) {
|
|
context.emitter.trigger('unselect', {
|
|
jsEvent: pev ? pev.origEvent : null,
|
|
view: context.viewApi || context.calendarApi.view,
|
|
});
|
|
}
|
|
function buildDateSpanApiWithContext(dateSpan, context) {
|
|
let props = {};
|
|
for (let transform of context.pluginHooks.dateSpanTransforms) {
|
|
Object.assign(props, transform(dateSpan, context));
|
|
}
|
|
Object.assign(props, buildDateSpanApi(dateSpan, context.dateEnv));
|
|
return props;
|
|
}
|
|
// Given an event's allDay status and start date, return what its fallback end date should be.
|
|
// TODO: rename to computeDefaultEventEnd
|
|
function getDefaultEventEnd(allDay, marker, context) {
|
|
let { dateEnv, options } = context;
|
|
let end = marker;
|
|
if (allDay) {
|
|
end = startOfDay(end);
|
|
end = dateEnv.add(end, options.defaultAllDayEventDuration);
|
|
}
|
|
else {
|
|
end = dateEnv.add(end, options.defaultTimedEventDuration);
|
|
}
|
|
return end;
|
|
}
|
|
|
|
// applies the mutation to ALL defs/instances within the event store
|
|
function applyMutationToEventStore(eventStore, eventConfigBase, mutation, context) {
|
|
let eventConfigs = compileEventUis(eventStore.defs, eventConfigBase);
|
|
let dest = createEmptyEventStore();
|
|
for (let defId in eventStore.defs) {
|
|
let def = eventStore.defs[defId];
|
|
dest.defs[defId] = applyMutationToEventDef(def, eventConfigs[defId], mutation, context);
|
|
}
|
|
for (let instanceId in eventStore.instances) {
|
|
let instance = eventStore.instances[instanceId];
|
|
let def = dest.defs[instance.defId]; // important to grab the newly modified def
|
|
dest.instances[instanceId] = applyMutationToEventInstance(instance, def, eventConfigs[instance.defId], mutation, context);
|
|
}
|
|
return dest;
|
|
}
|
|
function applyMutationToEventDef(eventDef, eventConfig, mutation, context) {
|
|
let standardProps = mutation.standardProps || {};
|
|
// if hasEnd has not been specified, guess a good value based on deltas.
|
|
// if duration will change, there's no way the default duration will persist,
|
|
// and thus, we need to mark the event as having a real end
|
|
if (standardProps.hasEnd == null &&
|
|
eventConfig.durationEditable &&
|
|
(mutation.startDelta || mutation.endDelta)) {
|
|
standardProps.hasEnd = true; // TODO: is this mutation okay?
|
|
}
|
|
let copy = Object.assign(Object.assign(Object.assign({}, eventDef), standardProps), { ui: Object.assign(Object.assign({}, eventDef.ui), standardProps.ui) });
|
|
if (mutation.extendedProps) {
|
|
copy.extendedProps = Object.assign(Object.assign({}, copy.extendedProps), mutation.extendedProps);
|
|
}
|
|
for (let applier of context.pluginHooks.eventDefMutationAppliers) {
|
|
applier(copy, mutation, context);
|
|
}
|
|
if (!copy.hasEnd && context.options.forceEventDuration) {
|
|
copy.hasEnd = true;
|
|
}
|
|
return copy;
|
|
}
|
|
function applyMutationToEventInstance(eventInstance, eventDef, // must first be modified by applyMutationToEventDef
|
|
eventConfig, mutation, context) {
|
|
let { dateEnv } = context;
|
|
let forceAllDay = mutation.standardProps && mutation.standardProps.allDay === true;
|
|
let clearEnd = mutation.standardProps && mutation.standardProps.hasEnd === false;
|
|
let copy = Object.assign({}, eventInstance);
|
|
if (forceAllDay) {
|
|
copy.range = computeAlignedDayRange(copy.range);
|
|
}
|
|
if (mutation.datesDelta && eventConfig.startEditable) {
|
|
copy.range = {
|
|
start: dateEnv.add(copy.range.start, mutation.datesDelta),
|
|
end: dateEnv.add(copy.range.end, mutation.datesDelta),
|
|
};
|
|
}
|
|
if (mutation.startDelta && eventConfig.durationEditable) {
|
|
copy.range = {
|
|
start: dateEnv.add(copy.range.start, mutation.startDelta),
|
|
end: copy.range.end,
|
|
};
|
|
}
|
|
if (mutation.endDelta && eventConfig.durationEditable) {
|
|
copy.range = {
|
|
start: copy.range.start,
|
|
end: dateEnv.add(copy.range.end, mutation.endDelta),
|
|
};
|
|
}
|
|
if (clearEnd) {
|
|
copy.range = {
|
|
start: copy.range.start,
|
|
end: getDefaultEventEnd(eventDef.allDay, copy.range.start, context),
|
|
};
|
|
}
|
|
// in case event was all-day but the supplied deltas were not
|
|
// better util for this?
|
|
if (eventDef.allDay) {
|
|
copy.range = {
|
|
start: startOfDay(copy.range.start),
|
|
end: startOfDay(copy.range.end),
|
|
};
|
|
}
|
|
// handle invalid durations
|
|
if (copy.range.end < copy.range.start) {
|
|
copy.range.end = getDefaultEventEnd(eventDef.allDay, copy.range.start, context);
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
class EventSourceImpl {
|
|
constructor(context, internalEventSource) {
|
|
this.context = context;
|
|
this.internalEventSource = internalEventSource;
|
|
}
|
|
remove() {
|
|
this.context.dispatch({
|
|
type: 'REMOVE_EVENT_SOURCE',
|
|
sourceId: this.internalEventSource.sourceId,
|
|
});
|
|
}
|
|
refetch() {
|
|
this.context.dispatch({
|
|
type: 'FETCH_EVENT_SOURCES',
|
|
sourceIds: [this.internalEventSource.sourceId],
|
|
isRefetch: true,
|
|
});
|
|
}
|
|
get id() {
|
|
return this.internalEventSource.publicId;
|
|
}
|
|
get url() {
|
|
return this.internalEventSource.meta.url;
|
|
}
|
|
get format() {
|
|
return this.internalEventSource.meta.format; // TODO: bad. not guaranteed
|
|
}
|
|
}
|
|
|
|
class EventImpl {
|
|
// instance will be null if expressing a recurring event that has no current instances,
|
|
// OR if trying to validate an incoming external event that has no dates assigned
|
|
constructor(context, def, instance) {
|
|
this._context = context;
|
|
this._def = def;
|
|
this._instance = instance || null;
|
|
}
|
|
/*
|
|
TODO: make event struct more responsible for this
|
|
*/
|
|
setProp(name, val) {
|
|
if (name in EVENT_DATE_REFINERS) {
|
|
console.warn('Could not set date-related prop \'name\'. Use one of the date-related methods instead.');
|
|
// TODO: make proper aliasing system?
|
|
}
|
|
else if (name === 'id') {
|
|
val = EVENT_NON_DATE_REFINERS[name](val);
|
|
this.mutate({
|
|
standardProps: { publicId: val }, // hardcoded internal name
|
|
});
|
|
}
|
|
else if (name in EVENT_NON_DATE_REFINERS) {
|
|
val = EVENT_NON_DATE_REFINERS[name](val);
|
|
this.mutate({
|
|
standardProps: { [name]: val },
|
|
});
|
|
}
|
|
else if (name in EVENT_UI_REFINERS) {
|
|
let ui = EVENT_UI_REFINERS[name](val);
|
|
if (name === 'color') {
|
|
ui = { backgroundColor: val, borderColor: val };
|
|
}
|
|
else if (name === 'editable') {
|
|
ui = { startEditable: val, durationEditable: val };
|
|
}
|
|
else {
|
|
ui = { [name]: val };
|
|
}
|
|
this.mutate({
|
|
standardProps: { ui },
|
|
});
|
|
}
|
|
else {
|
|
console.warn(`Could not set prop '${name}'. Use setExtendedProp instead.`);
|
|
}
|
|
}
|
|
setExtendedProp(name, val) {
|
|
this.mutate({
|
|
extendedProps: { [name]: val },
|
|
});
|
|
}
|
|
setStart(startInput, options = {}) {
|
|
let { dateEnv } = this._context;
|
|
let start = dateEnv.createMarker(startInput);
|
|
if (start && this._instance) { // TODO: warning if parsed bad
|
|
let instanceRange = this._instance.range;
|
|
let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity); // what if parsed bad!?
|
|
if (options.maintainDuration) {
|
|
this.mutate({ datesDelta: startDelta });
|
|
}
|
|
else {
|
|
this.mutate({ startDelta });
|
|
}
|
|
}
|
|
}
|
|
setEnd(endInput, options = {}) {
|
|
let { dateEnv } = this._context;
|
|
let end;
|
|
if (endInput != null) {
|
|
end = dateEnv.createMarker(endInput);
|
|
if (!end) {
|
|
return; // TODO: warning if parsed bad
|
|
}
|
|
}
|
|
if (this._instance) {
|
|
if (end) {
|
|
let endDelta = diffDates(this._instance.range.end, end, dateEnv, options.granularity);
|
|
this.mutate({ endDelta });
|
|
}
|
|
else {
|
|
this.mutate({ standardProps: { hasEnd: false } });
|
|
}
|
|
}
|
|
}
|
|
setDates(startInput, endInput, options = {}) {
|
|
let { dateEnv } = this._context;
|
|
let standardProps = { allDay: options.allDay };
|
|
let start = dateEnv.createMarker(startInput);
|
|
let end;
|
|
if (!start) {
|
|
return; // TODO: warning if parsed bad
|
|
}
|
|
if (endInput != null) {
|
|
end = dateEnv.createMarker(endInput);
|
|
if (!end) { // TODO: warning if parsed bad
|
|
return;
|
|
}
|
|
}
|
|
if (this._instance) {
|
|
let instanceRange = this._instance.range;
|
|
// when computing the diff for an event being converted to all-day,
|
|
// compute diff off of the all-day values the way event-mutation does.
|
|
if (options.allDay === true) {
|
|
instanceRange = computeAlignedDayRange(instanceRange);
|
|
}
|
|
let startDelta = diffDates(instanceRange.start, start, dateEnv, options.granularity);
|
|
if (end) {
|
|
let endDelta = diffDates(instanceRange.end, end, dateEnv, options.granularity);
|
|
if (durationsEqual(startDelta, endDelta)) {
|
|
this.mutate({ datesDelta: startDelta, standardProps });
|
|
}
|
|
else {
|
|
this.mutate({ startDelta, endDelta, standardProps });
|
|
}
|
|
}
|
|
else { // means "clear the end"
|
|
standardProps.hasEnd = false;
|
|
this.mutate({ datesDelta: startDelta, standardProps });
|
|
}
|
|
}
|
|
}
|
|
moveStart(deltaInput) {
|
|
let delta = createDuration(deltaInput);
|
|
if (delta) { // TODO: warning if parsed bad
|
|
this.mutate({ startDelta: delta });
|
|
}
|
|
}
|
|
moveEnd(deltaInput) {
|
|
let delta = createDuration(deltaInput);
|
|
if (delta) { // TODO: warning if parsed bad
|
|
this.mutate({ endDelta: delta });
|
|
}
|
|
}
|
|
moveDates(deltaInput) {
|
|
let delta = createDuration(deltaInput);
|
|
if (delta) { // TODO: warning if parsed bad
|
|
this.mutate({ datesDelta: delta });
|
|
}
|
|
}
|
|
setAllDay(allDay, options = {}) {
|
|
let standardProps = { allDay };
|
|
let { maintainDuration } = options;
|
|
if (maintainDuration == null) {
|
|
maintainDuration = this._context.options.allDayMaintainDuration;
|
|
}
|
|
if (this._def.allDay !== allDay) {
|
|
standardProps.hasEnd = maintainDuration;
|
|
}
|
|
this.mutate({ standardProps });
|
|
}
|
|
formatRange(formatInput) {
|
|
let { dateEnv } = this._context;
|
|
let instance = this._instance;
|
|
let formatter = createFormatter(formatInput);
|
|
if (this._def.hasEnd) {
|
|
return dateEnv.formatRange(instance.range.start, instance.range.end, formatter, {
|
|
forcedStartTzo: instance.forcedStartTzo,
|
|
forcedEndTzo: instance.forcedEndTzo,
|
|
});
|
|
}
|
|
return dateEnv.format(instance.range.start, formatter, {
|
|
forcedTzo: instance.forcedStartTzo,
|
|
});
|
|
}
|
|
mutate(mutation) {
|
|
let instance = this._instance;
|
|
if (instance) {
|
|
let def = this._def;
|
|
let context = this._context;
|
|
let { eventStore } = context.getCurrentData();
|
|
let relevantEvents = getRelevantEvents(eventStore, instance.instanceId);
|
|
let eventConfigBase = {
|
|
'': {
|
|
display: '',
|
|
startEditable: true,
|
|
durationEditable: true,
|
|
constraints: [],
|
|
overlap: null,
|
|
allows: [],
|
|
backgroundColor: '',
|
|
borderColor: '',
|
|
textColor: '',
|
|
classNames: [],
|
|
},
|
|
};
|
|
relevantEvents = applyMutationToEventStore(relevantEvents, eventConfigBase, mutation, context);
|
|
let oldEvent = new EventImpl(context, def, instance); // snapshot
|
|
this._def = relevantEvents.defs[def.defId];
|
|
this._instance = relevantEvents.instances[instance.instanceId];
|
|
context.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: relevantEvents,
|
|
});
|
|
context.emitter.trigger('eventChange', {
|
|
oldEvent,
|
|
event: this,
|
|
relatedEvents: buildEventApis(relevantEvents, context, instance),
|
|
revert() {
|
|
context.dispatch({
|
|
type: 'RESET_EVENTS',
|
|
eventStore, // the ORIGINAL store
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|
|
remove() {
|
|
let context = this._context;
|
|
let asStore = eventApiToStore(this);
|
|
context.dispatch({
|
|
type: 'REMOVE_EVENTS',
|
|
eventStore: asStore,
|
|
});
|
|
context.emitter.trigger('eventRemove', {
|
|
event: this,
|
|
relatedEvents: [],
|
|
revert() {
|
|
context.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: asStore,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
get source() {
|
|
let { sourceId } = this._def;
|
|
if (sourceId) {
|
|
return new EventSourceImpl(this._context, this._context.getCurrentData().eventSources[sourceId]);
|
|
}
|
|
return null;
|
|
}
|
|
get start() {
|
|
return this._instance ?
|
|
this._context.dateEnv.toDate(this._instance.range.start) :
|
|
null;
|
|
}
|
|
get end() {
|
|
return (this._instance && this._def.hasEnd) ?
|
|
this._context.dateEnv.toDate(this._instance.range.end) :
|
|
null;
|
|
}
|
|
get startStr() {
|
|
let instance = this._instance;
|
|
if (instance) {
|
|
return this._context.dateEnv.formatIso(instance.range.start, {
|
|
omitTime: this._def.allDay,
|
|
forcedTzo: instance.forcedStartTzo,
|
|
});
|
|
}
|
|
return '';
|
|
}
|
|
get endStr() {
|
|
let instance = this._instance;
|
|
if (instance && this._def.hasEnd) {
|
|
return this._context.dateEnv.formatIso(instance.range.end, {
|
|
omitTime: this._def.allDay,
|
|
forcedTzo: instance.forcedEndTzo,
|
|
});
|
|
}
|
|
return '';
|
|
}
|
|
// computable props that all access the def
|
|
// TODO: find a TypeScript-compatible way to do this at scale
|
|
get id() { return this._def.publicId; }
|
|
get groupId() { return this._def.groupId; }
|
|
get allDay() { return this._def.allDay; }
|
|
get title() { return this._def.title; }
|
|
get url() { return this._def.url; }
|
|
get display() { return this._def.ui.display || 'auto'; } // bad. just normalize the type earlier
|
|
get startEditable() { return this._def.ui.startEditable; }
|
|
get durationEditable() { return this._def.ui.durationEditable; }
|
|
get constraint() { return this._def.ui.constraints[0] || null; }
|
|
get overlap() { return this._def.ui.overlap; }
|
|
get allow() { return this._def.ui.allows[0] || null; }
|
|
get backgroundColor() { return this._def.ui.backgroundColor; }
|
|
get borderColor() { return this._def.ui.borderColor; }
|
|
get textColor() { return this._def.ui.textColor; }
|
|
// NOTE: user can't modify these because Object.freeze was called in event-def parsing
|
|
get classNames() { return this._def.ui.classNames; }
|
|
get extendedProps() { return this._def.extendedProps; }
|
|
toPlainObject(settings = {}) {
|
|
let def = this._def;
|
|
let { ui } = def;
|
|
let { startStr, endStr } = this;
|
|
let res = {
|
|
allDay: def.allDay,
|
|
};
|
|
if (def.title) {
|
|
res.title = def.title;
|
|
}
|
|
if (startStr) {
|
|
res.start = startStr;
|
|
}
|
|
if (endStr) {
|
|
res.end = endStr;
|
|
}
|
|
if (def.publicId) {
|
|
res.id = def.publicId;
|
|
}
|
|
if (def.groupId) {
|
|
res.groupId = def.groupId;
|
|
}
|
|
if (def.url) {
|
|
res.url = def.url;
|
|
}
|
|
if (ui.display && ui.display !== 'auto') {
|
|
res.display = ui.display;
|
|
}
|
|
// TODO: what about recurring-event properties???
|
|
// TODO: include startEditable/durationEditable/constraint/overlap/allow
|
|
if (settings.collapseColor && ui.backgroundColor && ui.backgroundColor === ui.borderColor) {
|
|
res.color = ui.backgroundColor;
|
|
}
|
|
else {
|
|
if (ui.backgroundColor) {
|
|
res.backgroundColor = ui.backgroundColor;
|
|
}
|
|
if (ui.borderColor) {
|
|
res.borderColor = ui.borderColor;
|
|
}
|
|
}
|
|
if (ui.textColor) {
|
|
res.textColor = ui.textColor;
|
|
}
|
|
if (ui.classNames.length) {
|
|
res.classNames = ui.classNames;
|
|
}
|
|
if (Object.keys(def.extendedProps).length) {
|
|
if (settings.collapseExtendedProps) {
|
|
Object.assign(res, def.extendedProps);
|
|
}
|
|
else {
|
|
res.extendedProps = def.extendedProps;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
toJSON() {
|
|
return this.toPlainObject();
|
|
}
|
|
}
|
|
function eventApiToStore(eventApi) {
|
|
let def = eventApi._def;
|
|
let instance = eventApi._instance;
|
|
return {
|
|
defs: { [def.defId]: def },
|
|
instances: instance
|
|
? { [instance.instanceId]: instance }
|
|
: {},
|
|
};
|
|
}
|
|
function buildEventApis(eventStore, context, excludeInstance) {
|
|
let { defs, instances } = eventStore;
|
|
let eventApis = [];
|
|
let excludeInstanceId = excludeInstance ? excludeInstance.instanceId : '';
|
|
for (let id in instances) {
|
|
let instance = instances[id];
|
|
let def = defs[instance.defId];
|
|
if (instance.instanceId !== excludeInstanceId) {
|
|
eventApis.push(new EventImpl(context, def, instance));
|
|
}
|
|
}
|
|
return eventApis;
|
|
}
|
|
|
|
/*
|
|
Specifying nextDayThreshold signals that all-day ranges should be sliced.
|
|
*/
|
|
function sliceEventStore(eventStore, eventUiBases, framingRange, nextDayThreshold) {
|
|
let inverseBgByGroupId = {};
|
|
let inverseBgByDefId = {};
|
|
let defByGroupId = {};
|
|
let bgRanges = [];
|
|
let fgRanges = [];
|
|
let eventUis = compileEventUis(eventStore.defs, eventUiBases);
|
|
for (let defId in eventStore.defs) {
|
|
let def = eventStore.defs[defId];
|
|
let ui = eventUis[def.defId];
|
|
if (ui.display === 'inverse-background') {
|
|
if (def.groupId) {
|
|
inverseBgByGroupId[def.groupId] = [];
|
|
if (!defByGroupId[def.groupId]) {
|
|
defByGroupId[def.groupId] = def;
|
|
}
|
|
}
|
|
else {
|
|
inverseBgByDefId[defId] = [];
|
|
}
|
|
}
|
|
}
|
|
for (let instanceId in eventStore.instances) {
|
|
let instance = eventStore.instances[instanceId];
|
|
let def = eventStore.defs[instance.defId];
|
|
let ui = eventUis[def.defId];
|
|
let origRange = instance.range;
|
|
let normalRange = (!def.allDay && nextDayThreshold) ?
|
|
computeVisibleDayRange(origRange, nextDayThreshold) :
|
|
origRange;
|
|
let slicedRange = intersectRanges(normalRange, framingRange);
|
|
if (slicedRange) {
|
|
if (ui.display === 'inverse-background') {
|
|
if (def.groupId) {
|
|
inverseBgByGroupId[def.groupId].push(slicedRange);
|
|
}
|
|
else {
|
|
inverseBgByDefId[instance.defId].push(slicedRange);
|
|
}
|
|
}
|
|
else if (ui.display !== 'none') {
|
|
(ui.display === 'background' ? bgRanges : fgRanges).push({
|
|
def,
|
|
ui,
|
|
instance,
|
|
range: slicedRange,
|
|
isStart: normalRange.start && normalRange.start.valueOf() === slicedRange.start.valueOf(),
|
|
isEnd: normalRange.end && normalRange.end.valueOf() === slicedRange.end.valueOf(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (let groupId in inverseBgByGroupId) { // BY GROUP
|
|
let ranges = inverseBgByGroupId[groupId];
|
|
let invertedRanges = invertRanges(ranges, framingRange);
|
|
for (let invertedRange of invertedRanges) {
|
|
let def = defByGroupId[groupId];
|
|
let ui = eventUis[def.defId];
|
|
bgRanges.push({
|
|
def,
|
|
ui,
|
|
instance: null,
|
|
range: invertedRange,
|
|
isStart: false,
|
|
isEnd: false,
|
|
});
|
|
}
|
|
}
|
|
for (let defId in inverseBgByDefId) {
|
|
let ranges = inverseBgByDefId[defId];
|
|
let invertedRanges = invertRanges(ranges, framingRange);
|
|
for (let invertedRange of invertedRanges) {
|
|
bgRanges.push({
|
|
def: eventStore.defs[defId],
|
|
ui: eventUis[defId],
|
|
instance: null,
|
|
range: invertedRange,
|
|
isStart: false,
|
|
isEnd: false,
|
|
});
|
|
}
|
|
}
|
|
return { bg: bgRanges, fg: fgRanges };
|
|
}
|
|
function hasBgRendering(def) {
|
|
return def.ui.display === 'background' || def.ui.display === 'inverse-background';
|
|
}
|
|
function setElSeg(el, seg) {
|
|
el.fcSeg = seg;
|
|
}
|
|
function getElSeg(el) {
|
|
return el.fcSeg ||
|
|
el.parentNode.fcSeg || // for the harness
|
|
null;
|
|
}
|
|
// event ui computation
|
|
function compileEventUis(eventDefs, eventUiBases) {
|
|
return mapHash(eventDefs, (eventDef) => compileEventUi(eventDef, eventUiBases));
|
|
}
|
|
function compileEventUi(eventDef, eventUiBases) {
|
|
let uis = [];
|
|
if (eventUiBases['']) {
|
|
uis.push(eventUiBases['']);
|
|
}
|
|
if (eventUiBases[eventDef.defId]) {
|
|
uis.push(eventUiBases[eventDef.defId]);
|
|
}
|
|
uis.push(eventDef.ui);
|
|
return combineEventUis(uis);
|
|
}
|
|
function sortEventSegs(segs, eventOrderSpecs) {
|
|
let objs = segs.map(buildSegCompareObj);
|
|
objs.sort((obj0, obj1) => compareByFieldSpecs(obj0, obj1, eventOrderSpecs));
|
|
return objs.map((c) => c._seg);
|
|
}
|
|
// returns a object with all primitive props that can be compared
|
|
function buildSegCompareObj(seg) {
|
|
let { eventRange } = seg;
|
|
let eventDef = eventRange.def;
|
|
let range = eventRange.instance ? eventRange.instance.range : eventRange.range;
|
|
let start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events
|
|
let end = range.end ? range.end.valueOf() : 0; // "
|
|
return Object.assign(Object.assign(Object.assign({}, eventDef.extendedProps), eventDef), { id: eventDef.publicId, start,
|
|
end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg });
|
|
}
|
|
function computeSegDraggable(seg, context) {
|
|
let { pluginHooks } = context;
|
|
let transformers = pluginHooks.isDraggableTransformers;
|
|
let { def, ui } = seg.eventRange;
|
|
let val = ui.startEditable;
|
|
for (let transformer of transformers) {
|
|
val = transformer(val, def, ui, context);
|
|
}
|
|
return val;
|
|
}
|
|
function computeSegStartResizable(seg, context) {
|
|
return seg.isStart && seg.eventRange.ui.durationEditable && context.options.eventResizableFromStart;
|
|
}
|
|
function computeSegEndResizable(seg, context) {
|
|
return seg.isEnd && seg.eventRange.ui.durationEditable;
|
|
}
|
|
function buildSegTimeText(seg, timeFormat, context, defaultDisplayEventTime, // defaults to true
|
|
defaultDisplayEventEnd, // defaults to true
|
|
startOverride, endOverride) {
|
|
let { dateEnv, options } = context;
|
|
let { displayEventTime, displayEventEnd } = options;
|
|
let eventDef = seg.eventRange.def;
|
|
let eventInstance = seg.eventRange.instance;
|
|
if (displayEventTime == null) {
|
|
displayEventTime = defaultDisplayEventTime !== false;
|
|
}
|
|
if (displayEventEnd == null) {
|
|
displayEventEnd = defaultDisplayEventEnd !== false;
|
|
}
|
|
let wholeEventStart = eventInstance.range.start;
|
|
let wholeEventEnd = eventInstance.range.end;
|
|
let segStart = startOverride || seg.start || seg.eventRange.range.start;
|
|
let segEnd = endOverride || seg.end || seg.eventRange.range.end;
|
|
let isStartDay = startOfDay(wholeEventStart).valueOf() === startOfDay(segStart).valueOf();
|
|
let isEndDay = startOfDay(addMs(wholeEventEnd, -1)).valueOf() === startOfDay(addMs(segEnd, -1)).valueOf();
|
|
if (displayEventTime && !eventDef.allDay && (isStartDay || isEndDay)) {
|
|
segStart = isStartDay ? wholeEventStart : segStart;
|
|
segEnd = isEndDay ? wholeEventEnd : segEnd;
|
|
if (displayEventEnd && eventDef.hasEnd) {
|
|
return dateEnv.formatRange(segStart, segEnd, timeFormat, {
|
|
forcedStartTzo: startOverride ? null : eventInstance.forcedStartTzo,
|
|
forcedEndTzo: endOverride ? null : eventInstance.forcedEndTzo,
|
|
});
|
|
}
|
|
return dateEnv.format(segStart, timeFormat, {
|
|
forcedTzo: startOverride ? null : eventInstance.forcedStartTzo, // nooooo, same
|
|
});
|
|
}
|
|
return '';
|
|
}
|
|
function getSegMeta(seg, todayRange, nowDate) {
|
|
let segRange = seg.eventRange.range;
|
|
return {
|
|
isPast: segRange.end <= (nowDate || todayRange.start),
|
|
isFuture: segRange.start >= (nowDate || todayRange.end),
|
|
isToday: todayRange && rangeContainsMarker(todayRange, segRange.start),
|
|
};
|
|
}
|
|
function getEventClassNames(props) {
|
|
let classNames = ['fc-event'];
|
|
if (props.isMirror) {
|
|
classNames.push('fc-event-mirror');
|
|
}
|
|
if (props.isDraggable) {
|
|
classNames.push('fc-event-draggable');
|
|
}
|
|
if (props.isStartResizable || props.isEndResizable) {
|
|
classNames.push('fc-event-resizable');
|
|
}
|
|
if (props.isDragging) {
|
|
classNames.push('fc-event-dragging');
|
|
}
|
|
if (props.isResizing) {
|
|
classNames.push('fc-event-resizing');
|
|
}
|
|
if (props.isSelected) {
|
|
classNames.push('fc-event-selected');
|
|
}
|
|
if (props.isStart) {
|
|
classNames.push('fc-event-start');
|
|
}
|
|
if (props.isEnd) {
|
|
classNames.push('fc-event-end');
|
|
}
|
|
if (props.isPast) {
|
|
classNames.push('fc-event-past');
|
|
}
|
|
if (props.isToday) {
|
|
classNames.push('fc-event-today');
|
|
}
|
|
if (props.isFuture) {
|
|
classNames.push('fc-event-future');
|
|
}
|
|
return classNames;
|
|
}
|
|
function buildEventRangeKey(eventRange) {
|
|
return eventRange.instance
|
|
? eventRange.instance.instanceId
|
|
: `${eventRange.def.defId}:${eventRange.range.start.toISOString()}`;
|
|
// inverse-background events don't have specific instances. TODO: better solution
|
|
}
|
|
function getSegAnchorAttrs(seg, context) {
|
|
let { def, instance } = seg.eventRange;
|
|
let { url } = def;
|
|
if (url) {
|
|
return { href: url };
|
|
}
|
|
let { emitter, options } = context;
|
|
let { eventInteractive } = options;
|
|
if (eventInteractive == null) {
|
|
eventInteractive = def.interactive;
|
|
if (eventInteractive == null) {
|
|
eventInteractive = Boolean(emitter.hasHandlers('eventClick'));
|
|
}
|
|
}
|
|
// mock what happens in EventClicking
|
|
if (eventInteractive) {
|
|
// only attach keyboard-related handlers because click handler is already done in EventClicking
|
|
return createAriaKeyboardAttrs((ev) => {
|
|
emitter.trigger('eventClick', {
|
|
el: ev.target,
|
|
event: new EventImpl(context, def, instance),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
});
|
|
}
|
|
return {};
|
|
}
|
|
|
|
const STANDARD_PROPS = {
|
|
start: identity,
|
|
end: identity,
|
|
allDay: Boolean,
|
|
};
|
|
function parseDateSpan(raw, dateEnv, defaultDuration) {
|
|
let span = parseOpenDateSpan(raw, dateEnv);
|
|
let { range } = span;
|
|
if (!range.start) {
|
|
return null;
|
|
}
|
|
if (!range.end) {
|
|
if (defaultDuration == null) {
|
|
return null;
|
|
}
|
|
range.end = dateEnv.add(range.start, defaultDuration);
|
|
}
|
|
return span;
|
|
}
|
|
/*
|
|
TODO: somehow combine with parseRange?
|
|
Will return null if the start/end props were present but parsed invalidly.
|
|
*/
|
|
function parseOpenDateSpan(raw, dateEnv) {
|
|
let { refined: standardProps, extra } = refineProps(raw, STANDARD_PROPS);
|
|
let startMeta = standardProps.start ? dateEnv.createMarkerMeta(standardProps.start) : null;
|
|
let endMeta = standardProps.end ? dateEnv.createMarkerMeta(standardProps.end) : null;
|
|
let { allDay } = standardProps;
|
|
if (allDay == null) {
|
|
allDay = (startMeta && startMeta.isTimeUnspecified) &&
|
|
(!endMeta || endMeta.isTimeUnspecified);
|
|
}
|
|
return Object.assign({ range: {
|
|
start: startMeta ? startMeta.marker : null,
|
|
end: endMeta ? endMeta.marker : null,
|
|
}, allDay }, extra);
|
|
}
|
|
function isDateSpansEqual(span0, span1) {
|
|
return rangesEqual(span0.range, span1.range) &&
|
|
span0.allDay === span1.allDay &&
|
|
isSpanPropsEqual(span0, span1);
|
|
}
|
|
// the NON-DATE-RELATED props
|
|
function isSpanPropsEqual(span0, span1) {
|
|
for (let propName in span1) {
|
|
if (propName !== 'range' && propName !== 'allDay') {
|
|
if (span0[propName] !== span1[propName]) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// are there any props that span0 has that span1 DOESN'T have?
|
|
// both have range/allDay, so no need to special-case.
|
|
for (let propName in span0) {
|
|
if (!(propName in span1)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function buildDateSpanApi(span, dateEnv) {
|
|
return Object.assign(Object.assign({}, buildRangeApi(span.range, dateEnv, span.allDay)), { allDay: span.allDay });
|
|
}
|
|
function buildRangeApiWithTimeZone(range, dateEnv, omitTime) {
|
|
return Object.assign(Object.assign({}, buildRangeApi(range, dateEnv, omitTime)), { timeZone: dateEnv.timeZone });
|
|
}
|
|
function buildRangeApi(range, dateEnv, omitTime) {
|
|
return {
|
|
start: dateEnv.toDate(range.start),
|
|
end: dateEnv.toDate(range.end),
|
|
startStr: dateEnv.formatIso(range.start, { omitTime }),
|
|
endStr: dateEnv.formatIso(range.end, { omitTime }),
|
|
};
|
|
}
|
|
function fabricateEventRange(dateSpan, eventUiBases, context) {
|
|
let res = refineEventDef({ editable: false }, context);
|
|
let def = parseEventDef(res.refined, res.extra, '', // sourceId
|
|
dateSpan.allDay, true, // hasEnd
|
|
context);
|
|
return {
|
|
def,
|
|
ui: compileEventUi(def, eventUiBases),
|
|
instance: createEventInstance(def.defId, dateSpan.range),
|
|
range: dateSpan.range,
|
|
isStart: true,
|
|
isEnd: true,
|
|
};
|
|
}
|
|
|
|
/*
|
|
given a function that resolves a result asynchronously.
|
|
the function can either call passed-in success and failure callbacks,
|
|
or it can return a promise.
|
|
if you need to pass additional params to func, bind them first.
|
|
*/
|
|
function unpromisify(func, normalizedSuccessCallback, normalizedFailureCallback) {
|
|
// guard against success/failure callbacks being called more than once
|
|
// and guard against a promise AND callback being used together.
|
|
let isResolved = false;
|
|
let wrappedSuccess = function (res) {
|
|
if (!isResolved) {
|
|
isResolved = true;
|
|
normalizedSuccessCallback(res);
|
|
}
|
|
};
|
|
let wrappedFailure = function (error) {
|
|
if (!isResolved) {
|
|
isResolved = true;
|
|
normalizedFailureCallback(error);
|
|
}
|
|
};
|
|
let res = func(wrappedSuccess, wrappedFailure);
|
|
if (res && typeof res.then === 'function') {
|
|
res.then(wrappedSuccess, wrappedFailure);
|
|
}
|
|
}
|
|
|
|
class JsonRequestError extends Error {
|
|
constructor(message, response) {
|
|
super(message);
|
|
this.response = response;
|
|
}
|
|
}
|
|
function requestJson(method, url, params) {
|
|
method = method.toUpperCase();
|
|
const fetchOptions = {
|
|
method,
|
|
};
|
|
if (method === 'GET') {
|
|
url += (url.indexOf('?') === -1 ? '?' : '&') +
|
|
new URLSearchParams(params);
|
|
}
|
|
else {
|
|
fetchOptions.body = new URLSearchParams(params);
|
|
fetchOptions.headers = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
};
|
|
}
|
|
return fetch(url, fetchOptions).then((fetchRes) => {
|
|
if (fetchRes.ok) {
|
|
return fetchRes.json().then((parsedResponse) => {
|
|
return [parsedResponse, fetchRes];
|
|
}, () => {
|
|
throw new JsonRequestError('Failure parsing JSON', fetchRes);
|
|
});
|
|
}
|
|
else {
|
|
throw new JsonRequestError('Request failed', fetchRes);
|
|
}
|
|
});
|
|
}
|
|
|
|
let canVGrowWithinCell;
|
|
function getCanVGrowWithinCell() {
|
|
if (canVGrowWithinCell == null) {
|
|
canVGrowWithinCell = computeCanVGrowWithinCell();
|
|
}
|
|
return canVGrowWithinCell;
|
|
}
|
|
function computeCanVGrowWithinCell() {
|
|
// for SSR, because this function is call immediately at top-level
|
|
// TODO: just make this logic execute top-level, immediately, instead of doing lazily
|
|
if (typeof document === 'undefined') {
|
|
return true;
|
|
}
|
|
let el = document.createElement('div');
|
|
el.style.position = 'absolute';
|
|
el.style.top = '0px';
|
|
el.style.left = '0px';
|
|
el.innerHTML = '<table><tr><td><div></div></td></tr></table>';
|
|
el.querySelector('table').style.height = '100px';
|
|
el.querySelector('div').style.height = '100%';
|
|
document.body.appendChild(el);
|
|
let div = el.querySelector('div');
|
|
let possible = div.offsetHeight > 0;
|
|
document.body.removeChild(el);
|
|
return possible;
|
|
}
|
|
|
|
class CalendarRoot extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
forPrint: false,
|
|
};
|
|
this.handleBeforePrint = () => {
|
|
flushSync(() => {
|
|
this.setState({ forPrint: true });
|
|
});
|
|
};
|
|
this.handleAfterPrint = () => {
|
|
flushSync(() => {
|
|
this.setState({ forPrint: false });
|
|
});
|
|
};
|
|
}
|
|
render() {
|
|
let { props } = this;
|
|
let { options } = props;
|
|
let { forPrint } = this.state;
|
|
let isHeightAuto = forPrint || options.height === 'auto' || options.contentHeight === 'auto';
|
|
let height = (!isHeightAuto && options.height != null) ? options.height : '';
|
|
let classNames = [
|
|
'fc',
|
|
forPrint ? 'fc-media-print' : 'fc-media-screen',
|
|
`fc-direction-${options.direction}`,
|
|
props.theme.getClass('root'),
|
|
];
|
|
if (!getCanVGrowWithinCell()) {
|
|
classNames.push('fc-liquid-hack');
|
|
}
|
|
return props.children(classNames, height, isHeightAuto, forPrint);
|
|
}
|
|
componentDidMount() {
|
|
let { emitter } = this.props;
|
|
emitter.on('_beforeprint', this.handleBeforePrint);
|
|
emitter.on('_afterprint', this.handleAfterPrint);
|
|
}
|
|
componentWillUnmount() {
|
|
let { emitter } = this.props;
|
|
emitter.off('_beforeprint', this.handleBeforePrint);
|
|
emitter.off('_afterprint', this.handleAfterPrint);
|
|
}
|
|
}
|
|
|
|
class internal_common_Interaction {
|
|
constructor(settings) {
|
|
this.component = settings.component;
|
|
this.isHitComboAllowed = settings.isHitComboAllowed || null;
|
|
}
|
|
destroy() {
|
|
}
|
|
}
|
|
function parseInteractionSettings(component, input) {
|
|
return {
|
|
component,
|
|
el: input.el,
|
|
useEventCenter: input.useEventCenter != null ? input.useEventCenter : true,
|
|
isHitComboAllowed: input.isHitComboAllowed || null,
|
|
};
|
|
}
|
|
function interactionSettingsToStore(settings) {
|
|
return {
|
|
[settings.component.uid]: settings,
|
|
};
|
|
}
|
|
// global state
|
|
const interactionSettingsStore = {};
|
|
|
|
class NowTimer extends x {
|
|
constructor(props, context) {
|
|
super(props, context);
|
|
this.handleRefresh = () => {
|
|
let timing = this.computeTiming();
|
|
if (timing.state.nowDate.valueOf() !== this.state.nowDate.valueOf()) {
|
|
this.setState(timing.state);
|
|
}
|
|
this.clearTimeout();
|
|
this.setTimeout(timing.waitMs);
|
|
};
|
|
this.handleVisibilityChange = () => {
|
|
if (!document.hidden) {
|
|
this.handleRefresh();
|
|
}
|
|
};
|
|
this.state = this.computeTiming().state;
|
|
}
|
|
render() {
|
|
let { props, state } = this;
|
|
return props.children(state.nowDate, state.todayRange);
|
|
}
|
|
componentDidMount() {
|
|
this.setTimeout();
|
|
this.context.nowManager.addResetListener(this.handleRefresh);
|
|
// fired tab becomes visible after being hidden
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
if (prevProps.unit !== this.props.unit) {
|
|
this.clearTimeout();
|
|
this.setTimeout();
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
this.clearTimeout();
|
|
this.context.nowManager.removeResetListener(this.handleRefresh);
|
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
|
}
|
|
computeTiming() {
|
|
let { props, context } = this;
|
|
let unroundedNow = context.nowManager.getDateMarker();
|
|
let { nowIndicatorSnap } = context.options;
|
|
if (nowIndicatorSnap === 'auto') {
|
|
nowIndicatorSnap =
|
|
// large unit?
|
|
/year|month|week|day/.test(props.unit) ||
|
|
// if slotDuration 30 mins for example, would NOT appear to snap (legacy behavior)
|
|
(props.unitValue || 1) === 1;
|
|
}
|
|
let nowDate;
|
|
let waitMs;
|
|
if (nowIndicatorSnap) {
|
|
nowDate = context.dateEnv.startOf(unroundedNow, props.unit); // aka currentUnitStart
|
|
let nextUnitStart = context.dateEnv.add(nowDate, createDuration(1, props.unit));
|
|
waitMs = nextUnitStart.valueOf() - unroundedNow.valueOf();
|
|
}
|
|
else {
|
|
nowDate = unroundedNow;
|
|
waitMs = 1000 * 60; // 1 minute
|
|
}
|
|
// there is a max setTimeout ms value (https://stackoverflow.com/a/3468650/96342)
|
|
// ensure no longer than a day
|
|
waitMs = Math.min(1000 * 60 * 60 * 24, waitMs);
|
|
return {
|
|
state: { nowDate, todayRange: buildDayRange(nowDate) },
|
|
waitMs,
|
|
};
|
|
}
|
|
setTimeout(waitMs = this.computeTiming().waitMs) {
|
|
// NOTE: timeout could take longer than expected if tab sleeps,
|
|
// which is why we listen to 'visibilitychange'
|
|
this.timeoutId = setTimeout(() => {
|
|
// NOTE: timeout could also return *earlier* than expected, and we need to wait 2 ms more
|
|
// This is why use use same waitMs from computeTiming, so we don't skip an interval while
|
|
// .setState() is executing
|
|
const timing = this.computeTiming();
|
|
this.setState(timing.state, () => {
|
|
this.setTimeout(timing.waitMs);
|
|
});
|
|
}, waitMs);
|
|
}
|
|
clearTimeout() {
|
|
if (this.timeoutId) {
|
|
clearTimeout(this.timeoutId);
|
|
}
|
|
}
|
|
}
|
|
NowTimer.contextType = ViewContextType;
|
|
function buildDayRange(date) {
|
|
let start = startOfDay(date);
|
|
let end = addDays(start, 1);
|
|
return { start, end };
|
|
}
|
|
|
|
class CalendarImpl {
|
|
getCurrentData() {
|
|
return this.currentDataManager.getCurrentData();
|
|
}
|
|
dispatch(action) {
|
|
this.currentDataManager.dispatch(action);
|
|
}
|
|
get view() { return this.getCurrentData().viewApi; }
|
|
batchRendering(callback) {
|
|
callback();
|
|
}
|
|
updateSize() {
|
|
this.trigger('_resize', true);
|
|
}
|
|
// Options
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
setOption(name, val) {
|
|
this.dispatch({
|
|
type: 'SET_OPTION',
|
|
optionName: name,
|
|
rawOptionValue: val,
|
|
});
|
|
}
|
|
getOption(name) {
|
|
return this.currentDataManager.currentCalendarOptionsInput[name];
|
|
}
|
|
getAvailableLocaleCodes() {
|
|
return Object.keys(this.getCurrentData().availableRawLocales);
|
|
}
|
|
// Trigger
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
on(handlerName, handler) {
|
|
let { currentDataManager } = this;
|
|
if (currentDataManager.currentCalendarOptionsRefiners[handlerName]) {
|
|
currentDataManager.emitter.on(handlerName, handler);
|
|
}
|
|
else {
|
|
console.warn(`Unknown listener name '${handlerName}'`);
|
|
}
|
|
}
|
|
off(handlerName, handler) {
|
|
this.currentDataManager.emitter.off(handlerName, handler);
|
|
}
|
|
// not meant for public use
|
|
trigger(handlerName, ...args) {
|
|
this.currentDataManager.emitter.trigger(handlerName, ...args);
|
|
}
|
|
// View
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
changeView(viewType, dateOrRange) {
|
|
this.batchRendering(() => {
|
|
this.unselect();
|
|
if (dateOrRange) {
|
|
if (dateOrRange.start && dateOrRange.end) { // a range
|
|
this.dispatch({
|
|
type: 'CHANGE_VIEW_TYPE',
|
|
viewType,
|
|
});
|
|
this.dispatch({
|
|
type: 'SET_OPTION',
|
|
optionName: 'visibleRange',
|
|
rawOptionValue: dateOrRange,
|
|
});
|
|
}
|
|
else {
|
|
let { dateEnv } = this.getCurrentData();
|
|
this.dispatch({
|
|
type: 'CHANGE_VIEW_TYPE',
|
|
viewType,
|
|
dateMarker: dateEnv.createMarker(dateOrRange),
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
this.dispatch({
|
|
type: 'CHANGE_VIEW_TYPE',
|
|
viewType,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
// Forces navigation to a view for the given date.
|
|
// `viewType` can be a specific view name or a generic one like "week" or "day".
|
|
// needs to change
|
|
zoomTo(dateMarker, viewType) {
|
|
let state = this.getCurrentData();
|
|
let spec;
|
|
viewType = viewType || 'day'; // day is default zoom
|
|
spec = state.viewSpecs[viewType] || this.getUnitViewSpec(viewType);
|
|
this.unselect();
|
|
if (spec) {
|
|
this.dispatch({
|
|
type: 'CHANGE_VIEW_TYPE',
|
|
viewType: spec.type,
|
|
dateMarker,
|
|
});
|
|
}
|
|
else {
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker,
|
|
});
|
|
}
|
|
}
|
|
// Given a duration singular unit, like "week" or "day", finds a matching view spec.
|
|
// Preference is given to views that have corresponding buttons.
|
|
getUnitViewSpec(unit) {
|
|
let { viewSpecs, toolbarConfig } = this.getCurrentData();
|
|
let viewTypes = [].concat(toolbarConfig.header ? toolbarConfig.header.viewsWithButtons : [], toolbarConfig.footer ? toolbarConfig.footer.viewsWithButtons : []);
|
|
let i;
|
|
let spec;
|
|
for (let viewType in viewSpecs) {
|
|
viewTypes.push(viewType);
|
|
}
|
|
for (i = 0; i < viewTypes.length; i += 1) {
|
|
spec = viewSpecs[viewTypes[i]];
|
|
if (spec) {
|
|
if (spec.singleUnit === unit) {
|
|
return spec;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
// Current Date
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
prev() {
|
|
this.unselect();
|
|
this.dispatch({ type: 'PREV' });
|
|
}
|
|
next() {
|
|
this.unselect();
|
|
this.dispatch({ type: 'NEXT' });
|
|
}
|
|
prevYear() {
|
|
let state = this.getCurrentData();
|
|
this.unselect();
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker: state.dateEnv.addYears(state.currentDate, -1),
|
|
});
|
|
}
|
|
nextYear() {
|
|
let state = this.getCurrentData();
|
|
this.unselect();
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker: state.dateEnv.addYears(state.currentDate, 1),
|
|
});
|
|
}
|
|
today() {
|
|
let state = this.getCurrentData();
|
|
this.unselect();
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker: state.nowManager.getDateMarker(),
|
|
});
|
|
}
|
|
gotoDate(zonedDateInput) {
|
|
let state = this.getCurrentData();
|
|
this.unselect();
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker: state.dateEnv.createMarker(zonedDateInput),
|
|
});
|
|
}
|
|
incrementDate(deltaInput) {
|
|
let state = this.getCurrentData();
|
|
let delta = createDuration(deltaInput);
|
|
if (delta) { // else, warn about invalid input?
|
|
this.unselect();
|
|
this.dispatch({
|
|
type: 'CHANGE_DATE',
|
|
dateMarker: state.dateEnv.add(state.currentDate, delta),
|
|
});
|
|
}
|
|
}
|
|
getDate() {
|
|
let state = this.getCurrentData();
|
|
return state.dateEnv.toDate(state.currentDate);
|
|
}
|
|
// Date Formatting Utils
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
formatDate(d, formatter) {
|
|
let { dateEnv } = this.getCurrentData();
|
|
return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter));
|
|
}
|
|
// `settings` is for formatter AND isEndExclusive
|
|
formatRange(d0, d1, settings) {
|
|
let { dateEnv } = this.getCurrentData();
|
|
return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings), settings);
|
|
}
|
|
formatIso(d, omitTime) {
|
|
let { dateEnv } = this.getCurrentData();
|
|
return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime });
|
|
}
|
|
// Date Selection / Event Selection / DayClick
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
select(dateOrObj, endDate) {
|
|
let selectionInput;
|
|
if (endDate == null) {
|
|
if (dateOrObj.start != null) {
|
|
selectionInput = dateOrObj;
|
|
}
|
|
else {
|
|
selectionInput = {
|
|
start: dateOrObj,
|
|
end: null,
|
|
};
|
|
}
|
|
}
|
|
else {
|
|
selectionInput = {
|
|
start: dateOrObj,
|
|
end: endDate,
|
|
};
|
|
}
|
|
let state = this.getCurrentData();
|
|
let selection = parseDateSpan(selectionInput, state.dateEnv, createDuration({ days: 1 }));
|
|
if (selection) { // throw parse error otherwise?
|
|
this.dispatch({ type: 'SELECT_DATES', selection });
|
|
triggerDateSelect(selection, null, state);
|
|
}
|
|
}
|
|
unselect(pev) {
|
|
let state = this.getCurrentData();
|
|
if (state.dateSelection) {
|
|
this.dispatch({ type: 'UNSELECT_DATES' });
|
|
triggerDateUnselect(pev, state);
|
|
}
|
|
}
|
|
// Public Events API
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
addEvent(eventInput, sourceInput) {
|
|
if (eventInput instanceof EventImpl) {
|
|
let def = eventInput._def;
|
|
let instance = eventInput._instance;
|
|
let currentData = this.getCurrentData();
|
|
// not already present? don't want to add an old snapshot
|
|
if (!currentData.eventStore.defs[def.defId]) {
|
|
this.dispatch({
|
|
type: 'ADD_EVENTS',
|
|
eventStore: eventTupleToStore({ def, instance }), // TODO: better util for two args?
|
|
});
|
|
this.triggerEventAdd(eventInput);
|
|
}
|
|
return eventInput;
|
|
}
|
|
let state = this.getCurrentData();
|
|
let eventSource;
|
|
if (sourceInput instanceof EventSourceImpl) {
|
|
eventSource = sourceInput.internalEventSource;
|
|
}
|
|
else if (typeof sourceInput === 'boolean') {
|
|
if (sourceInput) { // true. part of the first event source
|
|
[eventSource] = hashValuesToArray(state.eventSources);
|
|
}
|
|
}
|
|
else if (sourceInput != null) { // an ID. accepts a number too
|
|
let sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function
|
|
if (!sourceApi) {
|
|
console.warn(`Could not find an event source with ID "${sourceInput}"`); // TODO: test
|
|
return null;
|
|
}
|
|
eventSource = sourceApi.internalEventSource;
|
|
}
|
|
let tuple = parseEvent(eventInput, eventSource, state, false);
|
|
if (tuple) {
|
|
let newEventApi = new EventImpl(state, tuple.def, tuple.def.recurringDef ? null : tuple.instance);
|
|
this.dispatch({
|
|
type: 'ADD_EVENTS',
|
|
eventStore: eventTupleToStore(tuple),
|
|
});
|
|
this.triggerEventAdd(newEventApi);
|
|
return newEventApi;
|
|
}
|
|
return null;
|
|
}
|
|
triggerEventAdd(eventApi) {
|
|
let { emitter } = this.getCurrentData();
|
|
emitter.trigger('eventAdd', {
|
|
event: eventApi,
|
|
relatedEvents: [],
|
|
revert: () => {
|
|
this.dispatch({
|
|
type: 'REMOVE_EVENTS',
|
|
eventStore: eventApiToStore(eventApi),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
// TODO: optimize
|
|
getEventById(id) {
|
|
let state = this.getCurrentData();
|
|
let { defs, instances } = state.eventStore;
|
|
id = String(id);
|
|
for (let defId in defs) {
|
|
let def = defs[defId];
|
|
if (def.publicId === id) {
|
|
if (def.recurringDef) {
|
|
return new EventImpl(state, def, null);
|
|
}
|
|
for (let instanceId in instances) {
|
|
let instance = instances[instanceId];
|
|
if (instance.defId === def.defId) {
|
|
return new EventImpl(state, def, instance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
getEvents() {
|
|
let currentData = this.getCurrentData();
|
|
return buildEventApis(currentData.eventStore, currentData);
|
|
}
|
|
removeAllEvents() {
|
|
this.dispatch({ type: 'REMOVE_ALL_EVENTS' });
|
|
}
|
|
// Public Event Sources API
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
getEventSources() {
|
|
let state = this.getCurrentData();
|
|
let sourceHash = state.eventSources;
|
|
let sourceApis = [];
|
|
for (let internalId in sourceHash) {
|
|
sourceApis.push(new EventSourceImpl(state, sourceHash[internalId]));
|
|
}
|
|
return sourceApis;
|
|
}
|
|
getEventSourceById(id) {
|
|
let state = this.getCurrentData();
|
|
let sourceHash = state.eventSources;
|
|
id = String(id);
|
|
for (let sourceId in sourceHash) {
|
|
if (sourceHash[sourceId].publicId === id) {
|
|
return new EventSourceImpl(state, sourceHash[sourceId]);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
addEventSource(sourceInput) {
|
|
let state = this.getCurrentData();
|
|
if (sourceInput instanceof EventSourceImpl) {
|
|
// not already present? don't want to add an old snapshot
|
|
if (!state.eventSources[sourceInput.internalEventSource.sourceId]) {
|
|
this.dispatch({
|
|
type: 'ADD_EVENT_SOURCES',
|
|
sources: [sourceInput.internalEventSource],
|
|
});
|
|
}
|
|
return sourceInput;
|
|
}
|
|
let eventSource = parseEventSource(sourceInput, state);
|
|
if (eventSource) { // TODO: error otherwise?
|
|
this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] });
|
|
return new EventSourceImpl(state, eventSource);
|
|
}
|
|
return null;
|
|
}
|
|
removeAllEventSources() {
|
|
this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' });
|
|
}
|
|
refetchEvents() {
|
|
this.dispatch({ type: 'FETCH_EVENT_SOURCES', isRefetch: true });
|
|
}
|
|
// Scroll
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
scrollToTime(timeInput) {
|
|
let time = createDuration(timeInput);
|
|
if (time) {
|
|
this.trigger('_scrollRequest', { time });
|
|
}
|
|
}
|
|
}
|
|
|
|
function pointInsideRect(point, rect) {
|
|
return point.left >= rect.left &&
|
|
point.left < rect.right &&
|
|
point.top >= rect.top &&
|
|
point.top < rect.bottom;
|
|
}
|
|
// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
|
|
function intersectRects(rect1, rect2) {
|
|
let res = {
|
|
left: Math.max(rect1.left, rect2.left),
|
|
right: Math.min(rect1.right, rect2.right),
|
|
top: Math.max(rect1.top, rect2.top),
|
|
bottom: Math.min(rect1.bottom, rect2.bottom),
|
|
};
|
|
if (res.left < res.right && res.top < res.bottom) {
|
|
return res;
|
|
}
|
|
return false;
|
|
}
|
|
function translateRect(rect, deltaX, deltaY) {
|
|
return {
|
|
left: rect.left + deltaX,
|
|
right: rect.right + deltaX,
|
|
top: rect.top + deltaY,
|
|
bottom: rect.bottom + deltaY,
|
|
};
|
|
}
|
|
// Returns a new point that will have been moved to reside within the given rectangle
|
|
function constrainPoint(point, rect) {
|
|
return {
|
|
left: Math.min(Math.max(point.left, rect.left), rect.right),
|
|
top: Math.min(Math.max(point.top, rect.top), rect.bottom),
|
|
};
|
|
}
|
|
// Returns a point that is the center of the given rectangle
|
|
function getRectCenter(rect) {
|
|
return {
|
|
left: (rect.left + rect.right) / 2,
|
|
top: (rect.top + rect.bottom) / 2,
|
|
};
|
|
}
|
|
// Subtracts point2's coordinates from point1's coordinates, returning a delta
|
|
function diffPoints(point1, point2) {
|
|
return {
|
|
left: point1.left - point2.left,
|
|
top: point1.top - point2.top,
|
|
};
|
|
}
|
|
|
|
const EMPTY_EVENT_STORE = createEmptyEventStore(); // for purecomponents. TODO: keep elsewhere
|
|
class Splitter {
|
|
constructor() {
|
|
this.getKeysForEventDefs = memoize(this._getKeysForEventDefs);
|
|
this.splitDateSelection = memoize(this._splitDateSpan);
|
|
this.splitEventStore = memoize(this._splitEventStore);
|
|
this.splitIndividualUi = memoize(this._splitIndividualUi);
|
|
this.splitEventDrag = memoize(this._splitInteraction);
|
|
this.splitEventResize = memoize(this._splitInteraction);
|
|
this.eventUiBuilders = {}; // TODO: typescript protection
|
|
}
|
|
splitProps(props) {
|
|
let keyInfos = this.getKeyInfo(props);
|
|
let defKeys = this.getKeysForEventDefs(props.eventStore);
|
|
let dateSelections = this.splitDateSelection(props.dateSelection);
|
|
let individualUi = this.splitIndividualUi(props.eventUiBases, defKeys); // the individual *bases*
|
|
let eventStores = this.splitEventStore(props.eventStore, defKeys);
|
|
let eventDrags = this.splitEventDrag(props.eventDrag);
|
|
let eventResizes = this.splitEventResize(props.eventResize);
|
|
let splitProps = {};
|
|
this.eventUiBuilders = mapHash(keyInfos, (info, key) => this.eventUiBuilders[key] || memoize(buildEventUiForKey));
|
|
for (let key in keyInfos) {
|
|
let keyInfo = keyInfos[key];
|
|
let eventStore = eventStores[key] || EMPTY_EVENT_STORE;
|
|
let buildEventUi = this.eventUiBuilders[key];
|
|
splitProps[key] = {
|
|
businessHours: keyInfo.businessHours || props.businessHours,
|
|
dateSelection: dateSelections[key] || null,
|
|
eventStore,
|
|
eventUiBases: buildEventUi(props.eventUiBases[''], keyInfo.ui, individualUi[key]),
|
|
eventSelection: eventStore.instances[props.eventSelection] ? props.eventSelection : '',
|
|
eventDrag: eventDrags[key] || null,
|
|
eventResize: eventResizes[key] || null,
|
|
};
|
|
}
|
|
return splitProps;
|
|
}
|
|
_splitDateSpan(dateSpan) {
|
|
let dateSpans = {};
|
|
if (dateSpan) {
|
|
let keys = this.getKeysForDateSpan(dateSpan);
|
|
for (let key of keys) {
|
|
dateSpans[key] = dateSpan;
|
|
}
|
|
}
|
|
return dateSpans;
|
|
}
|
|
_getKeysForEventDefs(eventStore) {
|
|
return mapHash(eventStore.defs, (eventDef) => this.getKeysForEventDef(eventDef));
|
|
}
|
|
_splitEventStore(eventStore, defKeys) {
|
|
let { defs, instances } = eventStore;
|
|
let splitStores = {};
|
|
for (let defId in defs) {
|
|
for (let key of defKeys[defId]) {
|
|
if (!splitStores[key]) {
|
|
splitStores[key] = createEmptyEventStore();
|
|
}
|
|
splitStores[key].defs[defId] = defs[defId];
|
|
}
|
|
}
|
|
for (let instanceId in instances) {
|
|
let instance = instances[instanceId];
|
|
for (let key of defKeys[instance.defId]) {
|
|
if (splitStores[key]) { // must have already been created
|
|
splitStores[key].instances[instanceId] = instance;
|
|
}
|
|
}
|
|
}
|
|
return splitStores;
|
|
}
|
|
_splitIndividualUi(eventUiBases, defKeys) {
|
|
let splitHashes = {};
|
|
for (let defId in eventUiBases) {
|
|
if (defId) { // not the '' key
|
|
for (let key of defKeys[defId]) {
|
|
if (!splitHashes[key]) {
|
|
splitHashes[key] = {};
|
|
}
|
|
splitHashes[key][defId] = eventUiBases[defId];
|
|
}
|
|
}
|
|
}
|
|
return splitHashes;
|
|
}
|
|
_splitInteraction(interaction) {
|
|
let splitStates = {};
|
|
if (interaction) {
|
|
let affectedStores = this._splitEventStore(interaction.affectedEvents, this._getKeysForEventDefs(interaction.affectedEvents));
|
|
// can't rely on defKeys because event data is mutated
|
|
let mutatedKeysByDefId = this._getKeysForEventDefs(interaction.mutatedEvents);
|
|
let mutatedStores = this._splitEventStore(interaction.mutatedEvents, mutatedKeysByDefId);
|
|
let populate = (key) => {
|
|
if (!splitStates[key]) {
|
|
splitStates[key] = {
|
|
affectedEvents: affectedStores[key] || EMPTY_EVENT_STORE,
|
|
mutatedEvents: mutatedStores[key] || EMPTY_EVENT_STORE,
|
|
isEvent: interaction.isEvent,
|
|
};
|
|
}
|
|
};
|
|
for (let key in affectedStores) {
|
|
populate(key);
|
|
}
|
|
for (let key in mutatedStores) {
|
|
populate(key);
|
|
}
|
|
}
|
|
return splitStates;
|
|
}
|
|
}
|
|
function buildEventUiForKey(allUi, eventUiForKey, individualUi) {
|
|
let baseParts = [];
|
|
if (allUi) {
|
|
baseParts.push(allUi);
|
|
}
|
|
if (eventUiForKey) {
|
|
baseParts.push(eventUiForKey);
|
|
}
|
|
let stuff = {
|
|
'': combineEventUis(baseParts),
|
|
};
|
|
if (individualUi) {
|
|
Object.assign(stuff, individualUi);
|
|
}
|
|
return stuff;
|
|
}
|
|
|
|
function getDateMeta(date, todayRange, nowDate, dateProfile) {
|
|
return {
|
|
dow: date.getUTCDay(),
|
|
isDisabled: Boolean(dateProfile && (!dateProfile.activeRange || !rangeContainsMarker(dateProfile.activeRange, date))),
|
|
isOther: Boolean(dateProfile && !rangeContainsMarker(dateProfile.currentRange, date)),
|
|
isToday: Boolean(todayRange && rangeContainsMarker(todayRange, date)),
|
|
isPast: Boolean(nowDate ? (date < nowDate) : todayRange ? (date < todayRange.start) : false),
|
|
isFuture: Boolean(nowDate ? (date > nowDate) : todayRange ? (date >= todayRange.end) : false),
|
|
};
|
|
}
|
|
function getDayClassNames(meta, theme) {
|
|
let classNames = [
|
|
'fc-day',
|
|
`fc-day-${DAY_IDS[meta.dow]}`,
|
|
];
|
|
if (meta.isDisabled) {
|
|
classNames.push('fc-day-disabled');
|
|
}
|
|
else {
|
|
if (meta.isToday) {
|
|
classNames.push('fc-day-today');
|
|
classNames.push(theme.getClass('today'));
|
|
}
|
|
if (meta.isPast) {
|
|
classNames.push('fc-day-past');
|
|
}
|
|
if (meta.isFuture) {
|
|
classNames.push('fc-day-future');
|
|
}
|
|
if (meta.isOther) {
|
|
classNames.push('fc-day-other');
|
|
}
|
|
}
|
|
return classNames;
|
|
}
|
|
function getSlotClassNames(meta, theme) {
|
|
let classNames = [
|
|
'fc-slot',
|
|
`fc-slot-${DAY_IDS[meta.dow]}`,
|
|
];
|
|
if (meta.isDisabled) {
|
|
classNames.push('fc-slot-disabled');
|
|
}
|
|
else {
|
|
if (meta.isToday) {
|
|
classNames.push('fc-slot-today');
|
|
classNames.push(theme.getClass('today'));
|
|
}
|
|
if (meta.isPast) {
|
|
classNames.push('fc-slot-past');
|
|
}
|
|
if (meta.isFuture) {
|
|
classNames.push('fc-slot-future');
|
|
}
|
|
}
|
|
return classNames;
|
|
}
|
|
|
|
const DAY_FORMAT = createFormatter({ year: 'numeric', month: 'long', day: 'numeric' });
|
|
const WEEK_FORMAT = createFormatter({ week: 'long' });
|
|
function buildNavLinkAttrs(context, dateMarker, viewType = 'day', isTabbable = true) {
|
|
const { dateEnv, options, calendarApi } = context;
|
|
let dateStr = dateEnv.format(dateMarker, viewType === 'week' ? WEEK_FORMAT : DAY_FORMAT);
|
|
if (options.navLinks) {
|
|
let zonedDate = dateEnv.toDate(dateMarker);
|
|
const handleInteraction = (ev) => {
|
|
let customAction = viewType === 'day' ? options.navLinkDayClick :
|
|
viewType === 'week' ? options.navLinkWeekClick : null;
|
|
if (typeof customAction === 'function') {
|
|
customAction.call(calendarApi, dateEnv.toDate(dateMarker), ev);
|
|
}
|
|
else {
|
|
if (typeof customAction === 'string') {
|
|
viewType = customAction;
|
|
}
|
|
calendarApi.zoomTo(dateMarker, viewType);
|
|
}
|
|
};
|
|
return Object.assign({ title: formatWithOrdinals(options.navLinkHint, [dateStr, zonedDate], dateStr), 'data-navlink': '' }, (isTabbable
|
|
? createAriaClickAttrs(handleInteraction)
|
|
: { onClick: handleInteraction }));
|
|
}
|
|
return { 'aria-label': dateStr };
|
|
}
|
|
|
|
let _isRtlScrollbarOnLeft = null;
|
|
function getIsRtlScrollbarOnLeft() {
|
|
if (_isRtlScrollbarOnLeft === null) {
|
|
_isRtlScrollbarOnLeft = computeIsRtlScrollbarOnLeft();
|
|
}
|
|
return _isRtlScrollbarOnLeft;
|
|
}
|
|
function computeIsRtlScrollbarOnLeft() {
|
|
let outerEl = document.createElement('div');
|
|
applyStyle(outerEl, {
|
|
position: 'absolute',
|
|
top: -1000,
|
|
left: 0,
|
|
border: 0,
|
|
padding: 0,
|
|
overflow: 'scroll',
|
|
direction: 'rtl',
|
|
});
|
|
outerEl.innerHTML = '<div></div>';
|
|
document.body.appendChild(outerEl);
|
|
let innerEl = outerEl.firstChild;
|
|
let res = innerEl.getBoundingClientRect().left > outerEl.getBoundingClientRect().left;
|
|
removeElement(outerEl);
|
|
return res;
|
|
}
|
|
|
|
let _scrollbarWidths;
|
|
function getScrollbarWidths() {
|
|
if (!_scrollbarWidths) {
|
|
_scrollbarWidths = computeScrollbarWidths();
|
|
}
|
|
return _scrollbarWidths;
|
|
}
|
|
function computeScrollbarWidths() {
|
|
let el = document.createElement('div');
|
|
el.style.overflow = 'scroll';
|
|
el.style.position = 'absolute';
|
|
el.style.top = '-9999px';
|
|
el.style.left = '-9999px';
|
|
document.body.appendChild(el);
|
|
let res = computeScrollbarWidthsForEl(el);
|
|
document.body.removeChild(el);
|
|
return res;
|
|
}
|
|
// WARNING: will include border
|
|
function computeScrollbarWidthsForEl(el) {
|
|
return {
|
|
x: el.offsetHeight - el.clientHeight,
|
|
y: el.offsetWidth - el.clientWidth,
|
|
};
|
|
}
|
|
|
|
function computeEdges(el, getPadding = false) {
|
|
let computedStyle = window.getComputedStyle(el);
|
|
let borderLeft = parseInt(computedStyle.borderLeftWidth, 10) || 0;
|
|
let borderRight = parseInt(computedStyle.borderRightWidth, 10) || 0;
|
|
let borderTop = parseInt(computedStyle.borderTopWidth, 10) || 0;
|
|
let borderBottom = parseInt(computedStyle.borderBottomWidth, 10) || 0;
|
|
let badScrollbarWidths = computeScrollbarWidthsForEl(el); // includes border!
|
|
let scrollbarLeftRight = badScrollbarWidths.y - borderLeft - borderRight;
|
|
let scrollbarBottom = badScrollbarWidths.x - borderTop - borderBottom;
|
|
let res = {
|
|
borderLeft,
|
|
borderRight,
|
|
borderTop,
|
|
borderBottom,
|
|
scrollbarBottom,
|
|
scrollbarLeft: 0,
|
|
scrollbarRight: 0,
|
|
};
|
|
if (getIsRtlScrollbarOnLeft() && computedStyle.direction === 'rtl') { // is the scrollbar on the left side?
|
|
res.scrollbarLeft = scrollbarLeftRight;
|
|
}
|
|
else {
|
|
res.scrollbarRight = scrollbarLeftRight;
|
|
}
|
|
if (getPadding) {
|
|
res.paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;
|
|
res.paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;
|
|
res.paddingTop = parseInt(computedStyle.paddingTop, 10) || 0;
|
|
res.paddingBottom = parseInt(computedStyle.paddingBottom, 10) || 0;
|
|
}
|
|
return res;
|
|
}
|
|
function computeInnerRect(el, goWithinPadding = false, doFromWindowViewport) {
|
|
let outerRect = doFromWindowViewport ? el.getBoundingClientRect() : computeRect(el);
|
|
let edges = computeEdges(el, goWithinPadding);
|
|
let res = {
|
|
left: outerRect.left + edges.borderLeft + edges.scrollbarLeft,
|
|
right: outerRect.right - edges.borderRight - edges.scrollbarRight,
|
|
top: outerRect.top + edges.borderTop,
|
|
bottom: outerRect.bottom - edges.borderBottom - edges.scrollbarBottom,
|
|
};
|
|
if (goWithinPadding) {
|
|
res.left += edges.paddingLeft;
|
|
res.right -= edges.paddingRight;
|
|
res.top += edges.paddingTop;
|
|
res.bottom -= edges.paddingBottom;
|
|
}
|
|
return res;
|
|
}
|
|
function computeRect(el) {
|
|
let rect = el.getBoundingClientRect();
|
|
return {
|
|
left: rect.left + window.scrollX,
|
|
top: rect.top + window.scrollY,
|
|
right: rect.right + window.scrollX,
|
|
bottom: rect.bottom + window.scrollY,
|
|
};
|
|
}
|
|
function computeClippedClientRect(el) {
|
|
let clippingParents = getClippingParents(el);
|
|
let rect = el.getBoundingClientRect();
|
|
for (let clippingParent of clippingParents) {
|
|
let intersection = intersectRects(rect, clippingParent.getBoundingClientRect());
|
|
if (intersection) {
|
|
rect = intersection;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
return rect;
|
|
}
|
|
// does not return window
|
|
function getClippingParents(el) {
|
|
let parents = [];
|
|
while (el instanceof HTMLElement) { // will stop when gets to document or null
|
|
let computedStyle = window.getComputedStyle(el);
|
|
if (computedStyle.position === 'fixed') {
|
|
break;
|
|
}
|
|
if ((/(auto|scroll)/).test(computedStyle.overflow + computedStyle.overflowY + computedStyle.overflowX)) {
|
|
parents.push(el);
|
|
}
|
|
el = el.parentNode;
|
|
}
|
|
return parents;
|
|
}
|
|
|
|
/*
|
|
Records offset information for a set of elements, relative to an origin element.
|
|
Can record the left/right OR the top/bottom OR both.
|
|
Provides methods for querying the cache by position.
|
|
*/
|
|
class PositionCache {
|
|
constructor(originEl, els, isHorizontal, isVertical) {
|
|
this.els = els;
|
|
let originClientRect = this.originClientRect = originEl.getBoundingClientRect(); // relative to viewport top-left
|
|
if (isHorizontal) {
|
|
this.buildElHorizontals(originClientRect.left);
|
|
}
|
|
if (isVertical) {
|
|
this.buildElVerticals(originClientRect.top);
|
|
}
|
|
}
|
|
// Populates the left/right internal coordinate arrays
|
|
buildElHorizontals(originClientLeft) {
|
|
let lefts = [];
|
|
let rights = [];
|
|
for (let el of this.els) {
|
|
let rect = el.getBoundingClientRect();
|
|
lefts.push(rect.left - originClientLeft);
|
|
rights.push(rect.right - originClientLeft);
|
|
}
|
|
this.lefts = lefts;
|
|
this.rights = rights;
|
|
}
|
|
// Populates the top/bottom internal coordinate arrays
|
|
buildElVerticals(originClientTop) {
|
|
let tops = [];
|
|
let bottoms = [];
|
|
for (let el of this.els) {
|
|
let rect = el.getBoundingClientRect();
|
|
tops.push(rect.top - originClientTop);
|
|
bottoms.push(rect.bottom - originClientTop);
|
|
}
|
|
this.tops = tops;
|
|
this.bottoms = bottoms;
|
|
}
|
|
// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
|
|
// If no intersection is made, returns undefined.
|
|
leftToIndex(leftPosition) {
|
|
let { lefts, rights } = this;
|
|
let len = lefts.length;
|
|
let i;
|
|
for (i = 0; i < len; i += 1) {
|
|
if (leftPosition >= lefts[i] && leftPosition < rights[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return undefined; // TODO: better
|
|
}
|
|
// Given a top offset (from document top), returns the index of the el that it vertically intersects.
|
|
// If no intersection is made, returns undefined.
|
|
topToIndex(topPosition) {
|
|
let { tops, bottoms } = this;
|
|
let len = tops.length;
|
|
let i;
|
|
for (i = 0; i < len; i += 1) {
|
|
if (topPosition >= tops[i] && topPosition < bottoms[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
return undefined; // TODO: better
|
|
}
|
|
// Gets the width of the element at the given index
|
|
getWidth(leftIndex) {
|
|
return this.rights[leftIndex] - this.lefts[leftIndex];
|
|
}
|
|
// Gets the height of the element at the given index
|
|
getHeight(topIndex) {
|
|
return this.bottoms[topIndex] - this.tops[topIndex];
|
|
}
|
|
similarTo(otherCache) {
|
|
return similarNumArrays(this.tops || [], otherCache.tops || []) &&
|
|
similarNumArrays(this.bottoms || [], otherCache.bottoms || []) &&
|
|
similarNumArrays(this.lefts || [], otherCache.lefts || []) &&
|
|
similarNumArrays(this.rights || [], otherCache.rights || []);
|
|
}
|
|
}
|
|
function similarNumArrays(a, b) {
|
|
const len = a.length;
|
|
if (len !== b.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < len; i++) {
|
|
if (Math.round(a[i]) !== Math.round(b[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* eslint max-classes-per-file: "off" */
|
|
/*
|
|
An object for getting/setting scroll-related information for an element.
|
|
Internally, this is done very differently for window versus DOM element,
|
|
so this object serves as a common interface.
|
|
*/
|
|
class ScrollController {
|
|
getMaxScrollTop() {
|
|
return this.getScrollHeight() - this.getClientHeight();
|
|
}
|
|
getMaxScrollLeft() {
|
|
return this.getScrollWidth() - this.getClientWidth();
|
|
}
|
|
canScrollVertically() {
|
|
return this.getMaxScrollTop() > 0;
|
|
}
|
|
canScrollHorizontally() {
|
|
return this.getMaxScrollLeft() > 0;
|
|
}
|
|
canScrollUp() {
|
|
return this.getScrollTop() > 0;
|
|
}
|
|
canScrollDown() {
|
|
return this.getScrollTop() < this.getMaxScrollTop();
|
|
}
|
|
canScrollLeft() {
|
|
return this.getScrollLeft() > 0;
|
|
}
|
|
canScrollRight() {
|
|
return this.getScrollLeft() < this.getMaxScrollLeft();
|
|
}
|
|
}
|
|
class ElementScrollController extends ScrollController {
|
|
constructor(el) {
|
|
super();
|
|
this.el = el;
|
|
}
|
|
getScrollTop() {
|
|
return this.el.scrollTop;
|
|
}
|
|
getScrollLeft() {
|
|
return this.el.scrollLeft;
|
|
}
|
|
setScrollTop(top) {
|
|
this.el.scrollTop = top;
|
|
}
|
|
setScrollLeft(left) {
|
|
this.el.scrollLeft = left;
|
|
}
|
|
getScrollWidth() {
|
|
return this.el.scrollWidth;
|
|
}
|
|
getScrollHeight() {
|
|
return this.el.scrollHeight;
|
|
}
|
|
getClientHeight() {
|
|
return this.el.clientHeight;
|
|
}
|
|
getClientWidth() {
|
|
return this.el.clientWidth;
|
|
}
|
|
}
|
|
class WindowScrollController extends ScrollController {
|
|
getScrollTop() {
|
|
return window.scrollY;
|
|
}
|
|
getScrollLeft() {
|
|
return window.scrollX;
|
|
}
|
|
setScrollTop(n) {
|
|
window.scroll(window.scrollX, n);
|
|
}
|
|
setScrollLeft(n) {
|
|
window.scroll(n, window.scrollY);
|
|
}
|
|
getScrollWidth() {
|
|
return document.documentElement.scrollWidth;
|
|
}
|
|
getScrollHeight() {
|
|
return document.documentElement.scrollHeight;
|
|
}
|
|
getClientHeight() {
|
|
return document.documentElement.clientHeight;
|
|
}
|
|
getClientWidth() {
|
|
return document.documentElement.clientWidth;
|
|
}
|
|
}
|
|
|
|
/*
|
|
an INTERACTABLE date component
|
|
|
|
PURPOSES:
|
|
- hook up to fg, fill, and mirror renderers
|
|
- interface for dragging and hits
|
|
*/
|
|
class DateComponent extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.uid = guid();
|
|
}
|
|
// Hit System
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
prepareHits() {
|
|
}
|
|
queryHit(positionLeft, positionTop, elWidth, elHeight) {
|
|
return null; // this should be abstract
|
|
}
|
|
// Pointer Interaction Utils
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
isValidSegDownEl(el) {
|
|
return !this.props.eventDrag && // HACK
|
|
!this.props.eventResize && // HACK
|
|
!elementClosest(el, '.fc-event-mirror');
|
|
}
|
|
isValidDateDownEl(el) {
|
|
return !elementClosest(el, '.fc-event:not(.fc-bg-event)') &&
|
|
!elementClosest(el, '.fc-more-link') && // a "more.." link
|
|
!elementClosest(el, 'a[data-navlink]') && // a clickable nav link
|
|
!elementClosest(el, '.fc-popover'); // hack
|
|
}
|
|
}
|
|
|
|
class NamedTimeZoneImpl {
|
|
constructor(timeZoneName) {
|
|
this.timeZoneName = timeZoneName;
|
|
}
|
|
}
|
|
|
|
class SegHierarchy {
|
|
constructor(getEntryThickness = (entry) => {
|
|
// if no thickness known, assume 1 (if 0, so small it always fits)
|
|
return entry.thickness || 1;
|
|
}) {
|
|
this.getEntryThickness = getEntryThickness;
|
|
// settings
|
|
this.strictOrder = false;
|
|
this.allowReslicing = false;
|
|
this.maxCoord = -1; // -1 means no max
|
|
this.maxStackCnt = -1; // -1 means no max
|
|
this.levelCoords = []; // ordered
|
|
this.entriesByLevel = []; // parallel with levelCoords
|
|
this.stackCnts = {}; // TODO: use better technique!?
|
|
}
|
|
addSegs(inputs) {
|
|
let hiddenEntries = [];
|
|
for (let input of inputs) {
|
|
this.insertEntry(input, hiddenEntries);
|
|
}
|
|
return hiddenEntries;
|
|
}
|
|
insertEntry(entry, hiddenEntries) {
|
|
let insertion = this.findInsertion(entry);
|
|
if (this.isInsertionValid(insertion, entry)) {
|
|
this.insertEntryAt(entry, insertion);
|
|
}
|
|
else {
|
|
this.handleInvalidInsertion(insertion, entry, hiddenEntries);
|
|
}
|
|
}
|
|
isInsertionValid(insertion, entry) {
|
|
return (this.maxCoord === -1 || insertion.levelCoord + this.getEntryThickness(entry) <= this.maxCoord) &&
|
|
(this.maxStackCnt === -1 || insertion.stackCnt < this.maxStackCnt);
|
|
}
|
|
handleInvalidInsertion(insertion, entry, hiddenEntries) {
|
|
if (this.allowReslicing && insertion.touchingEntry) {
|
|
const hiddenEntry = Object.assign(Object.assign({}, entry), { span: intersectSpans(entry.span, insertion.touchingEntry.span) });
|
|
hiddenEntries.push(hiddenEntry);
|
|
this.splitEntry(entry, insertion.touchingEntry, hiddenEntries);
|
|
}
|
|
else {
|
|
hiddenEntries.push(entry);
|
|
}
|
|
}
|
|
/*
|
|
Does NOT add what hit the `barrier` into hiddenEntries. Should already be done.
|
|
*/
|
|
splitEntry(entry, barrier, hiddenEntries) {
|
|
let entrySpan = entry.span;
|
|
let barrierSpan = barrier.span;
|
|
if (entrySpan.start < barrierSpan.start) {
|
|
this.insertEntry({
|
|
index: entry.index,
|
|
thickness: entry.thickness,
|
|
span: { start: entrySpan.start, end: barrierSpan.start },
|
|
}, hiddenEntries);
|
|
}
|
|
if (entrySpan.end > barrierSpan.end) {
|
|
this.insertEntry({
|
|
index: entry.index,
|
|
thickness: entry.thickness,
|
|
span: { start: barrierSpan.end, end: entrySpan.end },
|
|
}, hiddenEntries);
|
|
}
|
|
}
|
|
insertEntryAt(entry, insertion) {
|
|
let { entriesByLevel, levelCoords } = this;
|
|
if (insertion.lateral === -1) {
|
|
// create a new level
|
|
insertAt(levelCoords, insertion.level, insertion.levelCoord);
|
|
insertAt(entriesByLevel, insertion.level, [entry]);
|
|
}
|
|
else {
|
|
// insert into existing level
|
|
insertAt(entriesByLevel[insertion.level], insertion.lateral, entry);
|
|
}
|
|
this.stackCnts[buildEntryKey(entry)] = insertion.stackCnt;
|
|
}
|
|
/*
|
|
does not care about limits
|
|
*/
|
|
findInsertion(newEntry) {
|
|
let { levelCoords, entriesByLevel, strictOrder, stackCnts } = this;
|
|
let levelCnt = levelCoords.length;
|
|
let candidateCoord = 0;
|
|
let touchingLevel = -1;
|
|
let touchingLateral = -1;
|
|
let touchingEntry = null;
|
|
let stackCnt = 0;
|
|
for (let trackingLevel = 0; trackingLevel < levelCnt; trackingLevel += 1) {
|
|
const trackingCoord = levelCoords[trackingLevel];
|
|
// if the current level is past the placed entry, we have found a good empty space and can stop.
|
|
// if strictOrder, keep finding more lateral intersections.
|
|
if (!strictOrder && trackingCoord >= candidateCoord + this.getEntryThickness(newEntry)) {
|
|
break;
|
|
}
|
|
let trackingEntries = entriesByLevel[trackingLevel];
|
|
let trackingEntry;
|
|
let searchRes = internal_common_binarySearch(trackingEntries, newEntry.span.start, getEntrySpanEnd); // find first entry after newEntry's end
|
|
let lateralIndex = searchRes[0] + searchRes[1]; // if exact match (which doesn't collide), go to next one
|
|
while ( // loop through entries that horizontally intersect
|
|
(trackingEntry = trackingEntries[lateralIndex]) && // but not past the whole entry list
|
|
trackingEntry.span.start < newEntry.span.end // and not entirely past newEntry
|
|
) {
|
|
let trackingEntryBottom = trackingCoord + this.getEntryThickness(trackingEntry);
|
|
// intersects into the top of the candidate?
|
|
if (trackingEntryBottom > candidateCoord) {
|
|
candidateCoord = trackingEntryBottom;
|
|
touchingEntry = trackingEntry;
|
|
touchingLevel = trackingLevel;
|
|
touchingLateral = lateralIndex;
|
|
}
|
|
// butts up against top of candidate? (will happen if just intersected as well)
|
|
if (trackingEntryBottom === candidateCoord) {
|
|
// accumulate the highest possible stackCnt of the trackingEntries that butt up
|
|
stackCnt = Math.max(stackCnt, stackCnts[buildEntryKey(trackingEntry)] + 1);
|
|
}
|
|
lateralIndex += 1;
|
|
}
|
|
}
|
|
// the destination level will be after touchingEntry's level. find it
|
|
let destLevel = 0;
|
|
if (touchingEntry) {
|
|
destLevel = touchingLevel + 1;
|
|
while (destLevel < levelCnt && levelCoords[destLevel] < candidateCoord) {
|
|
destLevel += 1;
|
|
}
|
|
}
|
|
// if adding to an existing level, find where to insert
|
|
let destLateral = -1;
|
|
if (destLevel < levelCnt && levelCoords[destLevel] === candidateCoord) {
|
|
destLateral = internal_common_binarySearch(entriesByLevel[destLevel], newEntry.span.end, getEntrySpanEnd)[0];
|
|
}
|
|
return {
|
|
touchingLevel,
|
|
touchingLateral,
|
|
touchingEntry,
|
|
stackCnt,
|
|
levelCoord: candidateCoord,
|
|
level: destLevel,
|
|
lateral: destLateral,
|
|
};
|
|
}
|
|
// sorted by levelCoord (lowest to highest)
|
|
toRects() {
|
|
let { entriesByLevel, levelCoords } = this;
|
|
let levelCnt = entriesByLevel.length;
|
|
let rects = [];
|
|
for (let level = 0; level < levelCnt; level += 1) {
|
|
let entries = entriesByLevel[level];
|
|
let levelCoord = levelCoords[level];
|
|
for (let entry of entries) {
|
|
rects.push(Object.assign(Object.assign({}, entry), { thickness: this.getEntryThickness(entry), levelCoord }));
|
|
}
|
|
}
|
|
return rects;
|
|
}
|
|
}
|
|
function getEntrySpanEnd(entry) {
|
|
return entry.span.end;
|
|
}
|
|
function buildEntryKey(entry) {
|
|
return entry.index + ':' + entry.span.start;
|
|
}
|
|
// returns groups with entries sorted by input order
|
|
function groupIntersectingEntries(entries) {
|
|
let merges = [];
|
|
for (let entry of entries) {
|
|
let filteredMerges = [];
|
|
let hungryMerge = {
|
|
span: entry.span,
|
|
entries: [entry],
|
|
};
|
|
for (let merge of merges) {
|
|
if (intersectSpans(merge.span, hungryMerge.span)) {
|
|
hungryMerge = {
|
|
entries: merge.entries.concat(hungryMerge.entries),
|
|
span: joinSpans(merge.span, hungryMerge.span),
|
|
};
|
|
}
|
|
else {
|
|
filteredMerges.push(merge);
|
|
}
|
|
}
|
|
filteredMerges.push(hungryMerge);
|
|
merges = filteredMerges;
|
|
}
|
|
return merges;
|
|
}
|
|
function joinSpans(span0, span1) {
|
|
return {
|
|
start: Math.min(span0.start, span1.start),
|
|
end: Math.max(span0.end, span1.end),
|
|
};
|
|
}
|
|
function intersectSpans(span0, span1) {
|
|
let start = Math.max(span0.start, span1.start);
|
|
let end = Math.min(span0.end, span1.end);
|
|
if (start < end) {
|
|
return { start, end };
|
|
}
|
|
return null;
|
|
}
|
|
// general util
|
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
function insertAt(arr, index, item) {
|
|
arr.splice(index, 0, item);
|
|
}
|
|
function internal_common_binarySearch(a, searchVal, getItemVal) {
|
|
let startIndex = 0;
|
|
let endIndex = a.length; // exclusive
|
|
if (!endIndex || searchVal < getItemVal(a[startIndex])) { // no items OR before first item
|
|
return [0, 0];
|
|
}
|
|
if (searchVal > getItemVal(a[endIndex - 1])) { // after last item
|
|
return [endIndex, 0];
|
|
}
|
|
while (startIndex < endIndex) {
|
|
let middleIndex = Math.floor(startIndex + (endIndex - startIndex) / 2);
|
|
let middleVal = getItemVal(a[middleIndex]);
|
|
if (searchVal < middleVal) {
|
|
endIndex = middleIndex;
|
|
}
|
|
else if (searchVal > middleVal) {
|
|
startIndex = middleIndex + 1;
|
|
}
|
|
else { // equal!
|
|
return [middleIndex, 1];
|
|
}
|
|
}
|
|
return [startIndex, 0];
|
|
}
|
|
|
|
/*
|
|
An abstraction for a dragging interaction originating on an event.
|
|
Does higher-level things than PointerDragger, such as possibly:
|
|
- a "mirror" that moves with the pointer
|
|
- a minimum number of pixels or other criteria for a true drag to begin
|
|
|
|
subclasses must emit:
|
|
- pointerdown
|
|
- dragstart
|
|
- dragmove
|
|
- pointerup
|
|
- dragend
|
|
*/
|
|
class ElementDragging {
|
|
constructor(el, selector) {
|
|
this.emitter = new Emitter();
|
|
}
|
|
destroy() {
|
|
}
|
|
setMirrorIsVisible(bool) {
|
|
// optional if subclass doesn't want to support a mirror
|
|
}
|
|
setMirrorNeedsRevert(bool) {
|
|
// optional if subclass doesn't want to support a mirror
|
|
}
|
|
setAutoScrollEnabled(bool) {
|
|
// optional
|
|
}
|
|
}
|
|
|
|
// TODO: get rid of this in favor of options system,
|
|
// tho it's really easy to access this globally rather than pass thru options.
|
|
const config = {};
|
|
|
|
/*
|
|
Information about what will happen when an external element is dragged-and-dropped
|
|
onto a calendar. Contains information for creating an event.
|
|
*/
|
|
const DRAG_META_REFINERS = {
|
|
startTime: createDuration,
|
|
duration: createDuration,
|
|
create: Boolean,
|
|
sourceId: String,
|
|
};
|
|
function parseDragMeta(raw) {
|
|
let { refined, extra } = refineProps(raw, DRAG_META_REFINERS);
|
|
return {
|
|
startTime: refined.startTime || null,
|
|
duration: refined.duration || null,
|
|
create: refined.create != null ? refined.create : true,
|
|
sourceId: refined.sourceId,
|
|
leftoverProps: extra,
|
|
};
|
|
}
|
|
|
|
// Computes a default column header formatting string if `colFormat` is not explicitly defined
|
|
function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) {
|
|
// if more than one week row, or if there are a lot of columns with not much space,
|
|
// put just the day numbers will be in each cell
|
|
if (!datesRepDistinctDays || dayCnt > 10) {
|
|
return createFormatter({ weekday: 'short' }); // "Sat"
|
|
}
|
|
if (dayCnt > 1) {
|
|
return createFormatter({ weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }); // "Sat 11/12"
|
|
}
|
|
return createFormatter({ weekday: 'long' }); // "Saturday"
|
|
}
|
|
|
|
const CLASS_NAME = 'fc-col-header-cell'; // do the cushion too? no
|
|
function renderInner$1(renderProps) {
|
|
return renderProps.text;
|
|
}
|
|
|
|
// BAD name for this class now. used in the Header
|
|
class TableDateCell extends BaseComponent {
|
|
render() {
|
|
let { dateEnv, options, theme, viewApi } = this.context;
|
|
let { props } = this;
|
|
let { date, dateProfile } = props;
|
|
let dayMeta = getDateMeta(date, props.todayRange, null, dateProfile);
|
|
let classNames = [CLASS_NAME].concat(getDayClassNames(dayMeta, theme));
|
|
let text = dateEnv.format(date, props.dayHeaderFormat);
|
|
// if colCnt is 1, we are already in a day-view and don't need a navlink
|
|
let navLinkAttrs = (!dayMeta.isDisabled && props.colCnt > 1)
|
|
? buildNavLinkAttrs(this.context, date)
|
|
: {};
|
|
let publicDate = dateEnv.toDate(date);
|
|
// workaround for Luxon (and maybe moment) returning prior-days when start-of-day
|
|
// in DST gap: https://github.com/fullcalendar/fullcalendar/issues/7633
|
|
if (dateEnv.namedTimeZoneImpl) {
|
|
publicDate = addMs(publicDate, 3600000); // add an hour
|
|
}
|
|
let renderProps = Object.assign(Object.assign(Object.assign({ date: publicDate, view: viewApi }, props.extraRenderProps), { text }), dayMeta);
|
|
return (y(ContentContainer, { elTag: "th", elClasses: classNames, elAttrs: Object.assign({ role: 'columnheader', colSpan: props.colSpan, 'data-date': !dayMeta.isDisabled ? formatDayString(date) : undefined }, props.extraDataAttrs), renderProps: renderProps, generatorName: "dayHeaderContent", customGenerator: options.dayHeaderContent, defaultGenerator: renderInner$1, classNameGenerator: options.dayHeaderClassNames, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, (InnerContainer) => (y("div", { className: "fc-scrollgrid-sync-inner" }, !dayMeta.isDisabled && (y(InnerContainer, { elTag: "a", elAttrs: navLinkAttrs, elClasses: [
|
|
'fc-col-header-cell-cushion',
|
|
props.isSticky && 'fc-sticky',
|
|
] }))))));
|
|
}
|
|
}
|
|
|
|
const WEEKDAY_FORMAT = createFormatter({ weekday: 'long' });
|
|
class TableDowCell extends BaseComponent {
|
|
render() {
|
|
let { props } = this;
|
|
let { dateEnv, theme, viewApi, options } = this.context;
|
|
let date = addDays(new Date(259200000), props.dow); // start with Sun, 04 Jan 1970 00:00:00 GMT
|
|
let dateMeta = {
|
|
dow: props.dow,
|
|
isDisabled: false,
|
|
isFuture: false,
|
|
isPast: false,
|
|
isToday: false,
|
|
isOther: false,
|
|
};
|
|
let text = dateEnv.format(date, props.dayHeaderFormat);
|
|
let renderProps = Object.assign(Object.assign(Object.assign(Object.assign({ // TODO: make this public?
|
|
date }, dateMeta), { view: viewApi }), props.extraRenderProps), { text });
|
|
return (y(ContentContainer, { elTag: "th", elClasses: [
|
|
CLASS_NAME,
|
|
...getDayClassNames(dateMeta, theme),
|
|
...(props.extraClassNames || []),
|
|
], elAttrs: Object.assign({ role: 'columnheader', colSpan: props.colSpan }, props.extraDataAttrs), renderProps: renderProps, generatorName: "dayHeaderContent", customGenerator: options.dayHeaderContent, defaultGenerator: renderInner$1, classNameGenerator: options.dayHeaderClassNames, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, (InnerContent) => (y("div", { className: "fc-scrollgrid-sync-inner" },
|
|
y(InnerContent, { elTag: "a", elClasses: [
|
|
'fc-col-header-cell-cushion',
|
|
props.isSticky && 'fc-sticky',
|
|
], elAttrs: {
|
|
'aria-label': dateEnv.format(date, WEEKDAY_FORMAT),
|
|
} })))));
|
|
}
|
|
}
|
|
|
|
class DayHeader extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.createDayHeaderFormatter = memoize(createDayHeaderFormatter);
|
|
}
|
|
render() {
|
|
let { context } = this;
|
|
let { dates, dateProfile, datesRepDistinctDays, renderIntro } = this.props;
|
|
let dayHeaderFormat = this.createDayHeaderFormatter(context.options.dayHeaderFormat, datesRepDistinctDays, dates.length);
|
|
return (y(NowTimer, { unit: "day" }, (nowDate, todayRange) => (y("tr", { role: "row" },
|
|
renderIntro && renderIntro('day'),
|
|
dates.map((date) => (datesRepDistinctDays ? (y(TableDateCell, { key: date.toISOString(), date: date, dateProfile: dateProfile, todayRange: todayRange, colCnt: dates.length, dayHeaderFormat: dayHeaderFormat })) : (y(TableDowCell, { key: date.getUTCDay(), dow: date.getUTCDay(), dayHeaderFormat: dayHeaderFormat }))))))));
|
|
}
|
|
}
|
|
function createDayHeaderFormatter(explicitFormat, datesRepDistinctDays, dateCnt) {
|
|
return explicitFormat || computeFallbackHeaderFormat(datesRepDistinctDays, dateCnt);
|
|
}
|
|
|
|
class DaySeriesModel {
|
|
constructor(range, dateProfileGenerator) {
|
|
let date = range.start;
|
|
let { end } = range;
|
|
let indices = [];
|
|
let dates = [];
|
|
let dayIndex = -1;
|
|
while (date < end) { // loop each day from start to end
|
|
if (dateProfileGenerator.isHiddenDay(date)) {
|
|
indices.push(dayIndex + 0.5); // mark that it's between indices
|
|
}
|
|
else {
|
|
dayIndex += 1;
|
|
indices.push(dayIndex);
|
|
dates.push(date);
|
|
}
|
|
date = addDays(date, 1);
|
|
}
|
|
this.dates = dates;
|
|
this.indices = indices;
|
|
this.cnt = dates.length;
|
|
}
|
|
sliceRange(range) {
|
|
let firstIndex = this.getDateDayIndex(range.start); // inclusive first index
|
|
let lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index
|
|
let clippedFirstIndex = Math.max(0, firstIndex);
|
|
let clippedLastIndex = Math.min(this.cnt - 1, lastIndex);
|
|
// deal with in-between indices
|
|
clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell
|
|
clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell
|
|
if (clippedFirstIndex <= clippedLastIndex) {
|
|
return {
|
|
firstIndex: clippedFirstIndex,
|
|
lastIndex: clippedLastIndex,
|
|
isStart: firstIndex === clippedFirstIndex,
|
|
isEnd: lastIndex === clippedLastIndex,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
// Given a date, returns its chronolocial cell-index from the first cell of the grid.
|
|
// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
|
|
// If before the first offset, returns a negative number.
|
|
// If after the last offset, returns an offset past the last cell offset.
|
|
// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
|
|
getDateDayIndex(date) {
|
|
let { indices } = this;
|
|
let dayOffset = Math.floor(diffDays(this.dates[0], date));
|
|
if (dayOffset < 0) {
|
|
return indices[0] - 1;
|
|
}
|
|
if (dayOffset >= indices.length) {
|
|
return indices[indices.length - 1] + 1;
|
|
}
|
|
return indices[dayOffset];
|
|
}
|
|
}
|
|
|
|
class DayTableModel {
|
|
constructor(daySeries, breakOnWeeks) {
|
|
let { dates } = daySeries;
|
|
let daysPerRow;
|
|
let firstDay;
|
|
let rowCnt;
|
|
if (breakOnWeeks) {
|
|
// count columns until the day-of-week repeats
|
|
firstDay = dates[0].getUTCDay();
|
|
for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow += 1) {
|
|
if (dates[daysPerRow].getUTCDay() === firstDay) {
|
|
break;
|
|
}
|
|
}
|
|
rowCnt = Math.ceil(dates.length / daysPerRow);
|
|
}
|
|
else {
|
|
rowCnt = 1;
|
|
daysPerRow = dates.length;
|
|
}
|
|
this.rowCnt = rowCnt;
|
|
this.colCnt = daysPerRow;
|
|
this.daySeries = daySeries;
|
|
this.cells = this.buildCells();
|
|
this.headerDates = this.buildHeaderDates();
|
|
}
|
|
buildCells() {
|
|
let rows = [];
|
|
for (let row = 0; row < this.rowCnt; row += 1) {
|
|
let cells = [];
|
|
for (let col = 0; col < this.colCnt; col += 1) {
|
|
cells.push(this.buildCell(row, col));
|
|
}
|
|
rows.push(cells);
|
|
}
|
|
return rows;
|
|
}
|
|
buildCell(row, col) {
|
|
let date = this.daySeries.dates[row * this.colCnt + col];
|
|
return {
|
|
key: date.toISOString(),
|
|
date,
|
|
};
|
|
}
|
|
buildHeaderDates() {
|
|
let dates = [];
|
|
for (let col = 0; col < this.colCnt; col += 1) {
|
|
dates.push(this.cells[0][col].date);
|
|
}
|
|
return dates;
|
|
}
|
|
sliceRange(range) {
|
|
let { colCnt } = this;
|
|
let seriesSeg = this.daySeries.sliceRange(range);
|
|
let segs = [];
|
|
if (seriesSeg) {
|
|
let { firstIndex, lastIndex } = seriesSeg;
|
|
let index = firstIndex;
|
|
while (index <= lastIndex) {
|
|
let row = Math.floor(index / colCnt);
|
|
let nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1);
|
|
segs.push({
|
|
row,
|
|
firstCol: index % colCnt,
|
|
lastCol: (nextIndex - 1) % colCnt,
|
|
isStart: seriesSeg.isStart && index === firstIndex,
|
|
isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex,
|
|
});
|
|
index = nextIndex;
|
|
}
|
|
}
|
|
return segs;
|
|
}
|
|
}
|
|
|
|
class Slicer {
|
|
constructor() {
|
|
this.sliceBusinessHours = memoize(this._sliceBusinessHours);
|
|
this.sliceDateSelection = memoize(this._sliceDateSpan);
|
|
this.sliceEventStore = memoize(this._sliceEventStore);
|
|
this.sliceEventDrag = memoize(this._sliceInteraction);
|
|
this.sliceEventResize = memoize(this._sliceInteraction);
|
|
this.forceDayIfListItem = false; // hack
|
|
}
|
|
sliceProps(props, dateProfile, nextDayThreshold, context, ...extraArgs) {
|
|
let { eventUiBases } = props;
|
|
let eventSegs = this.sliceEventStore(props.eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs);
|
|
return {
|
|
dateSelectionSegs: this.sliceDateSelection(props.dateSelection, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs),
|
|
businessHourSegs: this.sliceBusinessHours(props.businessHours, dateProfile, nextDayThreshold, context, ...extraArgs),
|
|
fgEventSegs: eventSegs.fg,
|
|
bgEventSegs: eventSegs.bg,
|
|
eventDrag: this.sliceEventDrag(props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
|
|
eventResize: this.sliceEventResize(props.eventResize, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs),
|
|
eventSelection: props.eventSelection,
|
|
}; // TODO: give interactionSegs?
|
|
}
|
|
sliceNowDate(// does not memoize
|
|
date, dateProfile, nextDayThreshold, context, ...extraArgs) {
|
|
return this._sliceDateSpan({ range: { start: date, end: addMs(date, 1) }, allDay: false }, // add 1 ms, protect against null range
|
|
dateProfile, nextDayThreshold, {}, context, ...extraArgs);
|
|
}
|
|
_sliceBusinessHours(businessHours, dateProfile, nextDayThreshold, context, ...extraArgs) {
|
|
if (!businessHours) {
|
|
return [];
|
|
}
|
|
return this._sliceEventStore(expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), context), {}, dateProfile, nextDayThreshold, ...extraArgs).bg;
|
|
}
|
|
_sliceEventStore(eventStore, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
|
|
if (eventStore) {
|
|
let rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
|
|
return {
|
|
bg: this.sliceEventRanges(rangeRes.bg, extraArgs),
|
|
fg: this.sliceEventRanges(rangeRes.fg, extraArgs),
|
|
};
|
|
}
|
|
return { bg: [], fg: [] };
|
|
}
|
|
_sliceInteraction(interaction, eventUiBases, dateProfile, nextDayThreshold, ...extraArgs) {
|
|
if (!interaction) {
|
|
return null;
|
|
}
|
|
let rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold);
|
|
return {
|
|
segs: this.sliceEventRanges(rangeRes.fg, extraArgs),
|
|
affectedInstances: interaction.affectedEvents.instances,
|
|
isEvent: interaction.isEvent,
|
|
};
|
|
}
|
|
_sliceDateSpan(dateSpan, dateProfile, nextDayThreshold, eventUiBases, context, ...extraArgs) {
|
|
if (!dateSpan) {
|
|
return [];
|
|
}
|
|
let activeRange = computeActiveRange(dateProfile, Boolean(nextDayThreshold));
|
|
let activeDateSpanRange = intersectRanges(dateSpan.range, activeRange);
|
|
if (activeDateSpanRange) {
|
|
dateSpan = Object.assign(Object.assign({}, dateSpan), { range: activeDateSpanRange });
|
|
let eventRange = fabricateEventRange(dateSpan, eventUiBases, context);
|
|
let segs = this.sliceRange(dateSpan.range, ...extraArgs);
|
|
for (let seg of segs) {
|
|
seg.eventRange = eventRange;
|
|
}
|
|
return segs;
|
|
}
|
|
return [];
|
|
}
|
|
/*
|
|
"complete" seg means it has component and eventRange
|
|
*/
|
|
sliceEventRanges(eventRanges, extraArgs) {
|
|
let segs = [];
|
|
for (let eventRange of eventRanges) {
|
|
segs.push(...this.sliceEventRange(eventRange, extraArgs));
|
|
}
|
|
return segs;
|
|
}
|
|
/*
|
|
"complete" seg means it has component and eventRange
|
|
*/
|
|
sliceEventRange(eventRange, extraArgs) {
|
|
let dateRange = eventRange.range;
|
|
// hack to make multi-day events that are being force-displayed as list-items to take up only one day
|
|
if (this.forceDayIfListItem && eventRange.ui.display === 'list-item') {
|
|
dateRange = {
|
|
start: dateRange.start,
|
|
end: addDays(dateRange.start, 1),
|
|
};
|
|
}
|
|
let segs = this.sliceRange(dateRange, ...extraArgs);
|
|
for (let seg of segs) {
|
|
seg.eventRange = eventRange;
|
|
seg.isStart = eventRange.isStart && seg.isStart;
|
|
seg.isEnd = eventRange.isEnd && seg.isEnd;
|
|
}
|
|
return segs;
|
|
}
|
|
}
|
|
/*
|
|
for incorporating slotMinTime/slotMaxTime if appropriate
|
|
TODO: should be part of DateProfile!
|
|
TimelineDateProfile already does this btw
|
|
*/
|
|
function computeActiveRange(dateProfile, isComponentAllDay) {
|
|
let range = dateProfile.activeRange;
|
|
if (isComponentAllDay) {
|
|
return range;
|
|
}
|
|
return {
|
|
start: addMs(range.start, dateProfile.slotMinTime.milliseconds),
|
|
end: addMs(range.end, dateProfile.slotMaxTime.milliseconds - 864e5), // 864e5 = ms in a day
|
|
};
|
|
}
|
|
|
|
// high-level segmenting-aware tester functions
|
|
// ------------------------------------------------------------------------------------------------------------------------
|
|
function isInteractionValid(interaction, dateProfile, context) {
|
|
let { instances } = interaction.mutatedEvents;
|
|
for (let instanceId in instances) {
|
|
if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
|
|
return false;
|
|
}
|
|
}
|
|
return isNewPropsValid({ eventDrag: interaction }, context); // HACK: the eventDrag props is used for ALL interactions
|
|
}
|
|
function isDateSelectionValid(dateSelection, dateProfile, context) {
|
|
if (!rangeContainsRange(dateProfile.validRange, dateSelection.range)) {
|
|
return false;
|
|
}
|
|
return isNewPropsValid({ dateSelection }, context);
|
|
}
|
|
function isNewPropsValid(newProps, context) {
|
|
let calendarState = context.getCurrentData();
|
|
let props = Object.assign({ businessHours: calendarState.businessHours, dateSelection: '', eventStore: calendarState.eventStore, eventUiBases: calendarState.eventUiBases, eventSelection: '', eventDrag: null, eventResize: null }, newProps);
|
|
return (context.pluginHooks.isPropsValid || isPropsValid)(props, context);
|
|
}
|
|
function isPropsValid(state, context, dateSpanMeta = {}, filterConfig) {
|
|
if (state.eventDrag && !isInteractionPropsValid(state, context, dateSpanMeta, filterConfig)) {
|
|
return false;
|
|
}
|
|
if (state.dateSelection && !isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
// Moving Event Validation
|
|
// ------------------------------------------------------------------------------------------------------------------------
|
|
function isInteractionPropsValid(state, context, dateSpanMeta, filterConfig) {
|
|
let currentState = context.getCurrentData();
|
|
let interaction = state.eventDrag; // HACK: the eventDrag props is used for ALL interactions
|
|
let subjectEventStore = interaction.mutatedEvents;
|
|
let subjectDefs = subjectEventStore.defs;
|
|
let subjectInstances = subjectEventStore.instances;
|
|
let subjectConfigs = compileEventUis(subjectDefs, interaction.isEvent ?
|
|
state.eventUiBases :
|
|
{ '': currentState.selectionConfig });
|
|
if (filterConfig) {
|
|
subjectConfigs = mapHash(subjectConfigs, filterConfig);
|
|
}
|
|
// exclude the subject events. TODO: exclude defs too?
|
|
let otherEventStore = excludeInstances(state.eventStore, interaction.affectedEvents.instances);
|
|
let otherDefs = otherEventStore.defs;
|
|
let otherInstances = otherEventStore.instances;
|
|
let otherConfigs = compileEventUis(otherDefs, state.eventUiBases);
|
|
for (let subjectInstanceId in subjectInstances) {
|
|
let subjectInstance = subjectInstances[subjectInstanceId];
|
|
let subjectRange = subjectInstance.range;
|
|
let subjectConfig = subjectConfigs[subjectInstance.defId];
|
|
let subjectDef = subjectDefs[subjectInstance.defId];
|
|
// constraint
|
|
if (!allConstraintsPass(subjectConfig.constraints, subjectRange, otherEventStore, state.businessHours, context)) {
|
|
return false;
|
|
}
|
|
// overlap
|
|
let { eventOverlap } = context.options;
|
|
let eventOverlapFunc = typeof eventOverlap === 'function' ? eventOverlap : null;
|
|
for (let otherInstanceId in otherInstances) {
|
|
let otherInstance = otherInstances[otherInstanceId];
|
|
// intersect! evaluate
|
|
if (rangesIntersect(subjectRange, otherInstance.range)) {
|
|
let otherOverlap = otherConfigs[otherInstance.defId].overlap;
|
|
// consider the other event's overlap. only do this if the subject event is a "real" event
|
|
if (otherOverlap === false && interaction.isEvent) {
|
|
return false;
|
|
}
|
|
if (subjectConfig.overlap === false) {
|
|
return false;
|
|
}
|
|
if (eventOverlapFunc && !eventOverlapFunc(new EventImpl(context, otherDefs[otherInstance.defId], otherInstance), // still event
|
|
new EventImpl(context, subjectDef, subjectInstance))) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// allow (a function)
|
|
let calendarEventStore = currentState.eventStore; // need global-to-calendar, not local to component (splittable)state
|
|
for (let subjectAllow of subjectConfig.allows) {
|
|
let subjectDateSpan = Object.assign(Object.assign({}, dateSpanMeta), { range: subjectInstance.range, allDay: subjectDef.allDay });
|
|
let origDef = calendarEventStore.defs[subjectDef.defId];
|
|
let origInstance = calendarEventStore.instances[subjectInstanceId];
|
|
let eventApi;
|
|
if (origDef) { // was previously in the calendar
|
|
eventApi = new EventImpl(context, origDef, origInstance);
|
|
}
|
|
else { // was an external event
|
|
eventApi = new EventImpl(context, subjectDef); // no instance, because had no dates
|
|
}
|
|
if (!subjectAllow(buildDateSpanApiWithContext(subjectDateSpan, context), eventApi)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// Date Selection Validation
|
|
// ------------------------------------------------------------------------------------------------------------------------
|
|
function isDateSelectionPropsValid(state, context, dateSpanMeta, filterConfig) {
|
|
let relevantEventStore = state.eventStore;
|
|
let relevantDefs = relevantEventStore.defs;
|
|
let relevantInstances = relevantEventStore.instances;
|
|
let selection = state.dateSelection;
|
|
let selectionRange = selection.range;
|
|
let { selectionConfig } = context.getCurrentData();
|
|
if (filterConfig) {
|
|
selectionConfig = filterConfig(selectionConfig);
|
|
}
|
|
// constraint
|
|
if (!allConstraintsPass(selectionConfig.constraints, selectionRange, relevantEventStore, state.businessHours, context)) {
|
|
return false;
|
|
}
|
|
// overlap
|
|
let { selectOverlap } = context.options;
|
|
let selectOverlapFunc = typeof selectOverlap === 'function' ? selectOverlap : null;
|
|
for (let relevantInstanceId in relevantInstances) {
|
|
let relevantInstance = relevantInstances[relevantInstanceId];
|
|
// intersect! evaluate
|
|
if (rangesIntersect(selectionRange, relevantInstance.range)) {
|
|
if (selectionConfig.overlap === false) {
|
|
return false;
|
|
}
|
|
if (selectOverlapFunc && !selectOverlapFunc(new EventImpl(context, relevantDefs[relevantInstance.defId], relevantInstance), null)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// allow (a function)
|
|
for (let selectionAllow of selectionConfig.allows) {
|
|
let fullDateSpan = Object.assign(Object.assign({}, dateSpanMeta), selection);
|
|
if (!selectionAllow(buildDateSpanApiWithContext(fullDateSpan, context), null)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// Constraint Utils
|
|
// ------------------------------------------------------------------------------------------------------------------------
|
|
function allConstraintsPass(constraints, subjectRange, otherEventStore, businessHoursUnexpanded, context) {
|
|
for (let constraint of constraints) {
|
|
if (!anyRangesContainRange(constraintToRanges(constraint, subjectRange, otherEventStore, businessHoursUnexpanded, context), subjectRange)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function constraintToRanges(constraint, subjectRange, // for expanding a recurring constraint, or expanding business hours
|
|
otherEventStore, // for if constraint is an even group ID
|
|
businessHoursUnexpanded, // for if constraint is 'businessHours'
|
|
context) {
|
|
if (constraint === 'businessHours') {
|
|
return eventStoreToRanges(expandRecurring(businessHoursUnexpanded, subjectRange, context));
|
|
}
|
|
if (typeof constraint === 'string') { // an group ID
|
|
return eventStoreToRanges(filterEventStoreDefs(otherEventStore, (eventDef) => eventDef.groupId === constraint));
|
|
}
|
|
if (typeof constraint === 'object' && constraint) { // non-null object
|
|
return eventStoreToRanges(expandRecurring(constraint, subjectRange, context));
|
|
}
|
|
return []; // if it's false
|
|
}
|
|
// TODO: move to event-store file?
|
|
function eventStoreToRanges(eventStore) {
|
|
let { instances } = eventStore;
|
|
let ranges = [];
|
|
for (let instanceId in instances) {
|
|
ranges.push(instances[instanceId].range);
|
|
}
|
|
return ranges;
|
|
}
|
|
// TODO: move to geom file?
|
|
function anyRangesContainRange(outerRanges, innerRange) {
|
|
for (let outerRange of outerRanges) {
|
|
if (rangeContainsRange(outerRange, innerRange)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const VISIBLE_HIDDEN_RE = /^(visible|hidden)$/;
|
|
class Scroller extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.handleEl = (el) => {
|
|
this.el = el;
|
|
setRef(this.props.elRef, el);
|
|
};
|
|
}
|
|
render() {
|
|
let { props } = this;
|
|
let { liquid, liquidIsAbsolute } = props;
|
|
let isAbsolute = liquid && liquidIsAbsolute;
|
|
let className = ['fc-scroller'];
|
|
if (liquid) {
|
|
if (liquidIsAbsolute) {
|
|
className.push('fc-scroller-liquid-absolute');
|
|
}
|
|
else {
|
|
className.push('fc-scroller-liquid');
|
|
}
|
|
}
|
|
return (y("div", { ref: this.handleEl, className: className.join(' '), style: {
|
|
overflowX: props.overflowX,
|
|
overflowY: props.overflowY,
|
|
left: (isAbsolute && -(props.overcomeLeft || 0)) || '',
|
|
right: (isAbsolute && -(props.overcomeRight || 0)) || '',
|
|
bottom: (isAbsolute && -(props.overcomeBottom || 0)) || '',
|
|
marginLeft: (!isAbsolute && -(props.overcomeLeft || 0)) || '',
|
|
marginRight: (!isAbsolute && -(props.overcomeRight || 0)) || '',
|
|
marginBottom: (!isAbsolute && -(props.overcomeBottom || 0)) || '',
|
|
maxHeight: props.maxHeight || '',
|
|
} }, props.children));
|
|
}
|
|
needsXScrolling() {
|
|
if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) {
|
|
return false;
|
|
}
|
|
// testing scrollWidth>clientWidth is unreliable cross-browser when pixel heights aren't integers.
|
|
// much more reliable to see if children are taller than the scroller, even tho doesn't account for
|
|
// inner-child margins and absolute positioning
|
|
let { el } = this;
|
|
let realClientWidth = this.el.getBoundingClientRect().width - this.getYScrollbarWidth();
|
|
let { children } = el;
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
let childEl = children[i];
|
|
if (childEl.getBoundingClientRect().width > realClientWidth) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
needsYScrolling() {
|
|
if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) {
|
|
return false;
|
|
}
|
|
// testing scrollHeight>clientHeight is unreliable cross-browser when pixel heights aren't integers.
|
|
// much more reliable to see if children are taller than the scroller, even tho doesn't account for
|
|
// inner-child margins and absolute positioning
|
|
let { el } = this;
|
|
let realClientHeight = this.el.getBoundingClientRect().height - this.getXScrollbarWidth();
|
|
let { children } = el;
|
|
for (let i = 0; i < children.length; i += 1) {
|
|
let childEl = children[i];
|
|
if (childEl.getBoundingClientRect().height > realClientHeight) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
getXScrollbarWidth() {
|
|
if (VISIBLE_HIDDEN_RE.test(this.props.overflowX)) {
|
|
return 0;
|
|
}
|
|
return this.el.offsetHeight - this.el.clientHeight; // only works because we guarantee no borders. TODO: add to CSS with important?
|
|
}
|
|
getYScrollbarWidth() {
|
|
if (VISIBLE_HIDDEN_RE.test(this.props.overflowY)) {
|
|
return 0;
|
|
}
|
|
return this.el.offsetWidth - this.el.clientWidth; // only works because we guarantee no borders. TODO: add to CSS with important?
|
|
}
|
|
}
|
|
|
|
/*
|
|
TODO: somehow infer OtherArgs from masterCallback?
|
|
TODO: infer RefType from masterCallback if provided
|
|
*/
|
|
class RefMap {
|
|
constructor(masterCallback) {
|
|
this.masterCallback = masterCallback;
|
|
this.currentMap = {};
|
|
this.depths = {};
|
|
this.callbackMap = {};
|
|
this.handleValue = (val, key) => {
|
|
let { depths, currentMap } = this;
|
|
let removed = false;
|
|
let added = false;
|
|
if (val !== null) {
|
|
// for bug... ACTUALLY: can probably do away with this now that callers don't share numeric indices anymore
|
|
removed = (key in currentMap);
|
|
currentMap[key] = val;
|
|
depths[key] = (depths[key] || 0) + 1;
|
|
added = true;
|
|
}
|
|
else {
|
|
depths[key] -= 1;
|
|
if (!depths[key]) {
|
|
delete currentMap[key];
|
|
delete this.callbackMap[key];
|
|
removed = true;
|
|
}
|
|
}
|
|
if (this.masterCallback) {
|
|
if (removed) {
|
|
this.masterCallback(null, String(key));
|
|
}
|
|
if (added) {
|
|
this.masterCallback(val, String(key));
|
|
}
|
|
}
|
|
};
|
|
}
|
|
createRef(key) {
|
|
let refCallback = this.callbackMap[key];
|
|
if (!refCallback) {
|
|
refCallback = this.callbackMap[key] = (val) => {
|
|
this.handleValue(val, String(key));
|
|
};
|
|
}
|
|
return refCallback;
|
|
}
|
|
// TODO: check callers that don't care about order. should use getAll instead
|
|
// NOTE: this method has become less valuable now that we are encouraged to map order by some other index
|
|
// TODO: provide ONE array-export function, buildArray, which fails on non-numeric indexes. caller can manipulate and "collect"
|
|
collect(startIndex, endIndex, step) {
|
|
return collectFromHash(this.currentMap, startIndex, endIndex, step);
|
|
}
|
|
getAll() {
|
|
return hashValuesToArray(this.currentMap);
|
|
}
|
|
}
|
|
|
|
function computeShrinkWidth(chunkEls) {
|
|
let shrinkCells = findElements(chunkEls, '.fc-scrollgrid-shrink');
|
|
let largestWidth = 0;
|
|
for (let shrinkCell of shrinkCells) {
|
|
largestWidth = Math.max(largestWidth, computeSmallestCellWidth(shrinkCell));
|
|
}
|
|
return Math.ceil(largestWidth); // <table> elements work best with integers. round up to ensure contents fits
|
|
}
|
|
function getSectionHasLiquidHeight(props, sectionConfig) {
|
|
return props.liquid && sectionConfig.liquid; // does the section do liquid-height? (need to have whole scrollgrid liquid-height as well)
|
|
}
|
|
function getAllowYScrolling(props, sectionConfig) {
|
|
return sectionConfig.maxHeight != null || // if its possible for the height to max out, we might need scrollbars
|
|
getSectionHasLiquidHeight(props, sectionConfig); // if the section is liquid height, it might condense enough to require scrollbars
|
|
}
|
|
// TODO: ONLY use `arg`. force out internal function to use same API
|
|
function renderChunkContent(sectionConfig, chunkConfig, arg, isHeader) {
|
|
let { expandRows } = arg;
|
|
let content = typeof chunkConfig.content === 'function' ?
|
|
chunkConfig.content(arg) :
|
|
y('table', {
|
|
role: 'presentation',
|
|
className: [
|
|
chunkConfig.tableClassName,
|
|
sectionConfig.syncRowHeights ? 'fc-scrollgrid-sync-table' : '',
|
|
].join(' '),
|
|
style: {
|
|
minWidth: arg.tableMinWidth,
|
|
width: arg.clientWidth,
|
|
height: expandRows ? arg.clientHeight : '', // css `height` on a <table> serves as a min-height
|
|
},
|
|
}, arg.tableColGroupNode, y(isHeader ? 'thead' : 'tbody', {
|
|
role: 'presentation',
|
|
}, typeof chunkConfig.rowContent === 'function'
|
|
? chunkConfig.rowContent(arg)
|
|
: chunkConfig.rowContent));
|
|
return content;
|
|
}
|
|
function isColPropsEqual(cols0, cols1) {
|
|
return isArraysEqual(cols0, cols1, isPropsEqual);
|
|
}
|
|
function renderMicroColGroup(cols, shrinkWidth) {
|
|
let colNodes = [];
|
|
/*
|
|
for ColProps with spans, it would have been great to make a single <col span="">
|
|
HOWEVER, Chrome was getting messing up distributing the width to <td>/<th> elements with colspans.
|
|
SOLUTION: making individual <col> elements makes Chrome behave.
|
|
*/
|
|
for (let colProps of cols) {
|
|
let span = colProps.span || 1;
|
|
for (let i = 0; i < span; i += 1) {
|
|
colNodes.push(y("col", { style: {
|
|
width: colProps.width === 'shrink' ? sanitizeShrinkWidth(shrinkWidth) : (colProps.width || ''),
|
|
minWidth: colProps.minWidth || '',
|
|
} }));
|
|
}
|
|
}
|
|
return y('colgroup', {}, ...colNodes);
|
|
}
|
|
function sanitizeShrinkWidth(shrinkWidth) {
|
|
/* why 4? if we do 0, it will kill any border, which are needed for computeSmallestCellWidth
|
|
4 accounts for 2 2-pixel borders. TODO: better solution? */
|
|
return shrinkWidth == null ? 4 : shrinkWidth;
|
|
}
|
|
function hasShrinkWidth(cols) {
|
|
for (let col of cols) {
|
|
if (col.width === 'shrink') {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function getScrollGridClassNames(liquid, context) {
|
|
let classNames = [
|
|
'fc-scrollgrid',
|
|
context.theme.getClass('table'),
|
|
];
|
|
if (liquid) {
|
|
classNames.push('fc-scrollgrid-liquid');
|
|
}
|
|
return classNames;
|
|
}
|
|
function getSectionClassNames(sectionConfig, wholeTableVGrow) {
|
|
let classNames = [
|
|
'fc-scrollgrid-section',
|
|
`fc-scrollgrid-section-${sectionConfig.type}`,
|
|
sectionConfig.className, // used?
|
|
];
|
|
if (wholeTableVGrow && sectionConfig.liquid && sectionConfig.maxHeight == null) {
|
|
classNames.push('fc-scrollgrid-section-liquid');
|
|
}
|
|
if (sectionConfig.isSticky) {
|
|
classNames.push('fc-scrollgrid-section-sticky');
|
|
}
|
|
return classNames;
|
|
}
|
|
function renderScrollShim(arg) {
|
|
return (y("div", { className: "fc-scrollgrid-sticky-shim", style: {
|
|
width: arg.clientWidth,
|
|
minWidth: arg.tableMinWidth,
|
|
} }));
|
|
}
|
|
function getStickyHeaderDates(options) {
|
|
let { stickyHeaderDates } = options;
|
|
if (stickyHeaderDates == null || stickyHeaderDates === 'auto') {
|
|
stickyHeaderDates = options.height === 'auto' || options.viewHeight === 'auto';
|
|
}
|
|
return stickyHeaderDates;
|
|
}
|
|
function getStickyFooterScrollbar(options) {
|
|
let { stickyFooterScrollbar } = options;
|
|
if (stickyFooterScrollbar == null || stickyFooterScrollbar === 'auto') {
|
|
stickyFooterScrollbar = options.height === 'auto' || options.viewHeight === 'auto';
|
|
}
|
|
return stickyFooterScrollbar;
|
|
}
|
|
|
|
class SimpleScrollGrid extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.processCols = memoize((a) => a, isColPropsEqual); // so we get same `cols` props every time
|
|
// yucky to memoize VNodes, but much more efficient for consumers
|
|
this.renderMicroColGroup = memoize(renderMicroColGroup);
|
|
this.scrollerRefs = new RefMap();
|
|
this.scrollerElRefs = new RefMap(this._handleScrollerEl.bind(this));
|
|
this.state = {
|
|
shrinkWidth: null,
|
|
forceYScrollbars: false,
|
|
scrollerClientWidths: {},
|
|
scrollerClientHeights: {},
|
|
};
|
|
// TODO: can do a really simple print-view. dont need to join rows
|
|
this.handleSizing = () => {
|
|
this.safeSetState(Object.assign({ shrinkWidth: this.computeShrinkWidth() }, this.computeScrollerDims()));
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state, context } = this;
|
|
let sectionConfigs = props.sections || [];
|
|
let cols = this.processCols(props.cols);
|
|
let microColGroupNode = this.renderMicroColGroup(cols, state.shrinkWidth);
|
|
let classNames = getScrollGridClassNames(props.liquid, context);
|
|
if (props.collapsibleWidth) {
|
|
classNames.push('fc-scrollgrid-collapsible');
|
|
}
|
|
// TODO: make DRY
|
|
let configCnt = sectionConfigs.length;
|
|
let configI = 0;
|
|
let currentConfig;
|
|
let headSectionNodes = [];
|
|
let bodySectionNodes = [];
|
|
let footSectionNodes = [];
|
|
while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'header') {
|
|
headSectionNodes.push(this.renderSection(currentConfig, microColGroupNode, true));
|
|
configI += 1;
|
|
}
|
|
while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'body') {
|
|
bodySectionNodes.push(this.renderSection(currentConfig, microColGroupNode, false));
|
|
configI += 1;
|
|
}
|
|
while (configI < configCnt && (currentConfig = sectionConfigs[configI]).type === 'footer') {
|
|
footSectionNodes.push(this.renderSection(currentConfig, microColGroupNode, true));
|
|
configI += 1;
|
|
}
|
|
// firefox bug: when setting height on table and there is a thead or tfoot,
|
|
// the necessary height:100% on the liquid-height body section forces the *whole* table to be taller. (bug #5524)
|
|
// use getCanVGrowWithinCell as a way to detect table-stupid firefox.
|
|
// if so, use a simpler dom structure, jam everything into a lone tbody.
|
|
let isBuggy = !getCanVGrowWithinCell();
|
|
const roleAttrs = { role: 'rowgroup' };
|
|
return y('table', {
|
|
role: 'grid',
|
|
className: classNames.join(' '),
|
|
style: { height: props.height },
|
|
}, Boolean(!isBuggy && headSectionNodes.length) && y('thead', roleAttrs, ...headSectionNodes), Boolean(!isBuggy && bodySectionNodes.length) && y('tbody', roleAttrs, ...bodySectionNodes), Boolean(!isBuggy && footSectionNodes.length) && y('tfoot', roleAttrs, ...footSectionNodes), isBuggy && y('tbody', roleAttrs, ...headSectionNodes, ...bodySectionNodes, ...footSectionNodes));
|
|
}
|
|
renderSection(sectionConfig, microColGroupNode, isHeader) {
|
|
if ('outerContent' in sectionConfig) {
|
|
return (y(_, { key: sectionConfig.key }, sectionConfig.outerContent));
|
|
}
|
|
return (y("tr", { key: sectionConfig.key, role: "presentation", className: getSectionClassNames(sectionConfig, this.props.liquid).join(' ') }, this.renderChunkTd(sectionConfig, microColGroupNode, sectionConfig.chunk, isHeader)));
|
|
}
|
|
renderChunkTd(sectionConfig, microColGroupNode, chunkConfig, isHeader) {
|
|
if ('outerContent' in chunkConfig) {
|
|
return chunkConfig.outerContent;
|
|
}
|
|
let { props } = this;
|
|
let { forceYScrollbars, scrollerClientWidths, scrollerClientHeights } = this.state;
|
|
let needsYScrolling = getAllowYScrolling(props, sectionConfig); // TODO: do lazily. do in section config?
|
|
let isLiquid = getSectionHasLiquidHeight(props, sectionConfig);
|
|
// for `!props.liquid` - is WHOLE scrollgrid natural height?
|
|
// TODO: do same thing in advanced scrollgrid? prolly not b/c always has horizontal scrollbars
|
|
let overflowY = !props.liquid ? 'visible' :
|
|
forceYScrollbars ? 'scroll' :
|
|
!needsYScrolling ? 'hidden' :
|
|
'auto';
|
|
let sectionKey = sectionConfig.key;
|
|
let content = renderChunkContent(sectionConfig, chunkConfig, {
|
|
tableColGroupNode: microColGroupNode,
|
|
tableMinWidth: '',
|
|
clientWidth: (!props.collapsibleWidth && scrollerClientWidths[sectionKey] !== undefined) ? scrollerClientWidths[sectionKey] : null,
|
|
clientHeight: scrollerClientHeights[sectionKey] !== undefined ? scrollerClientHeights[sectionKey] : null,
|
|
expandRows: sectionConfig.expandRows,
|
|
syncRowHeights: false,
|
|
rowSyncHeights: [],
|
|
reportRowHeightChange: () => { },
|
|
}, isHeader);
|
|
return y(isHeader ? 'th' : 'td', {
|
|
ref: chunkConfig.elRef,
|
|
role: 'presentation',
|
|
}, y("div", { className: `fc-scroller-harness${isLiquid ? ' fc-scroller-harness-liquid' : ''}` },
|
|
y(Scroller, { ref: this.scrollerRefs.createRef(sectionKey), elRef: this.scrollerElRefs.createRef(sectionKey), overflowY: overflowY, overflowX: !props.liquid ? 'visible' : 'hidden' /* natural height? */, maxHeight: sectionConfig.maxHeight, liquid: isLiquid, liquidIsAbsolute // because its within a harness
|
|
: true }, content)));
|
|
}
|
|
_handleScrollerEl(scrollerEl, key) {
|
|
let section = getSectionByKey(this.props.sections, key);
|
|
if (section) {
|
|
setRef(section.chunk.scrollerElRef, scrollerEl);
|
|
}
|
|
}
|
|
componentDidMount() {
|
|
this.handleSizing();
|
|
this.context.addResizeHandler(this.handleSizing);
|
|
}
|
|
componentDidUpdate() {
|
|
// TODO: need better solution when state contains non-sizing things
|
|
this.handleSizing();
|
|
}
|
|
componentWillUnmount() {
|
|
this.context.removeResizeHandler(this.handleSizing);
|
|
}
|
|
computeShrinkWidth() {
|
|
return hasShrinkWidth(this.props.cols)
|
|
? computeShrinkWidth(this.scrollerElRefs.getAll())
|
|
: 0;
|
|
}
|
|
computeScrollerDims() {
|
|
let scrollbarWidth = getScrollbarWidths();
|
|
let { scrollerRefs, scrollerElRefs } = this;
|
|
let forceYScrollbars = false;
|
|
let scrollerClientWidths = {};
|
|
let scrollerClientHeights = {};
|
|
for (let sectionKey in scrollerRefs.currentMap) {
|
|
let scroller = scrollerRefs.currentMap[sectionKey];
|
|
if (scroller && scroller.needsYScrolling()) {
|
|
forceYScrollbars = true;
|
|
break;
|
|
}
|
|
}
|
|
for (let section of this.props.sections) {
|
|
let sectionKey = section.key;
|
|
let scrollerEl = scrollerElRefs.currentMap[sectionKey];
|
|
if (scrollerEl) {
|
|
let harnessEl = scrollerEl.parentNode; // TODO: weird way to get this. need harness b/c doesn't include table borders
|
|
scrollerClientWidths[sectionKey] = Math.floor(harnessEl.getBoundingClientRect().width - (forceYScrollbars
|
|
? scrollbarWidth.y // use global because scroller might not have scrollbars yet but will need them in future
|
|
: 0));
|
|
scrollerClientHeights[sectionKey] = Math.floor(harnessEl.getBoundingClientRect().height);
|
|
}
|
|
}
|
|
return { forceYScrollbars, scrollerClientWidths, scrollerClientHeights };
|
|
}
|
|
}
|
|
SimpleScrollGrid.addStateEquality({
|
|
scrollerClientWidths: isPropsEqual,
|
|
scrollerClientHeights: isPropsEqual,
|
|
});
|
|
function getSectionByKey(sections, key) {
|
|
for (let section of sections) {
|
|
if (section.key === key) {
|
|
return section;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class EventContainer extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
// memo
|
|
this.buildPublicEvent = memoize((context, eventDef, eventInstance) => new EventImpl(context, eventDef, eventInstance));
|
|
this.handleEl = (el) => {
|
|
this.el = el;
|
|
setRef(this.props.elRef, el);
|
|
if (el) {
|
|
setElSeg(el, this.props.seg);
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
const { props, context } = this;
|
|
const { options } = context;
|
|
const { seg } = props;
|
|
const { eventRange } = seg;
|
|
const { ui } = eventRange;
|
|
const renderProps = {
|
|
event: this.buildPublicEvent(context, eventRange.def, eventRange.instance),
|
|
view: context.viewApi,
|
|
timeText: props.timeText,
|
|
textColor: ui.textColor,
|
|
backgroundColor: ui.backgroundColor,
|
|
borderColor: ui.borderColor,
|
|
isDraggable: !props.disableDragging && computeSegDraggable(seg, context),
|
|
isStartResizable: !props.disableResizing && computeSegStartResizable(seg, context),
|
|
isEndResizable: !props.disableResizing && computeSegEndResizable(seg),
|
|
isMirror: Boolean(props.isDragging || props.isResizing || props.isDateSelecting),
|
|
isStart: Boolean(seg.isStart),
|
|
isEnd: Boolean(seg.isEnd),
|
|
isPast: Boolean(props.isPast),
|
|
isFuture: Boolean(props.isFuture),
|
|
isToday: Boolean(props.isToday),
|
|
isSelected: Boolean(props.isSelected),
|
|
isDragging: Boolean(props.isDragging),
|
|
isResizing: Boolean(props.isResizing),
|
|
};
|
|
return (y(ContentContainer, { elRef: this.handleEl, elTag: props.elTag, elAttrs: props.elAttrs, elClasses: [
|
|
...getEventClassNames(renderProps),
|
|
...seg.eventRange.ui.classNames,
|
|
...(props.elClasses || []),
|
|
], elStyle: props.elStyle, renderProps: renderProps, generatorName: "eventContent", customGenerator: options.eventContent, defaultGenerator: props.defaultGenerator, classNameGenerator: options.eventClassNames, didMount: options.eventDidMount, willUnmount: options.eventWillUnmount }, props.children));
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
if (this.el && this.props.seg !== prevProps.seg) {
|
|
setElSeg(this.el, this.props.seg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// should not be a purecomponent
|
|
class StandardEvent extends BaseComponent {
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let { seg } = props;
|
|
let { ui } = seg.eventRange;
|
|
let timeFormat = options.eventTimeFormat || props.defaultTimeFormat;
|
|
let timeText = buildSegTimeText(seg, timeFormat, context, props.defaultDisplayEventTime, props.defaultDisplayEventEnd);
|
|
return (y(EventContainer, Object.assign({}, props /* includes elRef */, { elTag: "a", elStyle: {
|
|
borderColor: ui.borderColor,
|
|
backgroundColor: ui.backgroundColor,
|
|
}, elAttrs: getSegAnchorAttrs(seg, context), defaultGenerator: renderInnerContent$1, timeText: timeText }), (InnerContent, eventContentArg) => (y(_, null,
|
|
y(InnerContent, { elTag: "div", elClasses: ['fc-event-main'], elStyle: { color: eventContentArg.textColor } }),
|
|
Boolean(eventContentArg.isStartResizable) && (y("div", { className: "fc-event-resizer fc-event-resizer-start" })),
|
|
Boolean(eventContentArg.isEndResizable) && (y("div", { className: "fc-event-resizer fc-event-resizer-end" }))))));
|
|
}
|
|
}
|
|
StandardEvent.addPropsEquality({
|
|
seg: isPropsEqual,
|
|
});
|
|
function renderInnerContent$1(innerProps) {
|
|
return (y("div", { className: "fc-event-main-frame" },
|
|
innerProps.timeText && (y("div", { className: "fc-event-time" }, innerProps.timeText)),
|
|
y("div", { className: "fc-event-title-container" },
|
|
y("div", { className: "fc-event-title fc-sticky" }, innerProps.event.title || y(_, null, "\u00A0")))));
|
|
}
|
|
|
|
const NowIndicatorContainer = (props) => (y(ViewContextType.Consumer, null, (context) => {
|
|
let { options } = context;
|
|
let renderProps = {
|
|
isAxis: props.isAxis,
|
|
date: context.dateEnv.toDate(props.date),
|
|
view: context.viewApi,
|
|
};
|
|
return (y(ContentContainer, { elRef: props.elRef, elTag: props.elTag || 'div', elAttrs: props.elAttrs, elClasses: props.elClasses, elStyle: props.elStyle, renderProps: renderProps, generatorName: "nowIndicatorContent", customGenerator: options.nowIndicatorContent, classNameGenerator: options.nowIndicatorClassNames, didMount: options.nowIndicatorDidMount, willUnmount: options.nowIndicatorWillUnmount }, props.children));
|
|
}));
|
|
|
|
const DAY_NUM_FORMAT = createFormatter({ day: 'numeric' });
|
|
class DayCellContainer extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.refineRenderProps = memoizeObjArg(refineRenderProps);
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let renderProps = this.refineRenderProps({
|
|
date: props.date,
|
|
dateProfile: props.dateProfile,
|
|
todayRange: props.todayRange,
|
|
isMonthStart: props.isMonthStart || false,
|
|
showDayNumber: props.showDayNumber,
|
|
extraRenderProps: props.extraRenderProps,
|
|
viewApi: context.viewApi,
|
|
dateEnv: context.dateEnv,
|
|
monthStartFormat: options.monthStartFormat,
|
|
});
|
|
return (y(ContentContainer, { elRef: props.elRef, elTag: props.elTag, elAttrs: Object.assign(Object.assign({}, props.elAttrs), (renderProps.isDisabled ? {} : { 'data-date': formatDayString(props.date) })), elClasses: [
|
|
...getDayClassNames(renderProps, context.theme),
|
|
...(props.elClasses || []),
|
|
], elStyle: props.elStyle, renderProps: renderProps, generatorName: "dayCellContent", customGenerator: options.dayCellContent, defaultGenerator: props.defaultGenerator, classNameGenerator:
|
|
// don't use custom classNames if disabled
|
|
renderProps.isDisabled ? undefined : options.dayCellClassNames, didMount: options.dayCellDidMount, willUnmount: options.dayCellWillUnmount }, props.children));
|
|
}
|
|
}
|
|
function hasCustomDayCellContent(options) {
|
|
return Boolean(options.dayCellContent || hasCustomRenderingHandler('dayCellContent', options));
|
|
}
|
|
function refineRenderProps(raw) {
|
|
let { date, dateEnv, dateProfile, isMonthStart } = raw;
|
|
let dayMeta = getDateMeta(date, raw.todayRange, null, dateProfile);
|
|
let dayNumberText = raw.showDayNumber ? (dateEnv.format(date, isMonthStart ? raw.monthStartFormat : DAY_NUM_FORMAT)) : '';
|
|
return Object.assign(Object.assign(Object.assign({ date: dateEnv.toDate(date), view: raw.viewApi }, dayMeta), { isMonthStart,
|
|
dayNumberText }), raw.extraRenderProps);
|
|
}
|
|
|
|
class BgEvent extends BaseComponent {
|
|
render() {
|
|
let { props } = this;
|
|
let { seg } = props;
|
|
return (y(EventContainer, { elTag: "div", elClasses: ['fc-bg-event'], elStyle: { backgroundColor: seg.eventRange.ui.backgroundColor }, defaultGenerator: renderInnerContent, seg: seg, timeText: "", isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, isPast: props.isPast, isFuture: props.isFuture, isToday: props.isToday, disableDragging: true, disableResizing: true }));
|
|
}
|
|
}
|
|
function renderInnerContent(props) {
|
|
let { title } = props.event;
|
|
return title && (y("div", { className: "fc-event-title" }, props.event.title));
|
|
}
|
|
function renderFill(fillType) {
|
|
return (y("div", { className: `fc-${fillType}` }));
|
|
}
|
|
|
|
const WeekNumberContainer = (props) => (y(ViewContextType.Consumer, null, (context) => {
|
|
let { dateEnv, options } = context;
|
|
let { date } = props;
|
|
let format = options.weekNumberFormat || props.defaultFormat;
|
|
let num = dateEnv.computeWeekNumber(date); // TODO: somehow use for formatting as well?
|
|
let text = dateEnv.format(date, format);
|
|
let renderProps = { num, text, date };
|
|
return (y(ContentContainer // why isn't WeekNumberContentArg being auto-detected?
|
|
, { elRef: props.elRef, elTag: props.elTag, elAttrs: props.elAttrs, elClasses: props.elClasses, elStyle: props.elStyle, renderProps: renderProps, generatorName: "weekNumberContent", customGenerator: options.weekNumberContent, defaultGenerator: renderInner, classNameGenerator: options.weekNumberClassNames, didMount: options.weekNumberDidMount, willUnmount: options.weekNumberWillUnmount }, props.children));
|
|
}));
|
|
function renderInner(innerProps) {
|
|
return innerProps.text;
|
|
}
|
|
|
|
const PADDING_FROM_VIEWPORT = 10;
|
|
class Popover extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
titleId: getUniqueDomId(),
|
|
};
|
|
this.handleRootEl = (el) => {
|
|
this.rootEl = el;
|
|
if (this.props.elRef) {
|
|
setRef(this.props.elRef, el);
|
|
}
|
|
};
|
|
// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
|
|
this.handleDocumentMouseDown = (ev) => {
|
|
// only hide the popover if the click happened outside the popover
|
|
const target = getEventTargetViaRoot(ev);
|
|
if (!this.rootEl.contains(target)) {
|
|
this.handleCloseClick();
|
|
}
|
|
};
|
|
this.handleDocumentKeyDown = (ev) => {
|
|
if (ev.key === 'Escape') {
|
|
this.handleCloseClick();
|
|
}
|
|
};
|
|
this.handleCloseClick = () => {
|
|
let { onClose } = this.props;
|
|
if (onClose) {
|
|
onClose();
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
let { theme, options } = this.context;
|
|
let { props, state } = this;
|
|
let classNames = [
|
|
'fc-popover',
|
|
theme.getClass('popover'),
|
|
].concat(props.extraClassNames || []);
|
|
return compat_module_j(y("div", Object.assign({}, props.extraAttrs, { id: props.id, className: classNames.join(' '), "aria-labelledby": state.titleId, ref: this.handleRootEl }),
|
|
y("div", { className: 'fc-popover-header ' + theme.getClass('popoverHeader') },
|
|
y("span", { className: "fc-popover-title", id: state.titleId }, props.title),
|
|
y("span", { className: 'fc-popover-close ' + theme.getIconClass('close'), title: options.closeHint, onClick: this.handleCloseClick })),
|
|
y("div", { className: 'fc-popover-body ' + theme.getClass('popoverContent') }, props.children)), props.parentEl);
|
|
}
|
|
componentDidMount() {
|
|
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
|
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
|
this.updateSize();
|
|
}
|
|
componentWillUnmount() {
|
|
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
|
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
|
}
|
|
updateSize() {
|
|
let { isRtl } = this.context;
|
|
let { alignmentEl, alignGridTop } = this.props;
|
|
let { rootEl } = this;
|
|
let alignmentRect = computeClippedClientRect(alignmentEl);
|
|
if (alignmentRect) {
|
|
let popoverDims = rootEl.getBoundingClientRect();
|
|
// position relative to viewport
|
|
let popoverTop = alignGridTop
|
|
? elementClosest(alignmentEl, '.fc-scrollgrid').getBoundingClientRect().top
|
|
: alignmentRect.top;
|
|
let popoverLeft = isRtl ? alignmentRect.right - popoverDims.width : alignmentRect.left;
|
|
// constrain
|
|
popoverTop = Math.max(popoverTop, PADDING_FROM_VIEWPORT);
|
|
popoverLeft = Math.min(popoverLeft, document.documentElement.clientWidth - PADDING_FROM_VIEWPORT - popoverDims.width);
|
|
popoverLeft = Math.max(popoverLeft, PADDING_FROM_VIEWPORT);
|
|
let origin = rootEl.offsetParent.getBoundingClientRect();
|
|
applyStyle(rootEl, {
|
|
top: popoverTop - origin.top,
|
|
left: popoverLeft - origin.left,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class MorePopover extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.handleRootEl = (rootEl) => {
|
|
this.rootEl = rootEl;
|
|
if (rootEl) {
|
|
this.context.registerInteractiveComponent(this, {
|
|
el: rootEl,
|
|
useEventCenter: false,
|
|
});
|
|
}
|
|
else {
|
|
this.context.unregisterInteractiveComponent(this);
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
let { options, dateEnv } = this.context;
|
|
let { props } = this;
|
|
let { startDate, todayRange, dateProfile } = props;
|
|
let title = dateEnv.format(startDate, options.dayPopoverFormat);
|
|
return (y(DayCellContainer, { elRef: this.handleRootEl, date: startDate, dateProfile: dateProfile, todayRange: todayRange }, (InnerContent, renderProps, elAttrs) => (y(Popover, { elRef: elAttrs.ref, id: props.id, title: title, extraClassNames: ['fc-more-popover'].concat(elAttrs.className || []), extraAttrs: elAttrs /* TODO: make these time-based when not whole-day? */, parentEl: props.parentEl, alignmentEl: props.alignmentEl, alignGridTop: props.alignGridTop, onClose: props.onClose },
|
|
hasCustomDayCellContent(options) && (y(InnerContent, { elTag: "div", elClasses: ['fc-more-popover-misc'] })),
|
|
props.children))));
|
|
}
|
|
queryHit(positionLeft, positionTop, elWidth, elHeight) {
|
|
let { rootEl, props } = this;
|
|
if (positionLeft >= 0 && positionLeft < elWidth &&
|
|
positionTop >= 0 && positionTop < elHeight) {
|
|
return {
|
|
dateProfile: props.dateProfile,
|
|
dateSpan: Object.assign({ allDay: !props.forceTimed, range: {
|
|
start: props.startDate,
|
|
end: props.endDate,
|
|
} }, props.extraDateSpan),
|
|
dayEl: rootEl,
|
|
rect: {
|
|
left: 0,
|
|
top: 0,
|
|
right: elWidth,
|
|
bottom: elHeight,
|
|
},
|
|
layer: 1, // important when comparing with hits from other components
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class MoreLinkContainer extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
isPopoverOpen: false,
|
|
popoverId: getUniqueDomId(),
|
|
};
|
|
this.handleLinkEl = (linkEl) => {
|
|
this.linkEl = linkEl;
|
|
if (this.props.elRef) {
|
|
setRef(this.props.elRef, linkEl);
|
|
}
|
|
};
|
|
this.handleClick = (ev) => {
|
|
let { props, context } = this;
|
|
let { moreLinkClick } = context.options;
|
|
let date = computeRange(props).start;
|
|
function buildPublicSeg(seg) {
|
|
let { def, instance, range } = seg.eventRange;
|
|
return {
|
|
event: new EventImpl(context, def, instance),
|
|
start: context.dateEnv.toDate(range.start),
|
|
end: context.dateEnv.toDate(range.end),
|
|
isStart: seg.isStart,
|
|
isEnd: seg.isEnd,
|
|
};
|
|
}
|
|
if (typeof moreLinkClick === 'function') {
|
|
moreLinkClick = moreLinkClick({
|
|
date,
|
|
allDay: Boolean(props.allDayDate),
|
|
allSegs: props.allSegs.map(buildPublicSeg),
|
|
hiddenSegs: props.hiddenSegs.map(buildPublicSeg),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
}
|
|
if (!moreLinkClick || moreLinkClick === 'popover') {
|
|
this.setState({ isPopoverOpen: true });
|
|
}
|
|
else if (typeof moreLinkClick === 'string') { // a view name
|
|
context.calendarApi.zoomTo(date, moreLinkClick);
|
|
}
|
|
};
|
|
this.handlePopoverClose = () => {
|
|
this.setState({ isPopoverOpen: false });
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state } = this;
|
|
return (y(ViewContextType.Consumer, null, (context) => {
|
|
let { viewApi, options, calendarApi } = context;
|
|
let { moreLinkText } = options;
|
|
let { moreCnt } = props;
|
|
let range = computeRange(props);
|
|
let text = typeof moreLinkText === 'function' // TODO: eventually use formatWithOrdinals
|
|
? moreLinkText.call(calendarApi, moreCnt)
|
|
: `+${moreCnt} ${moreLinkText}`;
|
|
let hint = formatWithOrdinals(options.moreLinkHint, [moreCnt], text);
|
|
let renderProps = {
|
|
num: moreCnt,
|
|
shortText: `+${moreCnt}`,
|
|
text,
|
|
view: viewApi,
|
|
};
|
|
return (y(_, null,
|
|
Boolean(props.moreCnt) && (y(ContentContainer, { elTag: props.elTag || 'a', elRef: this.handleLinkEl, elClasses: [
|
|
...(props.elClasses || []),
|
|
'fc-more-link',
|
|
], elStyle: props.elStyle, elAttrs: Object.assign(Object.assign(Object.assign({}, props.elAttrs), createAriaClickAttrs(this.handleClick)), { title: hint, 'aria-expanded': state.isPopoverOpen, 'aria-controls': state.isPopoverOpen ? state.popoverId : '' }), renderProps: renderProps, generatorName: "moreLinkContent", customGenerator: options.moreLinkContent, defaultGenerator: props.defaultGenerator || renderMoreLinkInner, classNameGenerator: options.moreLinkClassNames, didMount: options.moreLinkDidMount, willUnmount: options.moreLinkWillUnmount }, props.children)),
|
|
state.isPopoverOpen && (y(MorePopover, { id: state.popoverId, startDate: range.start, endDate: range.end, dateProfile: props.dateProfile, todayRange: props.todayRange, extraDateSpan: props.extraDateSpan, parentEl: this.parentEl, alignmentEl: props.alignmentElRef ?
|
|
props.alignmentElRef.current :
|
|
this.linkEl, alignGridTop: props.alignGridTop, forceTimed: props.forceTimed, onClose: this.handlePopoverClose }, props.popoverContent()))));
|
|
}));
|
|
}
|
|
componentDidMount() {
|
|
this.updateParentEl();
|
|
}
|
|
componentDidUpdate() {
|
|
this.updateParentEl();
|
|
}
|
|
updateParentEl() {
|
|
if (this.linkEl) {
|
|
this.parentEl = elementClosest(this.linkEl, '.fc-view-harness');
|
|
}
|
|
}
|
|
}
|
|
function renderMoreLinkInner(props) {
|
|
return props.text;
|
|
}
|
|
function computeRange(props) {
|
|
if (props.allDayDate) {
|
|
return {
|
|
start: props.allDayDate,
|
|
end: addDays(props.allDayDate, 1),
|
|
};
|
|
}
|
|
let { hiddenSegs } = props;
|
|
return {
|
|
start: computeEarliestSegStart(hiddenSegs),
|
|
end: computeLatestSegEnd(hiddenSegs),
|
|
};
|
|
}
|
|
function computeEarliestSegStart(segs) {
|
|
return segs.reduce(pickEarliestStart).eventRange.range.start;
|
|
}
|
|
function pickEarliestStart(seg0, seg1) {
|
|
return seg0.eventRange.range.start < seg1.eventRange.range.start ? seg0 : seg1;
|
|
}
|
|
function computeLatestSegEnd(segs) {
|
|
return segs.reduce(pickLatestEnd).eventRange.range.end;
|
|
}
|
|
function pickLatestEnd(seg0, seg1) {
|
|
return seg0.eventRange.range.end > seg1.eventRange.range.end ? seg0 : seg1;
|
|
}
|
|
|
|
class Store {
|
|
constructor() {
|
|
this.handlers = [];
|
|
}
|
|
set(value) {
|
|
this.currentValue = value;
|
|
for (let handler of this.handlers) {
|
|
handler(value);
|
|
}
|
|
}
|
|
subscribe(handler) {
|
|
this.handlers.push(handler);
|
|
if (this.currentValue !== undefined) {
|
|
handler(this.currentValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Subscribers will get a LIST of CustomRenderings
|
|
*/
|
|
class CustomRenderingStore extends Store {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.map = new Map();
|
|
}
|
|
// for consistent order
|
|
handle(customRendering) {
|
|
const { map } = this;
|
|
let updated = false;
|
|
if (customRendering.isActive) {
|
|
map.set(customRendering.id, customRendering);
|
|
updated = true;
|
|
}
|
|
else if (map.has(customRendering.id)) {
|
|
map.delete(customRendering.id);
|
|
updated = true;
|
|
}
|
|
if (updated) {
|
|
this.set(map);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/core/index.js
|
|
/* unused harmony import specifier */ var core_createFormatter;
|
|
/* unused harmony import specifier */ var core_BASE_OPTION_DEFAULTS;
|
|
/* unused harmony import specifier */ var core_DateEnv;
|
|
/* unused harmony import specifier */ var core_sliceEventStore;
|
|
|
|
|
|
|
|
|
|
|
|
const globalLocales = [];
|
|
|
|
const MINIMAL_RAW_EN_LOCALE = {
|
|
code: 'en',
|
|
week: {
|
|
dow: 0,
|
|
doy: 4, // 4 days need to be within the year to be considered the first week
|
|
},
|
|
direction: 'ltr',
|
|
buttonText: {
|
|
prev: 'prev',
|
|
next: 'next',
|
|
prevYear: 'prev year',
|
|
nextYear: 'next year',
|
|
year: 'year',
|
|
today: 'today',
|
|
month: 'month',
|
|
week: 'week',
|
|
day: 'day',
|
|
list: 'list',
|
|
},
|
|
weekText: 'W',
|
|
weekTextLong: 'Week',
|
|
closeHint: 'Close',
|
|
timeHint: 'Time',
|
|
eventHint: 'Event',
|
|
allDayText: 'all-day',
|
|
moreLinkText: 'more',
|
|
noEventsText: 'No events to display',
|
|
};
|
|
const RAW_EN_LOCALE = Object.assign(Object.assign({}, MINIMAL_RAW_EN_LOCALE), {
|
|
// Includes things we don't want other locales to inherit,
|
|
// things that derive from other translatable strings.
|
|
buttonHints: {
|
|
prev: 'Previous $0',
|
|
next: 'Next $0',
|
|
today(buttonText, unit) {
|
|
return (unit === 'day')
|
|
? 'Today'
|
|
: `This ${buttonText}`;
|
|
},
|
|
}, viewHint: '$0 view', navLinkHint: 'Go to $0', moreLinkHint(eventCnt) {
|
|
return `Show ${eventCnt} more event${eventCnt === 1 ? '' : 's'}`;
|
|
} });
|
|
function organizeRawLocales(explicitRawLocales) {
|
|
let defaultCode = explicitRawLocales.length > 0 ? explicitRawLocales[0].code : 'en';
|
|
let allRawLocales = globalLocales.concat(explicitRawLocales);
|
|
let rawLocaleMap = {
|
|
en: RAW_EN_LOCALE,
|
|
};
|
|
for (let rawLocale of allRawLocales) {
|
|
rawLocaleMap[rawLocale.code] = rawLocale;
|
|
}
|
|
return {
|
|
map: rawLocaleMap,
|
|
defaultCode,
|
|
};
|
|
}
|
|
function buildLocale(inputSingular, available) {
|
|
if (typeof inputSingular === 'object' && !Array.isArray(inputSingular)) {
|
|
return parseLocale(inputSingular.code, [inputSingular.code], inputSingular);
|
|
}
|
|
return queryLocale(inputSingular, available);
|
|
}
|
|
function queryLocale(codeArg, available) {
|
|
let codes = [].concat(codeArg || []); // will convert to array
|
|
let raw = queryRawLocale(codes, available) || RAW_EN_LOCALE;
|
|
return parseLocale(codeArg, codes, raw);
|
|
}
|
|
function queryRawLocale(codes, available) {
|
|
for (let i = 0; i < codes.length; i += 1) {
|
|
let parts = codes[i].toLocaleLowerCase().split('-');
|
|
for (let j = parts.length; j > 0; j -= 1) {
|
|
let simpleId = parts.slice(0, j).join('-');
|
|
if (available[simpleId]) {
|
|
return available[simpleId];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function parseLocale(codeArg, codes, raw) {
|
|
let merged = mergeProps([MINIMAL_RAW_EN_LOCALE, raw], ['buttonText']);
|
|
delete merged.code; // don't want this part of the options
|
|
let { week } = merged;
|
|
delete merged.week;
|
|
return {
|
|
codeArg,
|
|
codes,
|
|
week,
|
|
simpleNumberFormat: new Intl.NumberFormat(codeArg),
|
|
options: merged,
|
|
};
|
|
}
|
|
|
|
// TODO: easier way to add new hooks? need to update a million things
|
|
function createPlugin(input) {
|
|
return {
|
|
id: guid(),
|
|
name: input.name,
|
|
premiumReleaseDate: input.premiumReleaseDate ? new Date(input.premiumReleaseDate) : undefined,
|
|
deps: input.deps || [],
|
|
reducers: input.reducers || [],
|
|
isLoadingFuncs: input.isLoadingFuncs || [],
|
|
contextInit: [].concat(input.contextInit || []),
|
|
eventRefiners: input.eventRefiners || {},
|
|
eventDefMemberAdders: input.eventDefMemberAdders || [],
|
|
eventSourceRefiners: input.eventSourceRefiners || {},
|
|
isDraggableTransformers: input.isDraggableTransformers || [],
|
|
eventDragMutationMassagers: input.eventDragMutationMassagers || [],
|
|
eventDefMutationAppliers: input.eventDefMutationAppliers || [],
|
|
dateSelectionTransformers: input.dateSelectionTransformers || [],
|
|
datePointTransforms: input.datePointTransforms || [],
|
|
dateSpanTransforms: input.dateSpanTransforms || [],
|
|
views: input.views || {},
|
|
viewPropsTransformers: input.viewPropsTransformers || [],
|
|
isPropsValid: input.isPropsValid || null,
|
|
externalDefTransforms: input.externalDefTransforms || [],
|
|
viewContainerAppends: input.viewContainerAppends || [],
|
|
eventDropTransformers: input.eventDropTransformers || [],
|
|
componentInteractions: input.componentInteractions || [],
|
|
calendarInteractions: input.calendarInteractions || [],
|
|
themeClasses: input.themeClasses || {},
|
|
eventSourceDefs: input.eventSourceDefs || [],
|
|
cmdFormatter: input.cmdFormatter,
|
|
recurringTypes: input.recurringTypes || [],
|
|
namedTimeZonedImpl: input.namedTimeZonedImpl,
|
|
initialView: input.initialView || '',
|
|
elementDraggingImpl: input.elementDraggingImpl,
|
|
optionChangeHandlers: input.optionChangeHandlers || {},
|
|
scrollGridImpl: input.scrollGridImpl || null,
|
|
listenerRefiners: input.listenerRefiners || {},
|
|
optionRefiners: input.optionRefiners || {},
|
|
propSetHandlers: input.propSetHandlers || {},
|
|
};
|
|
}
|
|
function buildPluginHooks(pluginDefs, globalDefs) {
|
|
let currentPluginIds = {};
|
|
let hooks = {
|
|
premiumReleaseDate: undefined,
|
|
reducers: [],
|
|
isLoadingFuncs: [],
|
|
contextInit: [],
|
|
eventRefiners: {},
|
|
eventDefMemberAdders: [],
|
|
eventSourceRefiners: {},
|
|
isDraggableTransformers: [],
|
|
eventDragMutationMassagers: [],
|
|
eventDefMutationAppliers: [],
|
|
dateSelectionTransformers: [],
|
|
datePointTransforms: [],
|
|
dateSpanTransforms: [],
|
|
views: {},
|
|
viewPropsTransformers: [],
|
|
isPropsValid: null,
|
|
externalDefTransforms: [],
|
|
viewContainerAppends: [],
|
|
eventDropTransformers: [],
|
|
componentInteractions: [],
|
|
calendarInteractions: [],
|
|
themeClasses: {},
|
|
eventSourceDefs: [],
|
|
cmdFormatter: null,
|
|
recurringTypes: [],
|
|
namedTimeZonedImpl: null,
|
|
initialView: '',
|
|
elementDraggingImpl: null,
|
|
optionChangeHandlers: {},
|
|
scrollGridImpl: null,
|
|
listenerRefiners: {},
|
|
optionRefiners: {},
|
|
propSetHandlers: {},
|
|
};
|
|
function addDefs(defs) {
|
|
for (let def of defs) {
|
|
const pluginName = def.name;
|
|
const currentId = currentPluginIds[pluginName];
|
|
if (currentId === undefined) {
|
|
currentPluginIds[pluginName] = def.id;
|
|
addDefs(def.deps);
|
|
hooks = combineHooks(hooks, def);
|
|
}
|
|
else if (currentId !== def.id) {
|
|
// different ID than the one already added
|
|
console.warn(`Duplicate plugin '${pluginName}'`);
|
|
}
|
|
}
|
|
}
|
|
if (pluginDefs) {
|
|
addDefs(pluginDefs);
|
|
}
|
|
addDefs(globalDefs);
|
|
return hooks;
|
|
}
|
|
function buildBuildPluginHooks() {
|
|
let currentOverrideDefs = [];
|
|
let currentGlobalDefs = [];
|
|
let currentHooks;
|
|
return (overrideDefs, globalDefs) => {
|
|
if (!currentHooks || !isArraysEqual(overrideDefs, currentOverrideDefs) || !isArraysEqual(globalDefs, currentGlobalDefs)) {
|
|
currentHooks = buildPluginHooks(overrideDefs, globalDefs);
|
|
}
|
|
currentOverrideDefs = overrideDefs;
|
|
currentGlobalDefs = globalDefs;
|
|
return currentHooks;
|
|
};
|
|
}
|
|
function combineHooks(hooks0, hooks1) {
|
|
return {
|
|
premiumReleaseDate: compareOptionalDates(hooks0.premiumReleaseDate, hooks1.premiumReleaseDate),
|
|
reducers: hooks0.reducers.concat(hooks1.reducers),
|
|
isLoadingFuncs: hooks0.isLoadingFuncs.concat(hooks1.isLoadingFuncs),
|
|
contextInit: hooks0.contextInit.concat(hooks1.contextInit),
|
|
eventRefiners: Object.assign(Object.assign({}, hooks0.eventRefiners), hooks1.eventRefiners),
|
|
eventDefMemberAdders: hooks0.eventDefMemberAdders.concat(hooks1.eventDefMemberAdders),
|
|
eventSourceRefiners: Object.assign(Object.assign({}, hooks0.eventSourceRefiners), hooks1.eventSourceRefiners),
|
|
isDraggableTransformers: hooks0.isDraggableTransformers.concat(hooks1.isDraggableTransformers),
|
|
eventDragMutationMassagers: hooks0.eventDragMutationMassagers.concat(hooks1.eventDragMutationMassagers),
|
|
eventDefMutationAppliers: hooks0.eventDefMutationAppliers.concat(hooks1.eventDefMutationAppliers),
|
|
dateSelectionTransformers: hooks0.dateSelectionTransformers.concat(hooks1.dateSelectionTransformers),
|
|
datePointTransforms: hooks0.datePointTransforms.concat(hooks1.datePointTransforms),
|
|
dateSpanTransforms: hooks0.dateSpanTransforms.concat(hooks1.dateSpanTransforms),
|
|
views: Object.assign(Object.assign({}, hooks0.views), hooks1.views),
|
|
viewPropsTransformers: hooks0.viewPropsTransformers.concat(hooks1.viewPropsTransformers),
|
|
isPropsValid: hooks1.isPropsValid || hooks0.isPropsValid,
|
|
externalDefTransforms: hooks0.externalDefTransforms.concat(hooks1.externalDefTransforms),
|
|
viewContainerAppends: hooks0.viewContainerAppends.concat(hooks1.viewContainerAppends),
|
|
eventDropTransformers: hooks0.eventDropTransformers.concat(hooks1.eventDropTransformers),
|
|
calendarInteractions: hooks0.calendarInteractions.concat(hooks1.calendarInteractions),
|
|
componentInteractions: hooks0.componentInteractions.concat(hooks1.componentInteractions),
|
|
themeClasses: Object.assign(Object.assign({}, hooks0.themeClasses), hooks1.themeClasses),
|
|
eventSourceDefs: hooks0.eventSourceDefs.concat(hooks1.eventSourceDefs),
|
|
cmdFormatter: hooks1.cmdFormatter || hooks0.cmdFormatter,
|
|
recurringTypes: hooks0.recurringTypes.concat(hooks1.recurringTypes),
|
|
namedTimeZonedImpl: hooks1.namedTimeZonedImpl || hooks0.namedTimeZonedImpl,
|
|
initialView: hooks0.initialView || hooks1.initialView,
|
|
elementDraggingImpl: hooks0.elementDraggingImpl || hooks1.elementDraggingImpl,
|
|
optionChangeHandlers: Object.assign(Object.assign({}, hooks0.optionChangeHandlers), hooks1.optionChangeHandlers),
|
|
scrollGridImpl: hooks1.scrollGridImpl || hooks0.scrollGridImpl,
|
|
listenerRefiners: Object.assign(Object.assign({}, hooks0.listenerRefiners), hooks1.listenerRefiners),
|
|
optionRefiners: Object.assign(Object.assign({}, hooks0.optionRefiners), hooks1.optionRefiners),
|
|
propSetHandlers: Object.assign(Object.assign({}, hooks0.propSetHandlers), hooks1.propSetHandlers),
|
|
};
|
|
}
|
|
function compareOptionalDates(date0, date1) {
|
|
if (date0 === undefined) {
|
|
return date1;
|
|
}
|
|
if (date1 === undefined) {
|
|
return date0;
|
|
}
|
|
return new Date(Math.max(date0.valueOf(), date1.valueOf()));
|
|
}
|
|
|
|
class StandardTheme extends internal_common_Theme {
|
|
}
|
|
StandardTheme.prototype.classes = {
|
|
root: 'fc-theme-standard',
|
|
tableCellShaded: 'fc-cell-shaded',
|
|
buttonGroup: 'fc-button-group',
|
|
button: 'fc-button fc-button-primary',
|
|
buttonActive: 'fc-button-active',
|
|
};
|
|
StandardTheme.prototype.baseIconClass = 'fc-icon';
|
|
StandardTheme.prototype.iconClasses = {
|
|
close: 'fc-icon-x',
|
|
prev: 'fc-icon-chevron-left',
|
|
next: 'fc-icon-chevron-right',
|
|
prevYear: 'fc-icon-chevrons-left',
|
|
nextYear: 'fc-icon-chevrons-right',
|
|
};
|
|
StandardTheme.prototype.rtlIconClasses = {
|
|
prev: 'fc-icon-chevron-right',
|
|
next: 'fc-icon-chevron-left',
|
|
prevYear: 'fc-icon-chevrons-right',
|
|
nextYear: 'fc-icon-chevrons-left',
|
|
};
|
|
StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; // TODO: make TS-friendly
|
|
StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon';
|
|
StandardTheme.prototype.iconOverridePrefix = 'fc-icon-';
|
|
|
|
function compileViewDefs(defaultConfigs, overrideConfigs) {
|
|
let hash = {};
|
|
let viewType;
|
|
for (viewType in defaultConfigs) {
|
|
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
for (viewType in overrideConfigs) {
|
|
ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
return hash;
|
|
}
|
|
function ensureViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
|
|
if (hash[viewType]) {
|
|
return hash[viewType];
|
|
}
|
|
let viewDef = buildViewDef(viewType, hash, defaultConfigs, overrideConfigs);
|
|
if (viewDef) {
|
|
hash[viewType] = viewDef;
|
|
}
|
|
return viewDef;
|
|
}
|
|
function buildViewDef(viewType, hash, defaultConfigs, overrideConfigs) {
|
|
let defaultConfig = defaultConfigs[viewType];
|
|
let overrideConfig = overrideConfigs[viewType];
|
|
let queryProp = (name) => ((defaultConfig && defaultConfig[name] !== null) ? defaultConfig[name] :
|
|
((overrideConfig && overrideConfig[name] !== null) ? overrideConfig[name] : null));
|
|
let theComponent = queryProp('component');
|
|
let superType = queryProp('superType');
|
|
let superDef = null;
|
|
if (superType) {
|
|
if (superType === viewType) {
|
|
throw new Error('Can\'t have a custom view type that references itself');
|
|
}
|
|
superDef = ensureViewDef(superType, hash, defaultConfigs, overrideConfigs);
|
|
}
|
|
if (!theComponent && superDef) {
|
|
theComponent = superDef.component;
|
|
}
|
|
if (!theComponent) {
|
|
return null; // don't throw a warning, might be settings for a single-unit view
|
|
}
|
|
return {
|
|
type: viewType,
|
|
component: theComponent,
|
|
defaults: Object.assign(Object.assign({}, (superDef ? superDef.defaults : {})), (defaultConfig ? defaultConfig.rawOptions : {})),
|
|
overrides: Object.assign(Object.assign({}, (superDef ? superDef.overrides : {})), (overrideConfig ? overrideConfig.rawOptions : {})),
|
|
};
|
|
}
|
|
|
|
function parseViewConfigs(inputs) {
|
|
return mapHash(inputs, parseViewConfig);
|
|
}
|
|
function parseViewConfig(input) {
|
|
let rawOptions = typeof input === 'function' ?
|
|
{ component: input } :
|
|
input;
|
|
let { component } = rawOptions;
|
|
if (rawOptions.content) {
|
|
// TODO: remove content/classNames/didMount/etc from options?
|
|
component = createViewHookComponent(rawOptions);
|
|
}
|
|
else if (component && !(component.prototype instanceof BaseComponent)) {
|
|
// WHY?: people were using `component` property for `content`
|
|
// TODO: converge on one setting name
|
|
component = createViewHookComponent(Object.assign(Object.assign({}, rawOptions), { content: component }));
|
|
}
|
|
return {
|
|
superType: rawOptions.type,
|
|
component: component,
|
|
rawOptions, // includes type and component too :(
|
|
};
|
|
}
|
|
function createViewHookComponent(options) {
|
|
return (viewProps) => (y(ViewContextType.Consumer, null, (context) => (y(ContentContainer, { elTag: "div", elClasses: buildViewClassNames(context.viewSpec), renderProps: Object.assign(Object.assign({}, viewProps), { nextDayThreshold: context.options.nextDayThreshold }), generatorName: undefined, customGenerator: options.content, classNameGenerator: options.classNames, didMount: options.didMount, willUnmount: options.willUnmount }))));
|
|
}
|
|
|
|
function buildViewSpecs(defaultInputs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
|
|
let defaultConfigs = parseViewConfigs(defaultInputs);
|
|
let overrideConfigs = parseViewConfigs(optionOverrides.views);
|
|
let viewDefs = compileViewDefs(defaultConfigs, overrideConfigs);
|
|
return mapHash(viewDefs, (viewDef) => buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults));
|
|
}
|
|
function buildViewSpec(viewDef, overrideConfigs, optionOverrides, dynamicOptionOverrides, localeDefaults) {
|
|
let durationInput = viewDef.overrides.duration ||
|
|
viewDef.defaults.duration ||
|
|
dynamicOptionOverrides.duration ||
|
|
optionOverrides.duration;
|
|
let duration = null;
|
|
let durationUnit = '';
|
|
let singleUnit = '';
|
|
let singleUnitOverrides = {};
|
|
if (durationInput) {
|
|
duration = createDurationCached(durationInput);
|
|
if (duration) { // valid?
|
|
let denom = greatestDurationDenominator(duration);
|
|
durationUnit = denom.unit;
|
|
if (denom.value === 1) {
|
|
singleUnit = durationUnit;
|
|
singleUnitOverrides = overrideConfigs[durationUnit] ? overrideConfigs[durationUnit].rawOptions : {};
|
|
}
|
|
}
|
|
}
|
|
let queryButtonText = (optionsSubset) => {
|
|
let buttonTextMap = optionsSubset.buttonText || {};
|
|
let buttonTextKey = viewDef.defaults.buttonTextKey;
|
|
if (buttonTextKey != null && buttonTextMap[buttonTextKey] != null) {
|
|
return buttonTextMap[buttonTextKey];
|
|
}
|
|
if (buttonTextMap[viewDef.type] != null) {
|
|
return buttonTextMap[viewDef.type];
|
|
}
|
|
if (buttonTextMap[singleUnit] != null) {
|
|
return buttonTextMap[singleUnit];
|
|
}
|
|
return null;
|
|
};
|
|
let queryButtonTitle = (optionsSubset) => {
|
|
let buttonHints = optionsSubset.buttonHints || {};
|
|
let buttonKey = viewDef.defaults.buttonTextKey; // use same key as text
|
|
if (buttonKey != null && buttonHints[buttonKey] != null) {
|
|
return buttonHints[buttonKey];
|
|
}
|
|
if (buttonHints[viewDef.type] != null) {
|
|
return buttonHints[viewDef.type];
|
|
}
|
|
if (buttonHints[singleUnit] != null) {
|
|
return buttonHints[singleUnit];
|
|
}
|
|
return null;
|
|
};
|
|
return {
|
|
type: viewDef.type,
|
|
component: viewDef.component,
|
|
duration,
|
|
durationUnit,
|
|
singleUnit,
|
|
optionDefaults: viewDef.defaults,
|
|
optionOverrides: Object.assign(Object.assign({}, singleUnitOverrides), viewDef.overrides),
|
|
buttonTextOverride: queryButtonText(dynamicOptionOverrides) ||
|
|
queryButtonText(optionOverrides) || // constructor-specified buttonText lookup hash takes precedence
|
|
viewDef.overrides.buttonText,
|
|
buttonTextDefault: queryButtonText(localeDefaults) ||
|
|
viewDef.defaults.buttonText ||
|
|
queryButtonText(BASE_OPTION_DEFAULTS) ||
|
|
viewDef.type,
|
|
// not DRY
|
|
buttonTitleOverride: queryButtonTitle(dynamicOptionOverrides) ||
|
|
queryButtonTitle(optionOverrides) ||
|
|
viewDef.overrides.buttonHint,
|
|
buttonTitleDefault: queryButtonTitle(localeDefaults) ||
|
|
viewDef.defaults.buttonHint ||
|
|
queryButtonTitle(BASE_OPTION_DEFAULTS),
|
|
// will eventually fall back to buttonText
|
|
};
|
|
}
|
|
// hack to get memoization working
|
|
let durationInputMap = {};
|
|
function createDurationCached(durationInput) {
|
|
let json = JSON.stringify(durationInput);
|
|
let res = durationInputMap[json];
|
|
if (res === undefined) {
|
|
res = createDuration(durationInput);
|
|
durationInputMap[json] = res;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function reduceViewType(viewType, action) {
|
|
switch (action.type) {
|
|
case 'CHANGE_VIEW_TYPE':
|
|
viewType = action.viewType;
|
|
}
|
|
return viewType;
|
|
}
|
|
|
|
function reduceCurrentDate(currentDate, action) {
|
|
switch (action.type) {
|
|
case 'CHANGE_DATE':
|
|
return action.dateMarker;
|
|
default:
|
|
return currentDate;
|
|
}
|
|
}
|
|
// should be initialized once and stay constant
|
|
// this will change too
|
|
function getInitialDate(options, dateEnv, nowManager) {
|
|
let initialDateInput = options.initialDate;
|
|
// compute the initial ambig-timezone date
|
|
if (initialDateInput != null) {
|
|
return dateEnv.createMarker(initialDateInput);
|
|
}
|
|
return nowManager.getDateMarker();
|
|
}
|
|
|
|
function reduceDynamicOptionOverrides(dynamicOptionOverrides, action) {
|
|
switch (action.type) {
|
|
case 'SET_OPTION':
|
|
return Object.assign(Object.assign({}, dynamicOptionOverrides), { [action.optionName]: action.rawOptionValue });
|
|
default:
|
|
return dynamicOptionOverrides;
|
|
}
|
|
}
|
|
|
|
function reduceDateProfile(currentDateProfile, action, currentDate, dateProfileGenerator) {
|
|
let dp;
|
|
switch (action.type) {
|
|
case 'CHANGE_VIEW_TYPE':
|
|
return dateProfileGenerator.build(action.dateMarker || currentDate);
|
|
case 'CHANGE_DATE':
|
|
return dateProfileGenerator.build(action.dateMarker);
|
|
case 'PREV':
|
|
dp = dateProfileGenerator.buildPrev(currentDateProfile, currentDate);
|
|
if (dp.isValid) {
|
|
return dp;
|
|
}
|
|
break;
|
|
case 'NEXT':
|
|
dp = dateProfileGenerator.buildNext(currentDateProfile, currentDate);
|
|
if (dp.isValid) {
|
|
return dp;
|
|
}
|
|
break;
|
|
}
|
|
return currentDateProfile;
|
|
}
|
|
|
|
function initEventSources(calendarOptions, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null;
|
|
return addSources({}, parseInitialSources(calendarOptions, context), activeRange, context);
|
|
}
|
|
function reduceEventSources(eventSources, action, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
|
|
switch (action.type) {
|
|
case 'ADD_EVENT_SOURCES': // already parsed
|
|
return addSources(eventSources, action.sources, activeRange, context);
|
|
case 'REMOVE_EVENT_SOURCE':
|
|
return removeSource(eventSources, action.sourceId);
|
|
case 'PREV': // TODO: how do we track all actions that affect dateProfile :(
|
|
case 'NEXT':
|
|
case 'CHANGE_DATE':
|
|
case 'CHANGE_VIEW_TYPE':
|
|
if (dateProfile) {
|
|
return fetchDirtySources(eventSources, activeRange, context);
|
|
}
|
|
return eventSources;
|
|
case 'FETCH_EVENT_SOURCES':
|
|
return fetchSourcesByIds(eventSources, action.sourceIds ? // why no type?
|
|
arrayToHash(action.sourceIds) :
|
|
excludeStaticSources(eventSources, context), activeRange, action.isRefetch || false, context);
|
|
case 'RECEIVE_EVENTS':
|
|
case 'RECEIVE_EVENT_ERROR':
|
|
return receiveResponse(eventSources, action.sourceId, action.fetchId, action.fetchRange);
|
|
case 'REMOVE_ALL_EVENT_SOURCES':
|
|
return {};
|
|
default:
|
|
return eventSources;
|
|
}
|
|
}
|
|
function reduceEventSourcesNewTimeZone(eventSources, dateProfile, context) {
|
|
let activeRange = dateProfile ? dateProfile.activeRange : null; // need this check?
|
|
return fetchSourcesByIds(eventSources, excludeStaticSources(eventSources, context), activeRange, true, context);
|
|
}
|
|
function computeEventSourcesLoading(eventSources) {
|
|
for (let sourceId in eventSources) {
|
|
if (eventSources[sourceId].isFetching) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function addSources(eventSourceHash, sources, fetchRange, context) {
|
|
let hash = {};
|
|
for (let source of sources) {
|
|
hash[source.sourceId] = source;
|
|
}
|
|
if (fetchRange) {
|
|
hash = fetchDirtySources(hash, fetchRange, context);
|
|
}
|
|
return Object.assign(Object.assign({}, eventSourceHash), hash);
|
|
}
|
|
function removeSource(eventSourceHash, sourceId) {
|
|
return filterHash(eventSourceHash, (eventSource) => eventSource.sourceId !== sourceId);
|
|
}
|
|
function fetchDirtySources(sourceHash, fetchRange, context) {
|
|
return fetchSourcesByIds(sourceHash, filterHash(sourceHash, (eventSource) => isSourceDirty(eventSource, fetchRange, context)), fetchRange, false, context);
|
|
}
|
|
function isSourceDirty(eventSource, fetchRange, context) {
|
|
if (!doesSourceNeedRange(eventSource, context)) {
|
|
return !eventSource.latestFetchId;
|
|
}
|
|
return !context.options.lazyFetching ||
|
|
!eventSource.fetchRange ||
|
|
eventSource.isFetching || // always cancel outdated in-progress fetches
|
|
fetchRange.start < eventSource.fetchRange.start ||
|
|
fetchRange.end > eventSource.fetchRange.end;
|
|
}
|
|
function fetchSourcesByIds(prevSources, sourceIdHash, fetchRange, isRefetch, context) {
|
|
let nextSources = {};
|
|
for (let sourceId in prevSources) {
|
|
let source = prevSources[sourceId];
|
|
if (sourceIdHash[sourceId]) {
|
|
nextSources[sourceId] = fetchSource(source, fetchRange, isRefetch, context);
|
|
}
|
|
else {
|
|
nextSources[sourceId] = source;
|
|
}
|
|
}
|
|
return nextSources;
|
|
}
|
|
function fetchSource(eventSource, fetchRange, isRefetch, context) {
|
|
let { options, calendarApi } = context;
|
|
let sourceDef = context.pluginHooks.eventSourceDefs[eventSource.sourceDefId];
|
|
let fetchId = guid();
|
|
sourceDef.fetch({
|
|
eventSource,
|
|
range: fetchRange,
|
|
isRefetch,
|
|
context,
|
|
}, (res) => {
|
|
let { rawEvents } = res;
|
|
if (options.eventSourceSuccess) {
|
|
rawEvents = options.eventSourceSuccess.call(calendarApi, rawEvents, res.response) || rawEvents;
|
|
}
|
|
if (eventSource.success) {
|
|
rawEvents = eventSource.success.call(calendarApi, rawEvents, res.response) || rawEvents;
|
|
}
|
|
context.dispatch({
|
|
type: 'RECEIVE_EVENTS',
|
|
sourceId: eventSource.sourceId,
|
|
fetchId,
|
|
fetchRange,
|
|
rawEvents,
|
|
});
|
|
}, (error) => {
|
|
let errorHandled = false;
|
|
if (options.eventSourceFailure) {
|
|
options.eventSourceFailure.call(calendarApi, error);
|
|
errorHandled = true;
|
|
}
|
|
if (eventSource.failure) {
|
|
eventSource.failure(error);
|
|
errorHandled = true;
|
|
}
|
|
if (!errorHandled) {
|
|
console.warn(error.message, error);
|
|
}
|
|
context.dispatch({
|
|
type: 'RECEIVE_EVENT_ERROR',
|
|
sourceId: eventSource.sourceId,
|
|
fetchId,
|
|
fetchRange,
|
|
error,
|
|
});
|
|
});
|
|
return Object.assign(Object.assign({}, eventSource), { isFetching: true, latestFetchId: fetchId });
|
|
}
|
|
function receiveResponse(sourceHash, sourceId, fetchId, fetchRange) {
|
|
let eventSource = sourceHash[sourceId];
|
|
if (eventSource && // not already removed
|
|
fetchId === eventSource.latestFetchId) {
|
|
return Object.assign(Object.assign({}, sourceHash), { [sourceId]: Object.assign(Object.assign({}, eventSource), { isFetching: false, fetchRange }) });
|
|
}
|
|
return sourceHash;
|
|
}
|
|
function excludeStaticSources(eventSources, context) {
|
|
return filterHash(eventSources, (eventSource) => doesSourceNeedRange(eventSource, context));
|
|
}
|
|
function parseInitialSources(rawOptions, context) {
|
|
let refiners = buildEventSourceRefiners(context);
|
|
let rawSources = [].concat(rawOptions.eventSources || []);
|
|
let sources = []; // parsed
|
|
if (rawOptions.initialEvents) {
|
|
rawSources.unshift(rawOptions.initialEvents);
|
|
}
|
|
if (rawOptions.events) {
|
|
rawSources.unshift(rawOptions.events);
|
|
}
|
|
for (let rawSource of rawSources) {
|
|
let source = parseEventSource(rawSource, context, refiners);
|
|
if (source) {
|
|
sources.push(source);
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
function doesSourceNeedRange(eventSource, context) {
|
|
let defs = context.pluginHooks.eventSourceDefs;
|
|
return !defs[eventSource.sourceDefId].ignoreRange;
|
|
}
|
|
|
|
function reduceDateSelection(currentSelection, action) {
|
|
switch (action.type) {
|
|
case 'UNSELECT_DATES':
|
|
return null;
|
|
case 'SELECT_DATES':
|
|
return action.selection;
|
|
default:
|
|
return currentSelection;
|
|
}
|
|
}
|
|
|
|
function reduceSelectedEvent(currentInstanceId, action) {
|
|
switch (action.type) {
|
|
case 'UNSELECT_EVENT':
|
|
return '';
|
|
case 'SELECT_EVENT':
|
|
return action.eventInstanceId;
|
|
default:
|
|
return currentInstanceId;
|
|
}
|
|
}
|
|
|
|
function reduceEventDrag(currentDrag, action) {
|
|
let newDrag;
|
|
switch (action.type) {
|
|
case 'UNSET_EVENT_DRAG':
|
|
return null;
|
|
case 'SET_EVENT_DRAG':
|
|
newDrag = action.state;
|
|
return {
|
|
affectedEvents: newDrag.affectedEvents,
|
|
mutatedEvents: newDrag.mutatedEvents,
|
|
isEvent: newDrag.isEvent,
|
|
};
|
|
default:
|
|
return currentDrag;
|
|
}
|
|
}
|
|
|
|
function reduceEventResize(currentResize, action) {
|
|
let newResize;
|
|
switch (action.type) {
|
|
case 'UNSET_EVENT_RESIZE':
|
|
return null;
|
|
case 'SET_EVENT_RESIZE':
|
|
newResize = action.state;
|
|
return {
|
|
affectedEvents: newResize.affectedEvents,
|
|
mutatedEvents: newResize.mutatedEvents,
|
|
isEvent: newResize.isEvent,
|
|
};
|
|
default:
|
|
return currentResize;
|
|
}
|
|
}
|
|
|
|
function parseToolbars(calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
|
|
let header = calendarOptions.headerToolbar ? parseToolbar(calendarOptions.headerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
|
|
let footer = calendarOptions.footerToolbar ? parseToolbar(calendarOptions.footerToolbar, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) : null;
|
|
return { header, footer };
|
|
}
|
|
function parseToolbar(sectionStrHash, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi) {
|
|
let sectionWidgets = {};
|
|
let viewsWithButtons = [];
|
|
let hasTitle = false;
|
|
for (let sectionName in sectionStrHash) {
|
|
let sectionStr = sectionStrHash[sectionName];
|
|
let sectionRes = parseSection(sectionStr, calendarOptions, calendarOptionOverrides, theme, viewSpecs, calendarApi);
|
|
sectionWidgets[sectionName] = sectionRes.widgets;
|
|
viewsWithButtons.push(...sectionRes.viewsWithButtons);
|
|
hasTitle = hasTitle || sectionRes.hasTitle;
|
|
}
|
|
return { sectionWidgets, viewsWithButtons, hasTitle };
|
|
}
|
|
/*
|
|
BAD: querying icons and text here. should be done at render time
|
|
*/
|
|
function parseSection(sectionStr, calendarOptions, // defaults+overrides, then refined
|
|
calendarOptionOverrides, // overrides only!, unrefined :(
|
|
theme, viewSpecs, calendarApi) {
|
|
let isRtl = calendarOptions.direction === 'rtl';
|
|
let calendarCustomButtons = calendarOptions.customButtons || {};
|
|
let calendarButtonTextOverrides = calendarOptionOverrides.buttonText || {};
|
|
let calendarButtonText = calendarOptions.buttonText || {};
|
|
let calendarButtonHintOverrides = calendarOptionOverrides.buttonHints || {};
|
|
let calendarButtonHints = calendarOptions.buttonHints || {};
|
|
let sectionSubstrs = sectionStr ? sectionStr.split(' ') : [];
|
|
let viewsWithButtons = [];
|
|
let hasTitle = false;
|
|
let widgets = sectionSubstrs.map((buttonGroupStr) => (buttonGroupStr.split(',').map((buttonName) => {
|
|
if (buttonName === 'title') {
|
|
hasTitle = true;
|
|
return { buttonName };
|
|
}
|
|
let customButtonProps;
|
|
let viewSpec;
|
|
let buttonClick;
|
|
let buttonIcon; // only one of these will be set
|
|
let buttonText; // "
|
|
let buttonHint;
|
|
// ^ for the title="" attribute, for accessibility
|
|
if ((customButtonProps = calendarCustomButtons[buttonName])) {
|
|
buttonClick = (ev) => {
|
|
if (customButtonProps.click) {
|
|
customButtonProps.click.call(ev.target, ev, ev.target); // TODO: use Calendar this context?
|
|
}
|
|
};
|
|
(buttonIcon = theme.getCustomButtonIconClass(customButtonProps)) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = customButtonProps.text);
|
|
buttonHint = customButtonProps.hint || customButtonProps.text;
|
|
}
|
|
else if ((viewSpec = viewSpecs[buttonName])) {
|
|
viewsWithButtons.push(buttonName);
|
|
buttonClick = () => {
|
|
calendarApi.changeView(buttonName);
|
|
};
|
|
(buttonText = viewSpec.buttonTextOverride) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = viewSpec.buttonTextDefault);
|
|
let textFallback = viewSpec.buttonTextOverride ||
|
|
viewSpec.buttonTextDefault;
|
|
buttonHint = formatWithOrdinals(viewSpec.buttonTitleOverride ||
|
|
viewSpec.buttonTitleDefault ||
|
|
calendarOptions.viewHint, [textFallback, buttonName], // view-name = buttonName
|
|
textFallback);
|
|
}
|
|
else if (calendarApi[buttonName]) { // a calendarApi method
|
|
buttonClick = () => {
|
|
calendarApi[buttonName]();
|
|
};
|
|
(buttonText = calendarButtonTextOverrides[buttonName]) ||
|
|
(buttonIcon = theme.getIconClass(buttonName, isRtl)) ||
|
|
(buttonText = calendarButtonText[buttonName]); // everything else is considered default
|
|
if (buttonName === 'prevYear' || buttonName === 'nextYear') {
|
|
let prevOrNext = buttonName === 'prevYear' ? 'prev' : 'next';
|
|
buttonHint = formatWithOrdinals(calendarButtonHintOverrides[prevOrNext] ||
|
|
calendarButtonHints[prevOrNext], [
|
|
calendarButtonText.year || 'year',
|
|
'year',
|
|
], calendarButtonText[buttonName]);
|
|
}
|
|
else {
|
|
buttonHint = (navUnit) => formatWithOrdinals(calendarButtonHintOverrides[buttonName] ||
|
|
calendarButtonHints[buttonName], [
|
|
calendarButtonText[navUnit] || navUnit,
|
|
navUnit,
|
|
], calendarButtonText[buttonName]);
|
|
}
|
|
}
|
|
return { buttonName, buttonClick, buttonIcon, buttonText, buttonHint };
|
|
})));
|
|
return { widgets, viewsWithButtons, hasTitle };
|
|
}
|
|
|
|
// always represents the current view. otherwise, it'd need to change value every time date changes
|
|
class ViewImpl {
|
|
constructor(type, getCurrentData, dateEnv) {
|
|
this.type = type;
|
|
this.getCurrentData = getCurrentData;
|
|
this.dateEnv = dateEnv;
|
|
}
|
|
get calendar() {
|
|
return this.getCurrentData().calendarApi;
|
|
}
|
|
get title() {
|
|
return this.getCurrentData().viewTitle;
|
|
}
|
|
get activeStart() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.start);
|
|
}
|
|
get activeEnd() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.activeRange.end);
|
|
}
|
|
get currentStart() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.start);
|
|
}
|
|
get currentEnd() {
|
|
return this.dateEnv.toDate(this.getCurrentData().dateProfile.currentRange.end);
|
|
}
|
|
getOption(name) {
|
|
return this.getCurrentData().options[name]; // are the view-specific options
|
|
}
|
|
}
|
|
|
|
let eventSourceDef$2 = {
|
|
ignoreRange: true,
|
|
parseMeta(refined) {
|
|
if (Array.isArray(refined.events)) {
|
|
return refined.events;
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback) {
|
|
successCallback({
|
|
rawEvents: arg.eventSource.meta,
|
|
});
|
|
},
|
|
};
|
|
const arrayEventSourcePlugin = createPlugin({
|
|
name: 'array-event-source',
|
|
eventSourceDefs: [eventSourceDef$2],
|
|
});
|
|
|
|
let eventSourceDef$1 = {
|
|
parseMeta(refined) {
|
|
if (typeof refined.events === 'function') {
|
|
return refined.events;
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback, errorCallback) {
|
|
const { dateEnv } = arg.context;
|
|
const func = arg.eventSource.meta;
|
|
unpromisify(func.bind(null, buildRangeApiWithTimeZone(arg.range, dateEnv)), (rawEvents) => successCallback({ rawEvents }), errorCallback);
|
|
},
|
|
};
|
|
const funcEventSourcePlugin = createPlugin({
|
|
name: 'func-event-source',
|
|
eventSourceDefs: [eventSourceDef$1],
|
|
});
|
|
|
|
const JSON_FEED_EVENT_SOURCE_REFINERS = {
|
|
method: String,
|
|
extraParams: identity,
|
|
startParam: String,
|
|
endParam: String,
|
|
timeZoneParam: String,
|
|
};
|
|
|
|
let eventSourceDef = {
|
|
parseMeta(refined) {
|
|
if (refined.url && (refined.format === 'json' || !refined.format)) {
|
|
return {
|
|
url: refined.url,
|
|
format: 'json',
|
|
method: (refined.method || 'GET').toUpperCase(),
|
|
extraParams: refined.extraParams,
|
|
startParam: refined.startParam,
|
|
endParam: refined.endParam,
|
|
timeZoneParam: refined.timeZoneParam,
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
fetch(arg, successCallback, errorCallback) {
|
|
const { meta } = arg.eventSource;
|
|
const requestParams = buildRequestParams(meta, arg.range, arg.context);
|
|
requestJson(meta.method, meta.url, requestParams).then(([rawEvents, response]) => {
|
|
successCallback({ rawEvents, response });
|
|
}, errorCallback);
|
|
},
|
|
};
|
|
const jsonFeedEventSourcePlugin = createPlugin({
|
|
name: 'json-event-source',
|
|
eventSourceRefiners: JSON_FEED_EVENT_SOURCE_REFINERS,
|
|
eventSourceDefs: [eventSourceDef],
|
|
});
|
|
function buildRequestParams(meta, range, context) {
|
|
let { dateEnv, options } = context;
|
|
let startParam;
|
|
let endParam;
|
|
let timeZoneParam;
|
|
let customRequestParams;
|
|
let params = {};
|
|
startParam = meta.startParam;
|
|
if (startParam == null) {
|
|
startParam = options.startParam;
|
|
}
|
|
endParam = meta.endParam;
|
|
if (endParam == null) {
|
|
endParam = options.endParam;
|
|
}
|
|
timeZoneParam = meta.timeZoneParam;
|
|
if (timeZoneParam == null) {
|
|
timeZoneParam = options.timeZoneParam;
|
|
}
|
|
// retrieve any outbound GET/POST data from the options
|
|
if (typeof meta.extraParams === 'function') {
|
|
// supplied as a function that returns a key/value object
|
|
customRequestParams = meta.extraParams();
|
|
}
|
|
else {
|
|
// probably supplied as a straight key/value object
|
|
customRequestParams = meta.extraParams || {};
|
|
}
|
|
Object.assign(params, customRequestParams);
|
|
params[startParam] = dateEnv.formatIso(range.start);
|
|
params[endParam] = dateEnv.formatIso(range.end);
|
|
if (dateEnv.timeZone !== 'local') {
|
|
params[timeZoneParam] = dateEnv.timeZone;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
const SIMPLE_RECURRING_REFINERS = {
|
|
daysOfWeek: identity,
|
|
startTime: createDuration,
|
|
endTime: createDuration,
|
|
duration: createDuration,
|
|
startRecur: identity,
|
|
endRecur: identity,
|
|
};
|
|
|
|
let recurring = {
|
|
parse(refined, dateEnv) {
|
|
if (refined.daysOfWeek || refined.startTime || refined.endTime || refined.startRecur || refined.endRecur) {
|
|
let recurringData = {
|
|
daysOfWeek: refined.daysOfWeek || null,
|
|
startTime: refined.startTime || null,
|
|
endTime: refined.endTime || null,
|
|
startRecur: refined.startRecur ? dateEnv.createMarker(refined.startRecur) : null,
|
|
endRecur: refined.endRecur ? dateEnv.createMarker(refined.endRecur) : null,
|
|
dateEnv,
|
|
};
|
|
let duration;
|
|
if (refined.duration) {
|
|
duration = refined.duration;
|
|
}
|
|
if (!duration && refined.startTime && refined.endTime) {
|
|
duration = subtractDurations(refined.endTime, refined.startTime);
|
|
}
|
|
return {
|
|
allDayGuess: Boolean(!refined.startTime && !refined.endTime),
|
|
duration,
|
|
typeData: recurringData, // doesn't need endTime anymore but oh well
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
expand(typeData, framingRange, dateEnv) {
|
|
let clippedFramingRange = intersectRanges(framingRange, { start: typeData.startRecur, end: typeData.endRecur });
|
|
if (clippedFramingRange) {
|
|
return expandRanges(typeData.daysOfWeek, typeData.startTime, typeData.dateEnv, dateEnv, clippedFramingRange);
|
|
}
|
|
return [];
|
|
},
|
|
};
|
|
const simpleRecurringEventsPlugin = createPlugin({
|
|
name: 'simple-recurring-event',
|
|
recurringTypes: [recurring],
|
|
eventRefiners: SIMPLE_RECURRING_REFINERS,
|
|
});
|
|
function expandRanges(daysOfWeek, startTime, eventDateEnv, calendarDateEnv, framingRange) {
|
|
let dowHash = daysOfWeek ? arrayToHash(daysOfWeek) : null;
|
|
let dayMarker = startOfDay(framingRange.start);
|
|
let endMarker = framingRange.end;
|
|
let instanceStarts = [];
|
|
// https://github.com/fullcalendar/fullcalendar/issues/7934
|
|
if (startTime) {
|
|
if (startTime.milliseconds < 0) {
|
|
// possible for next-day to have negative business hours that go into current day
|
|
endMarker = addDays(endMarker, 1);
|
|
}
|
|
else if (startTime.milliseconds >= 1000 * 60 * 60 * 24) {
|
|
// possible for prev-day to have >24hr business hours that go into current day
|
|
dayMarker = addDays(dayMarker, -1);
|
|
}
|
|
}
|
|
while (dayMarker < endMarker) {
|
|
let instanceStart;
|
|
// if everyday, or this particular day-of-week
|
|
if (!dowHash || dowHash[dayMarker.getUTCDay()]) {
|
|
if (startTime) {
|
|
instanceStart = calendarDateEnv.add(dayMarker, startTime);
|
|
}
|
|
else {
|
|
instanceStart = dayMarker;
|
|
}
|
|
instanceStarts.push(calendarDateEnv.createMarker(eventDateEnv.toDate(instanceStart)));
|
|
}
|
|
dayMarker = addDays(dayMarker, 1);
|
|
}
|
|
return instanceStarts;
|
|
}
|
|
|
|
const changeHandlerPlugin = createPlugin({
|
|
name: 'change-handler',
|
|
optionChangeHandlers: {
|
|
events(events, context) {
|
|
handleEventSources([events], context);
|
|
},
|
|
eventSources: handleEventSources,
|
|
},
|
|
});
|
|
/*
|
|
BUG: if `event` was supplied, all previously-given `eventSources` will be wiped out
|
|
*/
|
|
function handleEventSources(inputs, context) {
|
|
let unfoundSources = hashValuesToArray(context.getCurrentData().eventSources);
|
|
if (unfoundSources.length === 1 &&
|
|
inputs.length === 1 &&
|
|
Array.isArray(unfoundSources[0]._raw) &&
|
|
Array.isArray(inputs[0])) {
|
|
context.dispatch({
|
|
type: 'RESET_RAW_EVENTS',
|
|
sourceId: unfoundSources[0].sourceId,
|
|
rawEvents: inputs[0],
|
|
});
|
|
return;
|
|
}
|
|
let newInputs = [];
|
|
for (let input of inputs) {
|
|
let inputFound = false;
|
|
for (let i = 0; i < unfoundSources.length; i += 1) {
|
|
if (unfoundSources[i]._raw === input) {
|
|
unfoundSources.splice(i, 1); // delete
|
|
inputFound = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inputFound) {
|
|
newInputs.push(input);
|
|
}
|
|
}
|
|
for (let unfoundSource of unfoundSources) {
|
|
context.dispatch({
|
|
type: 'REMOVE_EVENT_SOURCE',
|
|
sourceId: unfoundSource.sourceId,
|
|
});
|
|
}
|
|
for (let newInput of newInputs) {
|
|
context.calendarApi.addEventSource(newInput);
|
|
}
|
|
}
|
|
|
|
function handleDateProfile(dateProfile, context) {
|
|
context.emitter.trigger('datesSet', Object.assign(Object.assign({}, buildRangeApiWithTimeZone(dateProfile.activeRange, context.dateEnv)), { view: context.viewApi }));
|
|
}
|
|
|
|
function handleEventStore(eventStore, context) {
|
|
let { emitter } = context;
|
|
if (emitter.hasHandlers('eventsSet')) {
|
|
emitter.trigger('eventsSet', buildEventApis(eventStore, context));
|
|
}
|
|
}
|
|
|
|
/*
|
|
this array is exposed on the root namespace so that UMD plugins can add to it.
|
|
see the rollup-bundles script.
|
|
*/
|
|
const globalPlugins = [
|
|
arrayEventSourcePlugin,
|
|
funcEventSourcePlugin,
|
|
jsonFeedEventSourcePlugin,
|
|
simpleRecurringEventsPlugin,
|
|
changeHandlerPlugin,
|
|
createPlugin({
|
|
name: 'misc',
|
|
isLoadingFuncs: [
|
|
(state) => computeEventSourcesLoading(state.eventSources),
|
|
],
|
|
propSetHandlers: {
|
|
dateProfile: handleDateProfile,
|
|
eventStore: handleEventStore,
|
|
},
|
|
}),
|
|
];
|
|
|
|
class TaskRunner {
|
|
constructor(runTaskOption, drainedOption) {
|
|
this.runTaskOption = runTaskOption;
|
|
this.drainedOption = drainedOption;
|
|
this.queue = [];
|
|
this.delayedRunner = new DelayedRunner(this.drain.bind(this));
|
|
}
|
|
request(task, delay) {
|
|
this.queue.push(task);
|
|
this.delayedRunner.request(delay);
|
|
}
|
|
pause(scope) {
|
|
this.delayedRunner.pause(scope);
|
|
}
|
|
resume(scope, force) {
|
|
this.delayedRunner.resume(scope, force);
|
|
}
|
|
drain() {
|
|
let { queue } = this;
|
|
while (queue.length) {
|
|
let completedTasks = [];
|
|
let task;
|
|
while ((task = queue.shift())) {
|
|
this.runTask(task);
|
|
completedTasks.push(task);
|
|
}
|
|
this.drained(completedTasks);
|
|
} // keep going, in case new tasks were added in the drained handler
|
|
}
|
|
runTask(task) {
|
|
if (this.runTaskOption) {
|
|
this.runTaskOption(task);
|
|
}
|
|
}
|
|
drained(completedTasks) {
|
|
if (this.drainedOption) {
|
|
this.drainedOption(completedTasks);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computes what the title at the top of the calendarApi should be for this view
|
|
function buildTitle(dateProfile, viewOptions, dateEnv) {
|
|
let range;
|
|
// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
|
|
if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
|
|
range = dateProfile.currentRange;
|
|
}
|
|
else { // for day units or smaller, use the actual day range
|
|
range = dateProfile.activeRange;
|
|
}
|
|
return dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || buildTitleFormat(dateProfile)), {
|
|
isEndExclusive: dateProfile.isRangeAllDay,
|
|
defaultSeparator: viewOptions.titleRangeSeparator,
|
|
});
|
|
}
|
|
// Generates the format string that should be used to generate the title for the current date range.
|
|
// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
|
|
function buildTitleFormat(dateProfile) {
|
|
let { currentRangeUnit } = dateProfile;
|
|
if (currentRangeUnit === 'year') {
|
|
return { year: 'numeric' };
|
|
}
|
|
if (currentRangeUnit === 'month') {
|
|
return { year: 'numeric', month: 'long' }; // like "September 2014"
|
|
}
|
|
let days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end);
|
|
if (days !== null && days > 1) {
|
|
// multi-day range. shorter, like "Sep 9 - 10 2014"
|
|
return { year: 'numeric', month: 'short', day: 'numeric' };
|
|
}
|
|
// one day. longer, like "September 9 2014"
|
|
return { year: 'numeric', month: 'long', day: 'numeric' };
|
|
}
|
|
|
|
/*
|
|
TODO: test switching timezones when NO timezone plugin
|
|
*/
|
|
class CalendarNowManager {
|
|
constructor() {
|
|
this.resetListeners = new Set();
|
|
}
|
|
handleInput(dateEnv, // will change if timezone setup changed
|
|
nowInput) {
|
|
const oldDateEnv = this.dateEnv;
|
|
if (dateEnv !== oldDateEnv) {
|
|
if (typeof nowInput === 'function') {
|
|
this.nowFn = nowInput;
|
|
}
|
|
else if (!oldDateEnv) { // first time?
|
|
this.nowAnchorDate = dateEnv.toDate(nowInput
|
|
? dateEnv.createMarker(nowInput)
|
|
: dateEnv.createNowMarker());
|
|
this.nowAnchorQueried = Date.now();
|
|
}
|
|
this.dateEnv = dateEnv;
|
|
// not first time? fire reset handlers
|
|
if (oldDateEnv) {
|
|
for (const resetListener of this.resetListeners.values()) {
|
|
resetListener();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
getDateMarker() {
|
|
return this.nowAnchorDate
|
|
? this.dateEnv.timestampToMarker(this.nowAnchorDate.valueOf() +
|
|
(Date.now() - this.nowAnchorQueried))
|
|
: this.dateEnv.createMarker(this.nowFn());
|
|
}
|
|
addResetListener(handler) {
|
|
this.resetListeners.add(handler);
|
|
}
|
|
removeResetListener(handler) {
|
|
this.resetListeners.delete(handler);
|
|
}
|
|
}
|
|
|
|
// in future refactor, do the redux-style function(state=initial) for initial-state
|
|
// also, whatever is happening in constructor, have it happen in action queue too
|
|
class CalendarDataManager {
|
|
constructor(props) {
|
|
this.computeCurrentViewData = memoize(this._computeCurrentViewData);
|
|
this.organizeRawLocales = memoize(organizeRawLocales);
|
|
this.buildLocale = memoize(buildLocale);
|
|
this.buildPluginHooks = buildBuildPluginHooks();
|
|
this.buildDateEnv = memoize(buildDateEnv$1);
|
|
this.buildTheme = memoize(buildTheme);
|
|
this.parseToolbars = memoize(parseToolbars);
|
|
this.buildViewSpecs = memoize(buildViewSpecs);
|
|
this.buildDateProfileGenerator = memoizeObjArg(buildDateProfileGenerator);
|
|
this.buildViewApi = memoize(buildViewApi);
|
|
this.buildViewUiProps = memoizeObjArg(buildViewUiProps);
|
|
this.buildEventUiBySource = memoize(buildEventUiBySource, isPropsEqual);
|
|
this.buildEventUiBases = memoize(buildEventUiBases);
|
|
this.parseContextBusinessHours = memoizeObjArg(parseContextBusinessHours);
|
|
this.buildTitle = memoize(buildTitle);
|
|
this.nowManager = new CalendarNowManager();
|
|
this.emitter = new Emitter();
|
|
this.actionRunner = new TaskRunner(this._handleAction.bind(this), this.updateData.bind(this));
|
|
this.currentCalendarOptionsInput = {};
|
|
this.currentCalendarOptionsRefined = {};
|
|
this.currentViewOptionsInput = {};
|
|
this.currentViewOptionsRefined = {};
|
|
this.currentCalendarOptionsRefiners = {};
|
|
this.optionsForRefining = [];
|
|
this.optionsForHandling = [];
|
|
this.getCurrentData = () => this.data;
|
|
this.dispatch = (action) => {
|
|
this.actionRunner.request(action); // protects against recursive calls to _handleAction
|
|
};
|
|
this.props = props;
|
|
this.actionRunner.pause();
|
|
this.nowManager = new CalendarNowManager();
|
|
let dynamicOptionOverrides = {};
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewType = optionsData.calendarOptions.initialView || optionsData.pluginHooks.initialView;
|
|
let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
|
|
// wire things up
|
|
// TODO: not DRY
|
|
props.calendarApi.currentDataManager = this;
|
|
this.emitter.setThisContext(props.calendarApi);
|
|
this.emitter.setOptions(currentViewData.options);
|
|
let calendarContext = {
|
|
nowManager: this.nowManager,
|
|
dateEnv: optionsData.dateEnv,
|
|
options: optionsData.calendarOptions,
|
|
pluginHooks: optionsData.pluginHooks,
|
|
calendarApi: props.calendarApi,
|
|
dispatch: this.dispatch,
|
|
emitter: this.emitter,
|
|
getCurrentData: this.getCurrentData,
|
|
};
|
|
let currentDate = getInitialDate(optionsData.calendarOptions, optionsData.dateEnv, this.nowManager);
|
|
let dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
|
|
if (!rangeContainsMarker(dateProfile.activeRange, currentDate)) {
|
|
currentDate = dateProfile.currentRange.start;
|
|
}
|
|
// needs to be after setThisContext
|
|
for (let callback of optionsData.pluginHooks.contextInit) {
|
|
callback(calendarContext);
|
|
}
|
|
// NOT DRY
|
|
let eventSources = initEventSources(optionsData.calendarOptions, dateProfile, calendarContext);
|
|
let initialState = {
|
|
dynamicOptionOverrides,
|
|
currentViewType,
|
|
currentDate,
|
|
dateProfile,
|
|
businessHours: this.parseContextBusinessHours(calendarContext),
|
|
eventSources,
|
|
eventUiBases: {},
|
|
eventStore: createEmptyEventStore(),
|
|
renderableEventStore: createEmptyEventStore(),
|
|
dateSelection: null,
|
|
eventSelection: '',
|
|
eventDrag: null,
|
|
eventResize: null,
|
|
selectionConfig: this.buildViewUiProps(calendarContext).selectionConfig,
|
|
};
|
|
let contextAndState = Object.assign(Object.assign({}, calendarContext), initialState);
|
|
for (let reducer of optionsData.pluginHooks.reducers) {
|
|
Object.assign(initialState, reducer(null, null, contextAndState));
|
|
}
|
|
if (computeIsLoading(initialState, calendarContext)) {
|
|
this.emitter.trigger('loading', true); // NOT DRY
|
|
}
|
|
this.state = initialState;
|
|
this.updateData();
|
|
this.actionRunner.resume();
|
|
}
|
|
resetOptions(optionOverrides, changedOptionNames) {
|
|
let { props } = this;
|
|
if (changedOptionNames === undefined) {
|
|
props.optionOverrides = optionOverrides;
|
|
}
|
|
else {
|
|
props.optionOverrides = Object.assign(Object.assign({}, (props.optionOverrides || {})), optionOverrides);
|
|
this.optionsForRefining.push(...changedOptionNames);
|
|
}
|
|
if (changedOptionNames === undefined || changedOptionNames.length) {
|
|
this.actionRunner.request({
|
|
type: 'NOTHING',
|
|
});
|
|
}
|
|
}
|
|
_handleAction(action) {
|
|
let { props, state, emitter } = this;
|
|
let dynamicOptionOverrides = reduceDynamicOptionOverrides(state.dynamicOptionOverrides, action);
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewType = reduceViewType(state.currentViewType, action);
|
|
let currentViewData = this.computeCurrentViewData(currentViewType, optionsData, props.optionOverrides, dynamicOptionOverrides);
|
|
// wire things up
|
|
// TODO: not DRY
|
|
props.calendarApi.currentDataManager = this;
|
|
emitter.setThisContext(props.calendarApi);
|
|
emitter.setOptions(currentViewData.options);
|
|
let calendarContext = {
|
|
nowManager: this.nowManager,
|
|
dateEnv: optionsData.dateEnv,
|
|
options: optionsData.calendarOptions,
|
|
pluginHooks: optionsData.pluginHooks,
|
|
calendarApi: props.calendarApi,
|
|
dispatch: this.dispatch,
|
|
emitter,
|
|
getCurrentData: this.getCurrentData,
|
|
};
|
|
let { currentDate, dateProfile } = state;
|
|
if (this.data && this.data.dateProfileGenerator !== currentViewData.dateProfileGenerator) { // hack
|
|
dateProfile = currentViewData.dateProfileGenerator.build(currentDate);
|
|
}
|
|
currentDate = reduceCurrentDate(currentDate, action);
|
|
dateProfile = reduceDateProfile(dateProfile, action, currentDate, currentViewData.dateProfileGenerator);
|
|
if (action.type === 'PREV' || // TODO: move this logic into DateProfileGenerator
|
|
action.type === 'NEXT' || // "
|
|
!rangeContainsMarker(dateProfile.currentRange, currentDate)) {
|
|
currentDate = dateProfile.currentRange.start;
|
|
}
|
|
let eventSources = reduceEventSources(state.eventSources, action, dateProfile, calendarContext);
|
|
let eventStore = reduceEventStore(state.eventStore, action, eventSources, dateProfile, calendarContext);
|
|
let isEventsLoading = computeEventSourcesLoading(eventSources); // BAD. also called in this func in computeIsLoading
|
|
let renderableEventStore = (isEventsLoading && !currentViewData.options.progressiveEventRendering) ?
|
|
(state.renderableEventStore || eventStore) : // try from previous state
|
|
eventStore;
|
|
let { eventUiSingleBase, selectionConfig } = this.buildViewUiProps(calendarContext); // will memoize obj
|
|
let eventUiBySource = this.buildEventUiBySource(eventSources);
|
|
let eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource);
|
|
let newState = {
|
|
dynamicOptionOverrides,
|
|
currentViewType,
|
|
currentDate,
|
|
dateProfile,
|
|
eventSources,
|
|
eventStore,
|
|
renderableEventStore,
|
|
selectionConfig,
|
|
eventUiBases,
|
|
businessHours: this.parseContextBusinessHours(calendarContext),
|
|
dateSelection: reduceDateSelection(state.dateSelection, action),
|
|
eventSelection: reduceSelectedEvent(state.eventSelection, action),
|
|
eventDrag: reduceEventDrag(state.eventDrag, action),
|
|
eventResize: reduceEventResize(state.eventResize, action),
|
|
};
|
|
let contextAndState = Object.assign(Object.assign({}, calendarContext), newState);
|
|
for (let reducer of optionsData.pluginHooks.reducers) {
|
|
Object.assign(newState, reducer(state, action, contextAndState)); // give the OLD state, for old value
|
|
}
|
|
let wasLoading = computeIsLoading(state, calendarContext);
|
|
let isLoading = computeIsLoading(newState, calendarContext);
|
|
// TODO: use propSetHandlers in plugin system
|
|
if (!wasLoading && isLoading) {
|
|
emitter.trigger('loading', true);
|
|
}
|
|
else if (wasLoading && !isLoading) {
|
|
emitter.trigger('loading', false);
|
|
}
|
|
this.state = newState;
|
|
if (props.onAction) {
|
|
props.onAction(action);
|
|
}
|
|
}
|
|
updateData() {
|
|
let { props, state } = this;
|
|
let oldData = this.data;
|
|
let optionsData = this.computeOptionsData(props.optionOverrides, state.dynamicOptionOverrides, props.calendarApi);
|
|
let currentViewData = this.computeCurrentViewData(state.currentViewType, optionsData, props.optionOverrides, state.dynamicOptionOverrides);
|
|
let data = this.data = Object.assign(Object.assign(Object.assign({ nowManager: this.nowManager, viewTitle: this.buildTitle(state.dateProfile, currentViewData.options, optionsData.dateEnv), calendarApi: props.calendarApi, dispatch: this.dispatch, emitter: this.emitter, getCurrentData: this.getCurrentData }, optionsData), currentViewData), state);
|
|
let changeHandlers = optionsData.pluginHooks.optionChangeHandlers;
|
|
let oldCalendarOptions = oldData && oldData.calendarOptions;
|
|
let newCalendarOptions = optionsData.calendarOptions;
|
|
if (oldCalendarOptions && oldCalendarOptions !== newCalendarOptions) {
|
|
if (oldCalendarOptions.timeZone !== newCalendarOptions.timeZone) {
|
|
// hack
|
|
state.eventSources = data.eventSources = reduceEventSourcesNewTimeZone(data.eventSources, state.dateProfile, data);
|
|
state.eventStore = data.eventStore = rezoneEventStoreDates(data.eventStore, oldData.dateEnv, data.dateEnv);
|
|
state.renderableEventStore = data.renderableEventStore = rezoneEventStoreDates(data.renderableEventStore, oldData.dateEnv, data.dateEnv);
|
|
}
|
|
for (let optionName in changeHandlers) {
|
|
if (this.optionsForHandling.indexOf(optionName) !== -1 ||
|
|
oldCalendarOptions[optionName] !== newCalendarOptions[optionName]) {
|
|
changeHandlers[optionName](newCalendarOptions[optionName], data);
|
|
}
|
|
}
|
|
}
|
|
this.optionsForHandling = [];
|
|
if (props.onData) {
|
|
props.onData(data);
|
|
}
|
|
}
|
|
computeOptionsData(optionOverrides, dynamicOptionOverrides, calendarApi) {
|
|
// TODO: blacklist options that are handled by optionChangeHandlers
|
|
if (!this.optionsForRefining.length &&
|
|
optionOverrides === this.stableOptionOverrides &&
|
|
dynamicOptionOverrides === this.stableDynamicOptionOverrides) {
|
|
return this.stableCalendarOptionsData;
|
|
}
|
|
let { refinedOptions, pluginHooks, localeDefaults, availableLocaleData, extra, } = this.processRawCalendarOptions(optionOverrides, dynamicOptionOverrides);
|
|
warnUnknownOptions(extra);
|
|
let dateEnv = this.buildDateEnv(refinedOptions.timeZone, refinedOptions.locale, refinedOptions.weekNumberCalculation, refinedOptions.firstDay, refinedOptions.weekText, pluginHooks, availableLocaleData, refinedOptions.defaultRangeSeparator);
|
|
let viewSpecs = this.buildViewSpecs(pluginHooks.views, this.stableOptionOverrides, this.stableDynamicOptionOverrides, localeDefaults);
|
|
let theme = this.buildTheme(refinedOptions, pluginHooks);
|
|
let toolbarConfig = this.parseToolbars(refinedOptions, this.stableOptionOverrides, theme, viewSpecs, calendarApi);
|
|
return this.stableCalendarOptionsData = {
|
|
calendarOptions: refinedOptions,
|
|
pluginHooks,
|
|
dateEnv,
|
|
viewSpecs,
|
|
theme,
|
|
toolbarConfig,
|
|
localeDefaults,
|
|
availableRawLocales: availableLocaleData.map,
|
|
};
|
|
}
|
|
// always called from behind a memoizer
|
|
processRawCalendarOptions(optionOverrides, dynamicOptionOverrides) {
|
|
let { locales, locale } = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
let availableLocaleData = this.organizeRawLocales(locales);
|
|
let availableRawLocales = availableLocaleData.map;
|
|
let localeDefaults = this.buildLocale(locale || availableLocaleData.defaultCode, availableRawLocales).options;
|
|
let pluginHooks = this.buildPluginHooks(optionOverrides.plugins || [], globalPlugins);
|
|
let refiners = this.currentCalendarOptionsRefiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
|
|
let extra = {};
|
|
let raw = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
localeDefaults,
|
|
optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
let refined = {};
|
|
let currentRaw = this.currentCalendarOptionsInput;
|
|
let currentRefined = this.currentCalendarOptionsRefined;
|
|
let anyChanges = false;
|
|
for (let optionName in raw) {
|
|
if (this.optionsForRefining.indexOf(optionName) === -1 && (raw[optionName] === currentRaw[optionName] || (COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
(optionName in currentRaw) &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](currentRaw[optionName], raw[optionName])))) {
|
|
refined[optionName] = currentRefined[optionName];
|
|
}
|
|
else if (refiners[optionName]) {
|
|
refined[optionName] = refiners[optionName](raw[optionName]);
|
|
anyChanges = true;
|
|
}
|
|
else {
|
|
extra[optionName] = currentRaw[optionName];
|
|
}
|
|
}
|
|
if (anyChanges) {
|
|
this.currentCalendarOptionsInput = raw;
|
|
this.currentCalendarOptionsRefined = refined;
|
|
this.stableOptionOverrides = optionOverrides;
|
|
this.stableDynamicOptionOverrides = dynamicOptionOverrides;
|
|
}
|
|
this.optionsForHandling.push(...this.optionsForRefining);
|
|
this.optionsForRefining = [];
|
|
return {
|
|
rawOptions: this.currentCalendarOptionsInput,
|
|
refinedOptions: this.currentCalendarOptionsRefined,
|
|
pluginHooks,
|
|
availableLocaleData,
|
|
localeDefaults,
|
|
extra,
|
|
};
|
|
}
|
|
_computeCurrentViewData(viewType, optionsData, optionOverrides, dynamicOptionOverrides) {
|
|
let viewSpec = optionsData.viewSpecs[viewType];
|
|
if (!viewSpec) {
|
|
throw new Error(`viewType "${viewType}" is not available. Please make sure you've loaded all neccessary plugins`);
|
|
}
|
|
let { refinedOptions, extra } = this.processRawViewOptions(viewSpec, optionsData.pluginHooks, optionsData.localeDefaults, optionOverrides, dynamicOptionOverrides);
|
|
warnUnknownOptions(extra);
|
|
this.nowManager.handleInput(optionsData.dateEnv, refinedOptions.now);
|
|
let dateProfileGenerator = this.buildDateProfileGenerator({
|
|
dateProfileGeneratorClass: viewSpec.optionDefaults.dateProfileGeneratorClass,
|
|
nowManager: this.nowManager,
|
|
duration: viewSpec.duration,
|
|
durationUnit: viewSpec.durationUnit,
|
|
usesMinMaxTime: viewSpec.optionDefaults.usesMinMaxTime,
|
|
dateEnv: optionsData.dateEnv,
|
|
calendarApi: this.props.calendarApi,
|
|
slotMinTime: refinedOptions.slotMinTime,
|
|
slotMaxTime: refinedOptions.slotMaxTime,
|
|
showNonCurrentDates: refinedOptions.showNonCurrentDates,
|
|
dayCount: refinedOptions.dayCount,
|
|
dateAlignment: refinedOptions.dateAlignment,
|
|
dateIncrement: refinedOptions.dateIncrement,
|
|
hiddenDays: refinedOptions.hiddenDays,
|
|
weekends: refinedOptions.weekends,
|
|
validRangeInput: refinedOptions.validRange,
|
|
visibleRangeInput: refinedOptions.visibleRange,
|
|
fixedWeekCount: refinedOptions.fixedWeekCount,
|
|
});
|
|
let viewApi = this.buildViewApi(viewType, this.getCurrentData, optionsData.dateEnv);
|
|
return { viewSpec, options: refinedOptions, dateProfileGenerator, viewApi };
|
|
}
|
|
processRawViewOptions(viewSpec, pluginHooks, localeDefaults, optionOverrides, dynamicOptionOverrides) {
|
|
let raw = mergeRawOptions([
|
|
BASE_OPTION_DEFAULTS,
|
|
viewSpec.optionDefaults,
|
|
localeDefaults,
|
|
optionOverrides,
|
|
viewSpec.optionOverrides,
|
|
dynamicOptionOverrides,
|
|
]);
|
|
let refiners = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, BASE_OPTION_REFINERS), CALENDAR_LISTENER_REFINERS), CALENDAR_OPTION_REFINERS), VIEW_OPTION_REFINERS), pluginHooks.listenerRefiners), pluginHooks.optionRefiners);
|
|
let refined = {};
|
|
let currentRaw = this.currentViewOptionsInput;
|
|
let currentRefined = this.currentViewOptionsRefined;
|
|
let anyChanges = false;
|
|
let extra = {};
|
|
for (let optionName in raw) {
|
|
if (raw[optionName] === currentRaw[optionName] ||
|
|
(COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], currentRaw[optionName]))) {
|
|
refined[optionName] = currentRefined[optionName];
|
|
}
|
|
else {
|
|
if (raw[optionName] === this.currentCalendarOptionsInput[optionName] ||
|
|
(COMPLEX_OPTION_COMPARATORS[optionName] &&
|
|
COMPLEX_OPTION_COMPARATORS[optionName](raw[optionName], this.currentCalendarOptionsInput[optionName]))) {
|
|
if (optionName in this.currentCalendarOptionsRefined) { // might be an "extra" prop
|
|
refined[optionName] = this.currentCalendarOptionsRefined[optionName];
|
|
}
|
|
}
|
|
else if (refiners[optionName]) {
|
|
refined[optionName] = refiners[optionName](raw[optionName]);
|
|
}
|
|
else {
|
|
extra[optionName] = raw[optionName];
|
|
}
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
if (anyChanges) {
|
|
this.currentViewOptionsInput = raw;
|
|
this.currentViewOptionsRefined = refined;
|
|
}
|
|
return {
|
|
rawOptions: this.currentViewOptionsInput,
|
|
refinedOptions: this.currentViewOptionsRefined,
|
|
extra,
|
|
};
|
|
}
|
|
}
|
|
function buildDateEnv$1(timeZone, explicitLocale, weekNumberCalculation, firstDay, weekText, pluginHooks, availableLocaleData, defaultSeparator) {
|
|
let locale = buildLocale(explicitLocale || availableLocaleData.defaultCode, availableLocaleData.map);
|
|
return new DateEnv({
|
|
calendarSystem: 'gregory',
|
|
timeZone,
|
|
namedTimeZoneImpl: pluginHooks.namedTimeZonedImpl,
|
|
locale,
|
|
weekNumberCalculation,
|
|
firstDay,
|
|
weekText,
|
|
cmdFormatter: pluginHooks.cmdFormatter,
|
|
defaultSeparator,
|
|
});
|
|
}
|
|
function buildTheme(options, pluginHooks) {
|
|
let ThemeClass = pluginHooks.themeClasses[options.themeSystem] || StandardTheme;
|
|
return new ThemeClass(options);
|
|
}
|
|
function buildDateProfileGenerator(props) {
|
|
let DateProfileGeneratorClass = props.dateProfileGeneratorClass || DateProfileGenerator;
|
|
return new DateProfileGeneratorClass(props);
|
|
}
|
|
function buildViewApi(type, getCurrentData, dateEnv) {
|
|
return new ViewImpl(type, getCurrentData, dateEnv);
|
|
}
|
|
function buildEventUiBySource(eventSources) {
|
|
return mapHash(eventSources, (eventSource) => eventSource.ui);
|
|
}
|
|
function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) {
|
|
let eventUiBases = { '': eventUiSingleBase };
|
|
for (let defId in eventDefs) {
|
|
let def = eventDefs[defId];
|
|
if (def.sourceId && eventUiBySource[def.sourceId]) {
|
|
eventUiBases[defId] = eventUiBySource[def.sourceId];
|
|
}
|
|
}
|
|
return eventUiBases;
|
|
}
|
|
function buildViewUiProps(calendarContext) {
|
|
let { options } = calendarContext;
|
|
return {
|
|
eventUiSingleBase: createEventUi({
|
|
display: options.eventDisplay,
|
|
editable: options.editable,
|
|
startEditable: options.eventStartEditable,
|
|
durationEditable: options.eventDurationEditable,
|
|
constraint: options.eventConstraint,
|
|
overlap: typeof options.eventOverlap === 'boolean' ? options.eventOverlap : undefined,
|
|
allow: options.eventAllow,
|
|
backgroundColor: options.eventBackgroundColor,
|
|
borderColor: options.eventBorderColor,
|
|
textColor: options.eventTextColor,
|
|
color: options.eventColor,
|
|
// classNames: options.eventClassNames // render hook will handle this
|
|
}, calendarContext),
|
|
selectionConfig: createEventUi({
|
|
constraint: options.selectConstraint,
|
|
overlap: typeof options.selectOverlap === 'boolean' ? options.selectOverlap : undefined,
|
|
allow: options.selectAllow,
|
|
}, calendarContext),
|
|
};
|
|
}
|
|
function computeIsLoading(state, context) {
|
|
for (let isLoadingFunc of context.pluginHooks.isLoadingFuncs) {
|
|
if (isLoadingFunc(state)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function parseContextBusinessHours(calendarContext) {
|
|
return parseBusinessHours(calendarContext.options.businessHours, calendarContext);
|
|
}
|
|
function warnUnknownOptions(options, viewName) {
|
|
for (let optionName in options) {
|
|
console.warn(`Unknown option '${optionName}'` +
|
|
(viewName ? ` for view '${viewName}'` : ''));
|
|
}
|
|
}
|
|
|
|
class ToolbarSection extends BaseComponent {
|
|
render() {
|
|
let children = this.props.widgetGroups.map((widgetGroup) => this.renderWidgetGroup(widgetGroup));
|
|
return y('div', { className: 'fc-toolbar-chunk' }, ...children);
|
|
}
|
|
renderWidgetGroup(widgetGroup) {
|
|
let { props } = this;
|
|
let { theme } = this.context;
|
|
let children = [];
|
|
let isOnlyButtons = true;
|
|
for (let widget of widgetGroup) {
|
|
let { buttonName, buttonClick, buttonText, buttonIcon, buttonHint } = widget;
|
|
if (buttonName === 'title') {
|
|
isOnlyButtons = false;
|
|
children.push(y("h2", { className: "fc-toolbar-title", id: props.titleId }, props.title));
|
|
}
|
|
else {
|
|
let isPressed = buttonName === props.activeButton;
|
|
let isDisabled = (!props.isTodayEnabled && buttonName === 'today') ||
|
|
(!props.isPrevEnabled && buttonName === 'prev') ||
|
|
(!props.isNextEnabled && buttonName === 'next');
|
|
let buttonClasses = [`fc-${buttonName}-button`, theme.getClass('button')];
|
|
if (isPressed) {
|
|
buttonClasses.push(theme.getClass('buttonActive'));
|
|
}
|
|
children.push(y("button", { type: "button", title: typeof buttonHint === 'function' ? buttonHint(props.navUnit) : buttonHint, disabled: isDisabled, "aria-pressed": isPressed, className: buttonClasses.join(' '), onClick: buttonClick }, buttonText || (buttonIcon ? y("span", { className: buttonIcon, role: "img" }) : '')));
|
|
}
|
|
}
|
|
if (children.length > 1) {
|
|
let groupClassName = (isOnlyButtons && theme.getClass('buttonGroup')) || '';
|
|
return y('div', { className: groupClassName }, ...children);
|
|
}
|
|
return children[0];
|
|
}
|
|
}
|
|
|
|
class Toolbar extends BaseComponent {
|
|
render() {
|
|
let { model, extraClassName } = this.props;
|
|
let forceLtr = false;
|
|
let startContent;
|
|
let endContent;
|
|
let sectionWidgets = model.sectionWidgets;
|
|
let centerContent = sectionWidgets.center;
|
|
if (sectionWidgets.left) {
|
|
forceLtr = true;
|
|
startContent = sectionWidgets.left;
|
|
}
|
|
else {
|
|
startContent = sectionWidgets.start;
|
|
}
|
|
if (sectionWidgets.right) {
|
|
forceLtr = true;
|
|
endContent = sectionWidgets.right;
|
|
}
|
|
else {
|
|
endContent = sectionWidgets.end;
|
|
}
|
|
let classNames = [
|
|
extraClassName || '',
|
|
'fc-toolbar',
|
|
forceLtr ? 'fc-toolbar-ltr' : '',
|
|
];
|
|
return (y("div", { className: classNames.join(' ') },
|
|
this.renderSection('start', startContent || []),
|
|
this.renderSection('center', centerContent || []),
|
|
this.renderSection('end', endContent || [])));
|
|
}
|
|
renderSection(key, widgetGroups) {
|
|
let { props } = this;
|
|
return (y(ToolbarSection, { key: key, widgetGroups: widgetGroups, title: props.title, navUnit: props.navUnit, activeButton: props.activeButton, isTodayEnabled: props.isTodayEnabled, isPrevEnabled: props.isPrevEnabled, isNextEnabled: props.isNextEnabled, titleId: props.titleId }));
|
|
}
|
|
}
|
|
|
|
class ViewHarness extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
availableWidth: null,
|
|
};
|
|
this.handleEl = (el) => {
|
|
this.el = el;
|
|
setRef(this.props.elRef, el);
|
|
this.updateAvailableWidth();
|
|
};
|
|
this.handleResize = () => {
|
|
this.updateAvailableWidth();
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state } = this;
|
|
let { aspectRatio } = props;
|
|
let classNames = [
|
|
'fc-view-harness',
|
|
(aspectRatio || props.liquid || props.height)
|
|
? 'fc-view-harness-active' // harness controls the height
|
|
: 'fc-view-harness-passive', // let the view do the height
|
|
];
|
|
let height = '';
|
|
let paddingBottom = '';
|
|
if (aspectRatio) {
|
|
if (state.availableWidth !== null) {
|
|
height = state.availableWidth / aspectRatio;
|
|
}
|
|
else {
|
|
// while waiting to know availableWidth, we can't set height to *zero*
|
|
// because will cause lots of unnecessary scrollbars within scrollgrid.
|
|
// BETTER: don't start rendering ANYTHING yet until we know container width
|
|
// NOTE: why not always use paddingBottom? Causes height oscillation (issue 5606)
|
|
paddingBottom = `${(1 / aspectRatio) * 100}%`;
|
|
}
|
|
}
|
|
else {
|
|
height = props.height || '';
|
|
}
|
|
return (y("div", { "aria-labelledby": props.labeledById, ref: this.handleEl, className: classNames.join(' '), style: { height, paddingBottom } }, props.children));
|
|
}
|
|
componentDidMount() {
|
|
this.context.addResizeHandler(this.handleResize);
|
|
}
|
|
componentWillUnmount() {
|
|
this.context.removeResizeHandler(this.handleResize);
|
|
}
|
|
updateAvailableWidth() {
|
|
if (this.el && // needed. but why?
|
|
this.props.aspectRatio // aspectRatio is the only height setting that needs availableWidth
|
|
) {
|
|
this.setState({ availableWidth: this.el.offsetWidth });
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Detects when the user clicks on an event within a DateComponent
|
|
*/
|
|
class EventClicking extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
this.handleSegClick = (ev, segEl) => {
|
|
let { component } = this;
|
|
let { context } = component;
|
|
let seg = getElSeg(segEl);
|
|
if (seg && // might be the <div> surrounding the more link
|
|
component.isValidSegDownEl(ev.target)) {
|
|
// our way to simulate a link click for elements that can't be <a> tags
|
|
// grab before trigger fired in case trigger trashes DOM thru rerendering
|
|
let hasUrlContainer = elementClosest(ev.target, '.fc-event-forced-url');
|
|
let url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : '';
|
|
context.emitter.trigger('eventClick', {
|
|
el: segEl,
|
|
event: new EventImpl(component.context, seg.eventRange.def, seg.eventRange.instance),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
if (url && !ev.defaultPrevented) {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
};
|
|
this.destroy = listenBySelector(settings.el, 'click', '.fc-event', // on both fg and bg events
|
|
this.handleSegClick);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Triggers events and adds/removes core classNames when the user's pointer
|
|
enters/leaves event-elements of a component.
|
|
*/
|
|
class EventHovering extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
// for simulating an eventMouseLeave when the event el is destroyed while mouse is over it
|
|
this.handleEventElRemove = (el) => {
|
|
if (el === this.currentSegEl) {
|
|
this.handleSegLeave(null, this.currentSegEl);
|
|
}
|
|
};
|
|
this.handleSegEnter = (ev, segEl) => {
|
|
if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper
|
|
this.currentSegEl = segEl;
|
|
this.triggerEvent('eventMouseEnter', ev, segEl);
|
|
}
|
|
};
|
|
this.handleSegLeave = (ev, segEl) => {
|
|
if (this.currentSegEl) {
|
|
this.currentSegEl = null;
|
|
this.triggerEvent('eventMouseLeave', ev, segEl);
|
|
}
|
|
};
|
|
this.removeHoverListeners = listenToHoverBySelector(settings.el, '.fc-event', // on both fg and bg events
|
|
this.handleSegEnter, this.handleSegLeave);
|
|
}
|
|
destroy() {
|
|
this.removeHoverListeners();
|
|
}
|
|
triggerEvent(publicEvName, ev, segEl) {
|
|
let { component } = this;
|
|
let { context } = component;
|
|
let seg = getElSeg(segEl);
|
|
if (!ev || component.isValidSegDownEl(ev.target)) {
|
|
context.emitter.trigger(publicEvName, {
|
|
el: segEl,
|
|
event: new EventImpl(context, seg.eventRange.def, seg.eventRange.instance),
|
|
jsEvent: ev,
|
|
view: context.viewApi,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class CalendarContent extends PureComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buildViewContext = memoize(buildViewContext);
|
|
this.buildViewPropTransformers = memoize(buildViewPropTransformers);
|
|
this.buildToolbarProps = memoize(buildToolbarProps);
|
|
this.headerRef = d();
|
|
this.footerRef = d();
|
|
this.interactionsStore = {};
|
|
// eslint-disable-next-line
|
|
this.state = {
|
|
viewLabelId: getUniqueDomId(),
|
|
};
|
|
// Component Registration
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
this.registerInteractiveComponent = (component, settingsInput) => {
|
|
let settings = parseInteractionSettings(component, settingsInput);
|
|
let DEFAULT_INTERACTIONS = [
|
|
EventClicking,
|
|
EventHovering,
|
|
];
|
|
let interactionClasses = DEFAULT_INTERACTIONS.concat(this.props.pluginHooks.componentInteractions);
|
|
let interactions = interactionClasses.map((TheInteractionClass) => new TheInteractionClass(settings));
|
|
this.interactionsStore[component.uid] = interactions;
|
|
interactionSettingsStore[component.uid] = settings;
|
|
};
|
|
this.unregisterInteractiveComponent = (component) => {
|
|
let listeners = this.interactionsStore[component.uid];
|
|
if (listeners) {
|
|
for (let listener of listeners) {
|
|
listener.destroy();
|
|
}
|
|
delete this.interactionsStore[component.uid];
|
|
}
|
|
delete interactionSettingsStore[component.uid];
|
|
};
|
|
// Resizing
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
this.resizeRunner = new DelayedRunner(() => {
|
|
this.props.emitter.trigger('_resize', true); // should window resizes be considered "forced" ?
|
|
this.props.emitter.trigger('windowResize', { view: this.props.viewApi });
|
|
});
|
|
this.handleWindowResize = (ev) => {
|
|
let { options } = this.props;
|
|
if (options.handleWindowResize &&
|
|
ev.target === window // avoid jqui events
|
|
) {
|
|
this.resizeRunner.request(options.windowResizeDelay);
|
|
}
|
|
};
|
|
}
|
|
/*
|
|
renders INSIDE of an outer div
|
|
*/
|
|
render() {
|
|
let { props } = this;
|
|
let { toolbarConfig, options } = props;
|
|
let viewVGrow = false;
|
|
let viewHeight = '';
|
|
let viewAspectRatio;
|
|
if (props.isHeightAuto || props.forPrint) {
|
|
viewHeight = '';
|
|
}
|
|
else if (options.height != null) {
|
|
viewVGrow = true;
|
|
}
|
|
else if (options.contentHeight != null) {
|
|
viewHeight = options.contentHeight;
|
|
}
|
|
else {
|
|
viewAspectRatio = Math.max(options.aspectRatio, 0.5); // prevent from getting too tall
|
|
}
|
|
let viewContext = this.buildViewContext(props.viewSpec, props.viewApi, props.options, props.dateProfileGenerator, props.dateEnv, props.nowManager, props.theme, props.pluginHooks, props.dispatch, props.getCurrentData, props.emitter, props.calendarApi, this.registerInteractiveComponent, this.unregisterInteractiveComponent);
|
|
let viewLabelId = (toolbarConfig.header && toolbarConfig.header.hasTitle)
|
|
? this.state.viewLabelId
|
|
: undefined;
|
|
return (y(ViewContextType.Provider, { value: viewContext },
|
|
y(NowTimer, { unit: "day" }, (nowDate) => {
|
|
let toolbarProps = this.buildToolbarProps(props.viewSpec, props.dateProfile, props.dateProfileGenerator, props.currentDate, nowDate, props.viewTitle);
|
|
return (y(_, null,
|
|
toolbarConfig.header && (y(Toolbar, Object.assign({ ref: this.headerRef, extraClassName: "fc-header-toolbar", model: toolbarConfig.header, titleId: viewLabelId }, toolbarProps))),
|
|
y(ViewHarness, { liquid: viewVGrow, height: viewHeight, aspectRatio: viewAspectRatio, labeledById: viewLabelId },
|
|
this.renderView(props),
|
|
this.buildAppendContent()),
|
|
toolbarConfig.footer && (y(Toolbar, Object.assign({ ref: this.footerRef, extraClassName: "fc-footer-toolbar", model: toolbarConfig.footer, titleId: "" }, toolbarProps)))));
|
|
})));
|
|
}
|
|
componentDidMount() {
|
|
let { props } = this;
|
|
this.calendarInteractions = props.pluginHooks.calendarInteractions
|
|
.map((CalendarInteractionClass) => new CalendarInteractionClass(props));
|
|
window.addEventListener('resize', this.handleWindowResize);
|
|
let { propSetHandlers } = props.pluginHooks;
|
|
for (let propName in propSetHandlers) {
|
|
propSetHandlers[propName](props[propName], props);
|
|
}
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
let { props } = this;
|
|
let { propSetHandlers } = props.pluginHooks;
|
|
for (let propName in propSetHandlers) {
|
|
if (props[propName] !== prevProps[propName]) {
|
|
propSetHandlers[propName](props[propName], props);
|
|
}
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.handleWindowResize);
|
|
this.resizeRunner.clear();
|
|
for (let interaction of this.calendarInteractions) {
|
|
interaction.destroy();
|
|
}
|
|
this.props.emitter.trigger('_unmount');
|
|
}
|
|
buildAppendContent() {
|
|
let { props } = this;
|
|
let children = props.pluginHooks.viewContainerAppends.map((buildAppendContent) => buildAppendContent(props));
|
|
return y(_, {}, ...children);
|
|
}
|
|
renderView(props) {
|
|
let { pluginHooks } = props;
|
|
let { viewSpec } = props;
|
|
let viewProps = {
|
|
dateProfile: props.dateProfile,
|
|
businessHours: props.businessHours,
|
|
eventStore: props.renderableEventStore,
|
|
eventUiBases: props.eventUiBases,
|
|
dateSelection: props.dateSelection,
|
|
eventSelection: props.eventSelection,
|
|
eventDrag: props.eventDrag,
|
|
eventResize: props.eventResize,
|
|
isHeightAuto: props.isHeightAuto,
|
|
forPrint: props.forPrint,
|
|
};
|
|
let transformers = this.buildViewPropTransformers(pluginHooks.viewPropsTransformers);
|
|
for (let transformer of transformers) {
|
|
Object.assign(viewProps, transformer.transform(viewProps, props));
|
|
}
|
|
let ViewComponent = viewSpec.component;
|
|
return (y(ViewComponent, Object.assign({}, viewProps)));
|
|
}
|
|
}
|
|
function buildToolbarProps(viewSpec, dateProfile, dateProfileGenerator, currentDate, now, title) {
|
|
// don't force any date-profiles to valid date profiles (the `false`) so that we can tell if it's invalid
|
|
let todayInfo = dateProfileGenerator.build(now, undefined, false); // TODO: need `undefined` or else INFINITE LOOP for some reason
|
|
let prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate, false);
|
|
let nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate, false);
|
|
return {
|
|
title,
|
|
activeButton: viewSpec.type,
|
|
navUnit: viewSpec.singleUnit,
|
|
isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now),
|
|
isPrevEnabled: prevInfo.isValid,
|
|
isNextEnabled: nextInfo.isValid,
|
|
};
|
|
}
|
|
// Plugin
|
|
// -----------------------------------------------------------------------------------------------------------------
|
|
function buildViewPropTransformers(theClasses) {
|
|
return theClasses.map((TheClass) => new TheClass());
|
|
}
|
|
|
|
class Calendar extends CalendarImpl {
|
|
constructor(el, optionOverrides = {}) {
|
|
super();
|
|
this.isRendering = false;
|
|
this.isRendered = false;
|
|
this.currentClassNames = [];
|
|
this.customContentRenderId = 0;
|
|
this.handleAction = (action) => {
|
|
// actions we know we want to render immediately
|
|
switch (action.type) {
|
|
case 'SET_EVENT_DRAG':
|
|
case 'SET_EVENT_RESIZE':
|
|
this.renderRunner.tryDrain();
|
|
}
|
|
};
|
|
this.handleData = (data) => {
|
|
this.currentData = data;
|
|
this.renderRunner.request(data.calendarOptions.rerenderDelay);
|
|
};
|
|
this.handleRenderRequest = () => {
|
|
if (this.isRendering) {
|
|
this.isRendered = true;
|
|
let { currentData } = this;
|
|
flushSync(() => {
|
|
D(y(CalendarRoot, { options: currentData.calendarOptions, theme: currentData.theme, emitter: currentData.emitter }, (classNames, height, isHeightAuto, forPrint) => {
|
|
this.setClassNames(classNames);
|
|
this.setHeight(height);
|
|
return (y(RenderId.Provider, { value: this.customContentRenderId },
|
|
y(CalendarContent, Object.assign({ isHeightAuto: isHeightAuto, forPrint: forPrint }, currentData))));
|
|
}), this.el);
|
|
});
|
|
}
|
|
else if (this.isRendered) {
|
|
this.isRendered = false;
|
|
D(null, this.el);
|
|
this.setClassNames([]);
|
|
this.setHeight('');
|
|
}
|
|
};
|
|
ensureElHasStyles(el);
|
|
this.el = el;
|
|
this.renderRunner = new DelayedRunner(this.handleRenderRequest);
|
|
new CalendarDataManager({
|
|
optionOverrides,
|
|
calendarApi: this,
|
|
onAction: this.handleAction,
|
|
onData: this.handleData,
|
|
});
|
|
}
|
|
render() {
|
|
let wasRendering = this.isRendering;
|
|
if (!wasRendering) {
|
|
this.isRendering = true;
|
|
}
|
|
else {
|
|
this.customContentRenderId += 1;
|
|
}
|
|
this.renderRunner.request();
|
|
if (wasRendering) {
|
|
this.updateSize();
|
|
}
|
|
}
|
|
destroy() {
|
|
if (this.isRendering) {
|
|
this.isRendering = false;
|
|
this.renderRunner.request();
|
|
}
|
|
}
|
|
updateSize() {
|
|
flushSync(() => {
|
|
super.updateSize();
|
|
});
|
|
}
|
|
batchRendering(func) {
|
|
this.renderRunner.pause('batchRendering');
|
|
func();
|
|
this.renderRunner.resume('batchRendering');
|
|
}
|
|
pauseRendering() {
|
|
this.renderRunner.pause('pauseRendering');
|
|
}
|
|
resumeRendering() {
|
|
this.renderRunner.resume('pauseRendering', true);
|
|
}
|
|
resetOptions(optionOverrides, changedOptionNames) {
|
|
this.currentDataManager.resetOptions(optionOverrides, changedOptionNames);
|
|
}
|
|
setClassNames(classNames) {
|
|
if (!isArraysEqual(classNames, this.currentClassNames)) {
|
|
let { classList } = this.el;
|
|
for (let className of this.currentClassNames) {
|
|
classList.remove(className);
|
|
}
|
|
for (let className of classNames) {
|
|
classList.add(className);
|
|
}
|
|
this.currentClassNames = classNames;
|
|
}
|
|
}
|
|
setHeight(height) {
|
|
applyStyleProp(this.el, 'height', height);
|
|
}
|
|
}
|
|
|
|
function formatDate(dateInput, options = {}) {
|
|
let dateEnv = buildDateEnv(options);
|
|
let formatter = core_createFormatter(options);
|
|
let dateMeta = dateEnv.createMarkerMeta(dateInput);
|
|
if (!dateMeta) { // TODO: warning?
|
|
return '';
|
|
}
|
|
return dateEnv.format(dateMeta.marker, formatter, {
|
|
forcedTzo: dateMeta.forcedTzo,
|
|
});
|
|
}
|
|
function formatRange(startInput, endInput, options) {
|
|
let dateEnv = buildDateEnv(typeof options === 'object' && options ? options : {}); // pass in if non-null object
|
|
let formatter = core_createFormatter(options);
|
|
let startMeta = dateEnv.createMarkerMeta(startInput);
|
|
let endMeta = dateEnv.createMarkerMeta(endInput);
|
|
if (!startMeta || !endMeta) { // TODO: warning?
|
|
return '';
|
|
}
|
|
return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, {
|
|
forcedStartTzo: startMeta.forcedTzo,
|
|
forcedEndTzo: endMeta.forcedTzo,
|
|
isEndExclusive: options.isEndExclusive,
|
|
defaultSeparator: core_BASE_OPTION_DEFAULTS.defaultRangeSeparator,
|
|
});
|
|
}
|
|
// TODO: more DRY and optimized
|
|
function buildDateEnv(settings) {
|
|
let locale = buildLocale(settings.locale || 'en', organizeRawLocales([]).map); // TODO: don't hardcode 'en' everywhere
|
|
return new core_DateEnv(Object.assign(Object.assign({ timeZone: core_BASE_OPTION_DEFAULTS.timeZone, calendarSystem: 'gregory' }, settings), { locale }));
|
|
}
|
|
|
|
// HELPERS
|
|
/*
|
|
if nextDayThreshold is specified, slicing is done in an all-day fashion.
|
|
you can get nextDayThreshold from context.nextDayThreshold
|
|
*/
|
|
function sliceEvents(props, allDay) {
|
|
return core_sliceEventStore(props.eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? props.nextDayThreshold : null).fg;
|
|
}
|
|
|
|
const core_version = '6.1.20';
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/interaction/index.js
|
|
/* unused harmony import specifier */ var interaction_createEmptyEventStore;
|
|
/* unused harmony import specifier */ var interaction_eventTupleToStore;
|
|
/* unused harmony import specifier */ var interaction_isInteractionValid;
|
|
/* unused harmony import specifier */ var interaction_enableCursor;
|
|
/* unused harmony import specifier */ var interaction_disableCursor;
|
|
/* unused harmony import specifier */ var interaction_EventImpl;
|
|
/* unused harmony import specifier */ var interaction_interactionSettingsStore;
|
|
/* unused harmony import specifier */ var interaction_parseDragMeta;
|
|
/* unused harmony import specifier */ var interaction_elementMatches;
|
|
/* unused harmony import specifier */ var interaction_refineEventDef;
|
|
/* unused harmony import specifier */ var interaction_parseEventDef;
|
|
/* unused harmony import specifier */ var interaction_getDefaultEventEnd;
|
|
/* unused harmony import specifier */ var interaction_createEventInstance;
|
|
/* unused harmony import specifier */ var interaction_config;
|
|
/* unused harmony import specifier */ var interaction_BASE_OPTION_DEFAULTS;
|
|
|
|
|
|
|
|
config.touchMouseIgnoreWait = 500;
|
|
let ignoreMouseDepth = 0;
|
|
let listenerCnt = 0;
|
|
let isWindowTouchMoveCancelled = false;
|
|
/*
|
|
Uses a "pointer" abstraction, which monitors UI events for both mouse and touch.
|
|
Tracks when the pointer "drags" on a certain element, meaning down+move+up.
|
|
|
|
Also, tracks if there was touch-scrolling.
|
|
Also, can prevent touch-scrolling from happening.
|
|
Also, can fire pointermove events when scrolling happens underneath, even when no real pointer movement.
|
|
|
|
emits:
|
|
- pointerdown
|
|
- pointermove
|
|
- pointerup
|
|
*/
|
|
class PointerDragging {
|
|
constructor(containerEl) {
|
|
this.subjectEl = null;
|
|
// options that can be directly assigned by caller
|
|
this.selector = ''; // will cause subjectEl in all emitted events to be this element
|
|
this.handleSelector = '';
|
|
this.shouldIgnoreMove = false;
|
|
this.shouldWatchScroll = true; // for simulating pointermove on scroll
|
|
// internal states
|
|
this.isDragging = false;
|
|
this.isTouchDragging = false;
|
|
this.wasTouchScroll = false;
|
|
// Mouse
|
|
// ----------------------------------------------------------------------------------------------------
|
|
this.handleMouseDown = (ev) => {
|
|
if (!this.shouldIgnoreMouse() &&
|
|
isPrimaryMouseButton(ev) &&
|
|
this.tryStart(ev)) {
|
|
let pev = this.createEventFromMouse(ev, true);
|
|
this.emitter.trigger('pointerdown', pev);
|
|
this.initScrollWatch(pev);
|
|
if (!this.shouldIgnoreMove) {
|
|
document.addEventListener('mousemove', this.handleMouseMove);
|
|
}
|
|
document.addEventListener('mouseup', this.handleMouseUp);
|
|
}
|
|
};
|
|
this.handleMouseMove = (ev) => {
|
|
let pev = this.createEventFromMouse(ev);
|
|
this.recordCoords(pev);
|
|
this.emitter.trigger('pointermove', pev);
|
|
};
|
|
this.handleMouseUp = (ev) => {
|
|
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
this.emitter.trigger('pointerup', this.createEventFromMouse(ev));
|
|
this.cleanup(); // call last so that pointerup has access to props
|
|
};
|
|
// Touch
|
|
// ----------------------------------------------------------------------------------------------------
|
|
this.handleTouchStart = (ev) => {
|
|
if (this.tryStart(ev)) {
|
|
this.isTouchDragging = true;
|
|
let pev = this.createEventFromTouch(ev, true);
|
|
this.emitter.trigger('pointerdown', pev);
|
|
this.initScrollWatch(pev);
|
|
// unlike mouse, need to attach to target, not document
|
|
// https://stackoverflow.com/a/45760014
|
|
let targetEl = ev.target;
|
|
if (!this.shouldIgnoreMove) {
|
|
targetEl.addEventListener('touchmove', this.handleTouchMove);
|
|
}
|
|
targetEl.addEventListener('touchend', this.handleTouchEnd);
|
|
targetEl.addEventListener('touchcancel', this.handleTouchEnd); // treat it as a touch end
|
|
// attach a handler to get called when ANY scroll action happens on the page.
|
|
// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
|
|
// http://stackoverflow.com/a/32954565/96342
|
|
window.addEventListener('scroll', this.handleTouchScroll, true);
|
|
}
|
|
};
|
|
this.handleTouchMove = (ev) => {
|
|
let pev = this.createEventFromTouch(ev);
|
|
this.recordCoords(pev);
|
|
this.emitter.trigger('pointermove', pev);
|
|
};
|
|
this.handleTouchEnd = (ev) => {
|
|
if (this.isDragging) { // done to guard against touchend followed by touchcancel
|
|
let targetEl = ev.target;
|
|
targetEl.removeEventListener('touchmove', this.handleTouchMove);
|
|
targetEl.removeEventListener('touchend', this.handleTouchEnd);
|
|
targetEl.removeEventListener('touchcancel', this.handleTouchEnd);
|
|
window.removeEventListener('scroll', this.handleTouchScroll, true); // useCaptured=true
|
|
this.emitter.trigger('pointerup', this.createEventFromTouch(ev));
|
|
this.cleanup(); // call last so that pointerup has access to props
|
|
this.isTouchDragging = false;
|
|
startIgnoringMouse();
|
|
}
|
|
};
|
|
this.handleTouchScroll = () => {
|
|
this.wasTouchScroll = true;
|
|
};
|
|
this.handleScroll = (ev) => {
|
|
if (!this.shouldIgnoreMove) {
|
|
let pageX = (window.scrollX - this.prevScrollX) + this.prevPageX;
|
|
let pageY = (window.scrollY - this.prevScrollY) + this.prevPageY;
|
|
this.emitter.trigger('pointermove', {
|
|
origEvent: ev,
|
|
isTouch: this.isTouchDragging,
|
|
subjectEl: this.subjectEl,
|
|
pageX,
|
|
pageY,
|
|
deltaX: pageX - this.origPageX,
|
|
deltaY: pageY - this.origPageY,
|
|
});
|
|
}
|
|
};
|
|
this.containerEl = containerEl;
|
|
this.emitter = new Emitter();
|
|
containerEl.addEventListener('mousedown', this.handleMouseDown);
|
|
containerEl.addEventListener('touchstart', this.handleTouchStart, { passive: true });
|
|
listenerCreated();
|
|
}
|
|
destroy() {
|
|
this.containerEl.removeEventListener('mousedown', this.handleMouseDown);
|
|
this.containerEl.removeEventListener('touchstart', this.handleTouchStart, { passive: true });
|
|
listenerDestroyed();
|
|
}
|
|
tryStart(ev) {
|
|
let subjectEl = this.querySubjectEl(ev);
|
|
let downEl = ev.target;
|
|
if (subjectEl &&
|
|
(!this.handleSelector || elementClosest(downEl, this.handleSelector))) {
|
|
this.subjectEl = subjectEl;
|
|
this.isDragging = true; // do this first so cancelTouchScroll will work
|
|
this.wasTouchScroll = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
cleanup() {
|
|
isWindowTouchMoveCancelled = false;
|
|
this.isDragging = false;
|
|
this.subjectEl = null;
|
|
// keep wasTouchScroll around for later access
|
|
this.destroyScrollWatch();
|
|
}
|
|
querySubjectEl(ev) {
|
|
if (this.selector) {
|
|
return elementClosest(ev.target, this.selector);
|
|
}
|
|
return this.containerEl;
|
|
}
|
|
shouldIgnoreMouse() {
|
|
return ignoreMouseDepth || this.isTouchDragging;
|
|
}
|
|
// can be called by user of this class, to cancel touch-based scrolling for the current drag
|
|
cancelTouchScroll() {
|
|
if (this.isDragging) {
|
|
isWindowTouchMoveCancelled = true;
|
|
}
|
|
}
|
|
// Scrolling that simulates pointermoves
|
|
// ----------------------------------------------------------------------------------------------------
|
|
initScrollWatch(ev) {
|
|
if (this.shouldWatchScroll) {
|
|
this.recordCoords(ev);
|
|
window.addEventListener('scroll', this.handleScroll, true); // useCapture=true
|
|
}
|
|
}
|
|
recordCoords(ev) {
|
|
if (this.shouldWatchScroll) {
|
|
this.prevPageX = ev.pageX;
|
|
this.prevPageY = ev.pageY;
|
|
this.prevScrollX = window.scrollX;
|
|
this.prevScrollY = window.scrollY;
|
|
}
|
|
}
|
|
destroyScrollWatch() {
|
|
if (this.shouldWatchScroll) {
|
|
window.removeEventListener('scroll', this.handleScroll, true); // useCaptured=true
|
|
}
|
|
}
|
|
// Event Normalization
|
|
// ----------------------------------------------------------------------------------------------------
|
|
createEventFromMouse(ev, isFirst) {
|
|
let deltaX = 0;
|
|
let deltaY = 0;
|
|
// TODO: repeat code
|
|
if (isFirst) {
|
|
this.origPageX = ev.pageX;
|
|
this.origPageY = ev.pageY;
|
|
}
|
|
else {
|
|
deltaX = ev.pageX - this.origPageX;
|
|
deltaY = ev.pageY - this.origPageY;
|
|
}
|
|
return {
|
|
origEvent: ev,
|
|
isTouch: false,
|
|
subjectEl: this.subjectEl,
|
|
pageX: ev.pageX,
|
|
pageY: ev.pageY,
|
|
deltaX,
|
|
deltaY,
|
|
};
|
|
}
|
|
createEventFromTouch(ev, isFirst) {
|
|
let touches = ev.touches;
|
|
let pageX;
|
|
let pageY;
|
|
let deltaX = 0;
|
|
let deltaY = 0;
|
|
// if touch coords available, prefer,
|
|
// because FF would give bad ev.pageX ev.pageY
|
|
if (touches && touches.length) {
|
|
pageX = touches[0].pageX;
|
|
pageY = touches[0].pageY;
|
|
}
|
|
else {
|
|
pageX = ev.pageX;
|
|
pageY = ev.pageY;
|
|
}
|
|
// TODO: repeat code
|
|
if (isFirst) {
|
|
this.origPageX = pageX;
|
|
this.origPageY = pageY;
|
|
}
|
|
else {
|
|
deltaX = pageX - this.origPageX;
|
|
deltaY = pageY - this.origPageY;
|
|
}
|
|
return {
|
|
origEvent: ev,
|
|
isTouch: true,
|
|
subjectEl: this.subjectEl,
|
|
pageX,
|
|
pageY,
|
|
deltaX,
|
|
deltaY,
|
|
};
|
|
}
|
|
}
|
|
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
|
|
function isPrimaryMouseButton(ev) {
|
|
return ev.button === 0 && !ev.ctrlKey;
|
|
}
|
|
// Ignoring fake mouse events generated by touch
|
|
// ----------------------------------------------------------------------------------------------------
|
|
function startIgnoringMouse() {
|
|
ignoreMouseDepth += 1;
|
|
setTimeout(() => {
|
|
ignoreMouseDepth -= 1;
|
|
}, config.touchMouseIgnoreWait);
|
|
}
|
|
// We want to attach touchmove as early as possible for Safari
|
|
// ----------------------------------------------------------------------------------------------------
|
|
function listenerCreated() {
|
|
listenerCnt += 1;
|
|
if (listenerCnt === 1) {
|
|
window.addEventListener('touchmove', onWindowTouchMove, { passive: false });
|
|
}
|
|
}
|
|
function listenerDestroyed() {
|
|
listenerCnt -= 1;
|
|
if (!listenerCnt) {
|
|
window.removeEventListener('touchmove', onWindowTouchMove, { passive: false });
|
|
}
|
|
}
|
|
function onWindowTouchMove(ev) {
|
|
if (isWindowTouchMoveCancelled) {
|
|
ev.preventDefault();
|
|
}
|
|
}
|
|
|
|
/*
|
|
An effect in which an element follows the movement of a pointer across the screen.
|
|
The moving element is a clone of some other element.
|
|
Must call start + handleMove + stop.
|
|
*/
|
|
class ElementMirror {
|
|
constructor() {
|
|
this.isVisible = false; // must be explicitly enabled
|
|
this.sourceEl = null;
|
|
this.mirrorEl = null;
|
|
this.sourceElRect = null; // screen coords relative to viewport
|
|
// options that can be set directly by caller
|
|
this.parentNode = document.body; // HIGHLY SUGGESTED to set this to sidestep ShadowDOM issues
|
|
this.zIndex = 9999;
|
|
this.revertDuration = 0;
|
|
}
|
|
start(sourceEl, pageX, pageY) {
|
|
this.sourceEl = sourceEl;
|
|
this.sourceElRect = this.sourceEl.getBoundingClientRect();
|
|
this.origScreenX = pageX - window.scrollX;
|
|
this.origScreenY = pageY - window.scrollY;
|
|
this.deltaX = 0;
|
|
this.deltaY = 0;
|
|
this.updateElPosition();
|
|
}
|
|
handleMove(pageX, pageY) {
|
|
this.deltaX = (pageX - window.scrollX) - this.origScreenX;
|
|
this.deltaY = (pageY - window.scrollY) - this.origScreenY;
|
|
this.updateElPosition();
|
|
}
|
|
// can be called before start
|
|
setIsVisible(bool) {
|
|
if (bool) {
|
|
if (!this.isVisible) {
|
|
if (this.mirrorEl) {
|
|
this.mirrorEl.style.display = '';
|
|
}
|
|
this.isVisible = bool; // needs to happen before updateElPosition
|
|
this.updateElPosition(); // because was not updating the position while invisible
|
|
}
|
|
}
|
|
else if (this.isVisible) {
|
|
if (this.mirrorEl) {
|
|
this.mirrorEl.style.display = 'none';
|
|
}
|
|
this.isVisible = bool;
|
|
}
|
|
}
|
|
// always async
|
|
stop(needsRevertAnimation, callback) {
|
|
let done = () => {
|
|
this.cleanup();
|
|
callback();
|
|
};
|
|
if (needsRevertAnimation &&
|
|
this.mirrorEl &&
|
|
this.isVisible &&
|
|
this.revertDuration && // if 0, transition won't work
|
|
(this.deltaX || this.deltaY) // if same coords, transition won't work
|
|
) {
|
|
this.doRevertAnimation(done, this.revertDuration);
|
|
}
|
|
else {
|
|
setTimeout(done, 0);
|
|
}
|
|
}
|
|
doRevertAnimation(callback, revertDuration) {
|
|
let mirrorEl = this.mirrorEl;
|
|
let finalSourceElRect = this.sourceEl.getBoundingClientRect(); // because autoscrolling might have happened
|
|
mirrorEl.style.transition =
|
|
'top ' + revertDuration + 'ms,' +
|
|
'left ' + revertDuration + 'ms';
|
|
applyStyle(mirrorEl, {
|
|
left: finalSourceElRect.left,
|
|
top: finalSourceElRect.top,
|
|
});
|
|
whenTransitionDone(mirrorEl, () => {
|
|
mirrorEl.style.transition = '';
|
|
callback();
|
|
});
|
|
}
|
|
cleanup() {
|
|
if (this.mirrorEl) {
|
|
removeElement(this.mirrorEl);
|
|
this.mirrorEl = null;
|
|
}
|
|
this.sourceEl = null;
|
|
}
|
|
updateElPosition() {
|
|
if (this.sourceEl && this.isVisible) {
|
|
applyStyle(this.getMirrorEl(), {
|
|
left: this.sourceElRect.left + this.deltaX,
|
|
top: this.sourceElRect.top + this.deltaY,
|
|
});
|
|
}
|
|
}
|
|
getMirrorEl() {
|
|
let sourceElRect = this.sourceElRect;
|
|
let mirrorEl = this.mirrorEl;
|
|
if (!mirrorEl) {
|
|
mirrorEl = this.mirrorEl = this.sourceEl.cloneNode(true); // cloneChildren=true
|
|
// we don't want long taps or any mouse interaction causing selection/menus.
|
|
// would use preventSelection(), but that prevents selectstart, causing problems.
|
|
mirrorEl.style.userSelect = 'none';
|
|
mirrorEl.style.webkitUserSelect = 'none';
|
|
mirrorEl.style.pointerEvents = 'none';
|
|
mirrorEl.classList.add('fc-event-dragging');
|
|
applyStyle(mirrorEl, {
|
|
position: 'fixed',
|
|
zIndex: this.zIndex,
|
|
visibility: '',
|
|
boxSizing: 'border-box',
|
|
width: sourceElRect.right - sourceElRect.left,
|
|
height: sourceElRect.bottom - sourceElRect.top,
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
margin: 0,
|
|
});
|
|
this.parentNode.appendChild(mirrorEl);
|
|
}
|
|
return mirrorEl;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Is a cache for a given element's scroll information (all the info that ScrollController stores)
|
|
in addition the "client rectangle" of the element.. the area within the scrollbars.
|
|
|
|
The cache can be in one of two modes:
|
|
- doesListening:false - ignores when the container is scrolled by someone else
|
|
- doesListening:true - watch for scrolling and update the cache
|
|
*/
|
|
class ScrollGeomCache extends ScrollController {
|
|
constructor(scrollController, doesListening) {
|
|
super();
|
|
this.handleScroll = () => {
|
|
this.scrollTop = this.scrollController.getScrollTop();
|
|
this.scrollLeft = this.scrollController.getScrollLeft();
|
|
this.handleScrollChange();
|
|
};
|
|
this.scrollController = scrollController;
|
|
this.doesListening = doesListening;
|
|
this.scrollTop = this.origScrollTop = scrollController.getScrollTop();
|
|
this.scrollLeft = this.origScrollLeft = scrollController.getScrollLeft();
|
|
this.scrollWidth = scrollController.getScrollWidth();
|
|
this.scrollHeight = scrollController.getScrollHeight();
|
|
this.clientWidth = scrollController.getClientWidth();
|
|
this.clientHeight = scrollController.getClientHeight();
|
|
this.clientRect = this.computeClientRect(); // do last in case it needs cached values
|
|
if (this.doesListening) {
|
|
this.getEventTarget().addEventListener('scroll', this.handleScroll);
|
|
}
|
|
}
|
|
destroy() {
|
|
if (this.doesListening) {
|
|
this.getEventTarget().removeEventListener('scroll', this.handleScroll);
|
|
}
|
|
}
|
|
getScrollTop() {
|
|
return this.scrollTop;
|
|
}
|
|
getScrollLeft() {
|
|
return this.scrollLeft;
|
|
}
|
|
setScrollTop(top) {
|
|
this.scrollController.setScrollTop(top);
|
|
if (!this.doesListening) {
|
|
// we are not relying on the element to normalize out-of-bounds scroll values
|
|
// so we need to sanitize ourselves
|
|
this.scrollTop = Math.max(Math.min(top, this.getMaxScrollTop()), 0);
|
|
this.handleScrollChange();
|
|
}
|
|
}
|
|
setScrollLeft(top) {
|
|
this.scrollController.setScrollLeft(top);
|
|
if (!this.doesListening) {
|
|
// we are not relying on the element to normalize out-of-bounds scroll values
|
|
// so we need to sanitize ourselves
|
|
this.scrollLeft = Math.max(Math.min(top, this.getMaxScrollLeft()), 0);
|
|
this.handleScrollChange();
|
|
}
|
|
}
|
|
getClientWidth() {
|
|
return this.clientWidth;
|
|
}
|
|
getClientHeight() {
|
|
return this.clientHeight;
|
|
}
|
|
getScrollWidth() {
|
|
return this.scrollWidth;
|
|
}
|
|
getScrollHeight() {
|
|
return this.scrollHeight;
|
|
}
|
|
handleScrollChange() {
|
|
}
|
|
}
|
|
|
|
class ElementScrollGeomCache extends ScrollGeomCache {
|
|
constructor(el, doesListening) {
|
|
super(new ElementScrollController(el), doesListening);
|
|
}
|
|
getEventTarget() {
|
|
return this.scrollController.el;
|
|
}
|
|
computeClientRect() {
|
|
return computeInnerRect(this.scrollController.el);
|
|
}
|
|
}
|
|
|
|
class WindowScrollGeomCache extends ScrollGeomCache {
|
|
constructor(doesListening) {
|
|
super(new WindowScrollController(), doesListening);
|
|
}
|
|
getEventTarget() {
|
|
return window;
|
|
}
|
|
computeClientRect() {
|
|
return {
|
|
left: this.scrollLeft,
|
|
right: this.scrollLeft + this.clientWidth,
|
|
top: this.scrollTop,
|
|
bottom: this.scrollTop + this.clientHeight,
|
|
};
|
|
}
|
|
// the window is the only scroll object that changes it's rectangle relative
|
|
// to the document's topleft as it scrolls
|
|
handleScrollChange() {
|
|
this.clientRect = this.computeClientRect();
|
|
}
|
|
}
|
|
|
|
// If available we are using native "performance" API instead of "Date"
|
|
// Read more about it on MDN:
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
|
const getTime = typeof performance === 'function' ? performance.now : Date.now;
|
|
/*
|
|
For a pointer interaction, automatically scrolls certain scroll containers when the pointer
|
|
approaches the edge.
|
|
|
|
The caller must call start + handleMove + stop.
|
|
*/
|
|
class AutoScroller {
|
|
constructor() {
|
|
// options that can be set by caller
|
|
this.isEnabled = true;
|
|
this.scrollQuery = [window, '.fc-scroller'];
|
|
this.edgeThreshold = 50; // pixels
|
|
this.maxVelocity = 300; // pixels per second
|
|
// internal state
|
|
this.pointerScreenX = null;
|
|
this.pointerScreenY = null;
|
|
this.isAnimating = false;
|
|
this.scrollCaches = null;
|
|
// protect against the initial pointerdown being too close to an edge and starting the scroll
|
|
this.everMovedUp = false;
|
|
this.everMovedDown = false;
|
|
this.everMovedLeft = false;
|
|
this.everMovedRight = false;
|
|
this.animate = () => {
|
|
if (this.isAnimating) { // wasn't cancelled between animation calls
|
|
let edge = this.computeBestEdge(this.pointerScreenX + window.scrollX, this.pointerScreenY + window.scrollY);
|
|
if (edge) {
|
|
let now = getTime();
|
|
this.handleSide(edge, (now - this.msSinceRequest) / 1000);
|
|
this.requestAnimation(now);
|
|
}
|
|
else {
|
|
this.isAnimating = false; // will stop animation
|
|
}
|
|
}
|
|
};
|
|
}
|
|
start(pageX, pageY, scrollStartEl) {
|
|
if (this.isEnabled) {
|
|
this.scrollCaches = this.buildCaches(scrollStartEl);
|
|
this.pointerScreenX = null;
|
|
this.pointerScreenY = null;
|
|
this.everMovedUp = false;
|
|
this.everMovedDown = false;
|
|
this.everMovedLeft = false;
|
|
this.everMovedRight = false;
|
|
this.handleMove(pageX, pageY);
|
|
}
|
|
}
|
|
handleMove(pageX, pageY) {
|
|
if (this.isEnabled) {
|
|
let pointerScreenX = pageX - window.scrollX;
|
|
let pointerScreenY = pageY - window.scrollY;
|
|
let yDelta = this.pointerScreenY === null ? 0 : pointerScreenY - this.pointerScreenY;
|
|
let xDelta = this.pointerScreenX === null ? 0 : pointerScreenX - this.pointerScreenX;
|
|
if (yDelta < 0) {
|
|
this.everMovedUp = true;
|
|
}
|
|
else if (yDelta > 0) {
|
|
this.everMovedDown = true;
|
|
}
|
|
if (xDelta < 0) {
|
|
this.everMovedLeft = true;
|
|
}
|
|
else if (xDelta > 0) {
|
|
this.everMovedRight = true;
|
|
}
|
|
this.pointerScreenX = pointerScreenX;
|
|
this.pointerScreenY = pointerScreenY;
|
|
if (!this.isAnimating) {
|
|
this.isAnimating = true;
|
|
this.requestAnimation(getTime());
|
|
}
|
|
}
|
|
}
|
|
stop() {
|
|
if (this.isEnabled) {
|
|
this.isAnimating = false; // will stop animation
|
|
for (let scrollCache of this.scrollCaches) {
|
|
scrollCache.destroy();
|
|
}
|
|
this.scrollCaches = null;
|
|
}
|
|
}
|
|
requestAnimation(now) {
|
|
this.msSinceRequest = now;
|
|
requestAnimationFrame(this.animate);
|
|
}
|
|
handleSide(edge, seconds) {
|
|
let { scrollCache } = edge;
|
|
let { edgeThreshold } = this;
|
|
let invDistance = edgeThreshold - edge.distance;
|
|
let velocity = // the closer to the edge, the faster we scroll
|
|
((invDistance * invDistance) / (edgeThreshold * edgeThreshold)) * // quadratic
|
|
this.maxVelocity * seconds;
|
|
let sign = 1;
|
|
switch (edge.name) {
|
|
case 'left':
|
|
sign = -1;
|
|
// falls through
|
|
case 'right':
|
|
scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign);
|
|
break;
|
|
case 'top':
|
|
sign = -1;
|
|
// falls through
|
|
case 'bottom':
|
|
scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign);
|
|
break;
|
|
}
|
|
}
|
|
// left/top are relative to document topleft
|
|
computeBestEdge(left, top) {
|
|
let { edgeThreshold } = this;
|
|
let bestSide = null;
|
|
let scrollCaches = this.scrollCaches || [];
|
|
for (let scrollCache of scrollCaches) {
|
|
let rect = scrollCache.clientRect;
|
|
let leftDist = left - rect.left;
|
|
let rightDist = rect.right - left;
|
|
let topDist = top - rect.top;
|
|
let bottomDist = rect.bottom - top;
|
|
// completely within the rect?
|
|
if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) {
|
|
if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() &&
|
|
(!bestSide || bestSide.distance > topDist)) {
|
|
bestSide = { scrollCache, name: 'top', distance: topDist };
|
|
}
|
|
if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() &&
|
|
(!bestSide || bestSide.distance > bottomDist)) {
|
|
bestSide = { scrollCache, name: 'bottom', distance: bottomDist };
|
|
}
|
|
/*
|
|
TODO: fix broken RTL scrolling. canScrollLeft always returning false
|
|
https://github.com/fullcalendar/fullcalendar/issues/4837
|
|
*/
|
|
if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() &&
|
|
(!bestSide || bestSide.distance > leftDist)) {
|
|
bestSide = { scrollCache, name: 'left', distance: leftDist };
|
|
}
|
|
if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() &&
|
|
(!bestSide || bestSide.distance > rightDist)) {
|
|
bestSide = { scrollCache, name: 'right', distance: rightDist };
|
|
}
|
|
}
|
|
}
|
|
return bestSide;
|
|
}
|
|
buildCaches(scrollStartEl) {
|
|
return this.queryScrollEls(scrollStartEl).map((el) => {
|
|
if (el === window) {
|
|
return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls
|
|
}
|
|
return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls
|
|
});
|
|
}
|
|
queryScrollEls(scrollStartEl) {
|
|
let els = [];
|
|
for (let query of this.scrollQuery) {
|
|
if (typeof query === 'object') {
|
|
els.push(query);
|
|
}
|
|
else {
|
|
/*
|
|
TODO: in the future, always have auto-scroll happen on element where current Hit came from
|
|
Ticket: https://github.com/fullcalendar/fullcalendar/issues/4593
|
|
*/
|
|
els.push(...Array.prototype.slice.call(scrollStartEl.getRootNode().querySelectorAll(query)));
|
|
}
|
|
}
|
|
return els;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Monitors dragging on an element. Has a number of high-level features:
|
|
- minimum distance required before dragging
|
|
- minimum wait time ("delay") before dragging
|
|
- a mirror element that follows the pointer
|
|
*/
|
|
class FeaturefulElementDragging extends ElementDragging {
|
|
constructor(containerEl, selector) {
|
|
super(containerEl);
|
|
this.containerEl = containerEl;
|
|
// options that can be directly set by caller
|
|
// the caller can also set the PointerDragging's options as well
|
|
this.delay = null;
|
|
this.minDistance = 0;
|
|
this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag
|
|
this.mirrorNeedsRevert = false;
|
|
this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup
|
|
this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation
|
|
this.isDelayEnded = false;
|
|
this.isDistanceSurpassed = false;
|
|
this.delayTimeoutId = null;
|
|
this.onPointerDown = (ev) => {
|
|
if (!this.isDragging) { // so new drag doesn't happen while revert animation is going
|
|
this.isInteracting = true;
|
|
this.isDelayEnded = false;
|
|
this.isDistanceSurpassed = false;
|
|
preventSelection(document.body);
|
|
preventContextMenu(document.body);
|
|
// prevent links from being visited if there's an eventual drag.
|
|
// also prevents selection in older browsers (maybe?).
|
|
// not necessary for touch, besides, browser would complain about passiveness.
|
|
if (!ev.isTouch) {
|
|
ev.origEvent.preventDefault();
|
|
}
|
|
this.emitter.trigger('pointerdown', ev);
|
|
if (this.isInteracting && // not destroyed via pointerdown handler
|
|
!this.pointer.shouldIgnoreMove) {
|
|
// actions related to initiating dragstart+dragmove+dragend...
|
|
this.mirror.setIsVisible(false); // reset. caller must set-visible
|
|
this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down
|
|
this.startDelay(ev);
|
|
if (!this.minDistance) {
|
|
this.handleDistanceSurpassed(ev);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
this.onPointerMove = (ev) => {
|
|
if (this.isInteracting) {
|
|
this.emitter.trigger('pointermove', ev);
|
|
if (!this.isDistanceSurpassed) {
|
|
let minDistance = this.minDistance;
|
|
let distanceSq; // current distance from the origin, squared
|
|
let { deltaX, deltaY } = ev;
|
|
distanceSq = deltaX * deltaX + deltaY * deltaY;
|
|
if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
|
|
this.handleDistanceSurpassed(ev);
|
|
}
|
|
}
|
|
if (this.isDragging) {
|
|
// a real pointer move? (not one simulated by scrolling)
|
|
if (ev.origEvent.type !== 'scroll') {
|
|
this.mirror.handleMove(ev.pageX, ev.pageY);
|
|
this.autoScroller.handleMove(ev.pageX, ev.pageY);
|
|
}
|
|
this.emitter.trigger('dragmove', ev);
|
|
}
|
|
}
|
|
};
|
|
this.onPointerUp = (ev) => {
|
|
if (this.isInteracting) {
|
|
this.isInteracting = false;
|
|
allowSelection(document.body);
|
|
allowContextMenu(document.body);
|
|
this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert
|
|
if (this.isDragging) {
|
|
this.autoScroller.stop();
|
|
this.tryStopDrag(ev); // which will stop the mirror
|
|
}
|
|
if (this.delayTimeoutId) {
|
|
clearTimeout(this.delayTimeoutId);
|
|
this.delayTimeoutId = null;
|
|
}
|
|
}
|
|
};
|
|
let pointer = this.pointer = new PointerDragging(containerEl);
|
|
pointer.emitter.on('pointerdown', this.onPointerDown);
|
|
pointer.emitter.on('pointermove', this.onPointerMove);
|
|
pointer.emitter.on('pointerup', this.onPointerUp);
|
|
if (selector) {
|
|
pointer.selector = selector;
|
|
}
|
|
this.mirror = new ElementMirror();
|
|
this.autoScroller = new AutoScroller();
|
|
}
|
|
destroy() {
|
|
this.pointer.destroy();
|
|
// HACK: simulate a pointer-up to end the current drag
|
|
// TODO: fire 'dragend' directly and stop interaction. discourage use of pointerup event (b/c might not fire)
|
|
this.onPointerUp({});
|
|
}
|
|
startDelay(ev) {
|
|
if (typeof this.delay === 'number') {
|
|
this.delayTimeoutId = setTimeout(() => {
|
|
this.delayTimeoutId = null;
|
|
this.handleDelayEnd(ev);
|
|
}, this.delay); // not assignable to number!
|
|
}
|
|
else {
|
|
this.handleDelayEnd(ev);
|
|
}
|
|
}
|
|
handleDelayEnd(ev) {
|
|
this.isDelayEnded = true;
|
|
this.tryStartDrag(ev);
|
|
}
|
|
handleDistanceSurpassed(ev) {
|
|
this.isDistanceSurpassed = true;
|
|
this.tryStartDrag(ev);
|
|
}
|
|
tryStartDrag(ev) {
|
|
if (this.isDelayEnded && this.isDistanceSurpassed) {
|
|
if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) {
|
|
this.isDragging = true;
|
|
this.mirrorNeedsRevert = false;
|
|
this.autoScroller.start(ev.pageX, ev.pageY, this.containerEl);
|
|
this.emitter.trigger('dragstart', ev);
|
|
if (this.touchScrollAllowed === false) {
|
|
this.pointer.cancelTouchScroll();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tryStopDrag(ev) {
|
|
// .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events
|
|
// that come from the document to fire beforehand. much more convenient this way.
|
|
this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev));
|
|
}
|
|
stopDrag(ev) {
|
|
this.isDragging = false;
|
|
this.emitter.trigger('dragend', ev);
|
|
}
|
|
// fill in the implementations...
|
|
setIgnoreMove(bool) {
|
|
this.pointer.shouldIgnoreMove = bool;
|
|
}
|
|
setMirrorIsVisible(bool) {
|
|
this.mirror.setIsVisible(bool);
|
|
}
|
|
setMirrorNeedsRevert(bool) {
|
|
this.mirrorNeedsRevert = bool;
|
|
}
|
|
setAutoScrollEnabled(bool) {
|
|
this.autoScroller.isEnabled = bool;
|
|
}
|
|
}
|
|
|
|
/*
|
|
When this class is instantiated, it records the offset of an element (relative to the document topleft),
|
|
and continues to monitor scrolling, updating the cached coordinates if it needs to.
|
|
Does not access the DOM after instantiation, so highly performant.
|
|
|
|
Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element
|
|
and an determine if a given point is inside the combined clipping rectangle.
|
|
*/
|
|
class OffsetTracker {
|
|
constructor(el) {
|
|
this.el = el;
|
|
this.origRect = computeRect(el);
|
|
// will work fine for divs that have overflow:hidden
|
|
this.scrollCaches = getClippingParents(el).map((scrollEl) => new ElementScrollGeomCache(scrollEl, true));
|
|
}
|
|
destroy() {
|
|
for (let scrollCache of this.scrollCaches) {
|
|
scrollCache.destroy();
|
|
}
|
|
}
|
|
computeLeft() {
|
|
let left = this.origRect.left;
|
|
for (let scrollCache of this.scrollCaches) {
|
|
left += scrollCache.origScrollLeft - scrollCache.getScrollLeft();
|
|
}
|
|
return left;
|
|
}
|
|
computeTop() {
|
|
let top = this.origRect.top;
|
|
for (let scrollCache of this.scrollCaches) {
|
|
top += scrollCache.origScrollTop - scrollCache.getScrollTop();
|
|
}
|
|
return top;
|
|
}
|
|
isWithinClipping(pageX, pageY) {
|
|
let point = { left: pageX, top: pageY };
|
|
for (let scrollCache of this.scrollCaches) {
|
|
if (!isIgnoredClipping(scrollCache.getEventTarget()) &&
|
|
!pointInsideRect(point, scrollCache.clientRect)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
// certain clipping containers should never constrain interactions, like <html> and <body>
|
|
// https://github.com/fullcalendar/fullcalendar/issues/3615
|
|
function isIgnoredClipping(node) {
|
|
let tagName = node.tagName;
|
|
return tagName === 'HTML' || tagName === 'BODY';
|
|
}
|
|
|
|
/*
|
|
Tracks movement over multiple droppable areas (aka "hits")
|
|
that exist in one or more DateComponents.
|
|
Relies on an existing draggable.
|
|
|
|
emits:
|
|
- pointerdown
|
|
- dragstart
|
|
- hitchange - fires initially, even if not over a hit
|
|
- pointerup
|
|
- (hitchange - again, to null, if ended over a hit)
|
|
- dragend
|
|
*/
|
|
class HitDragging {
|
|
constructor(dragging, droppableStore) {
|
|
// options that can be set by caller
|
|
this.useSubjectCenter = false;
|
|
this.requireInitial = true; // if doesn't start out on a hit, won't emit any events
|
|
this.disablePointCheck = false;
|
|
this.initialHit = null;
|
|
this.movingHit = null;
|
|
this.finalHit = null; // won't ever be populated if shouldIgnoreMove
|
|
this.handlePointerDown = (ev) => {
|
|
let { dragging } = this;
|
|
this.initialHit = null;
|
|
this.movingHit = null;
|
|
this.finalHit = null;
|
|
this.prepareHits();
|
|
this.processFirstCoord(ev);
|
|
if (this.initialHit || !this.requireInitial) {
|
|
dragging.setIgnoreMove(false);
|
|
// TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :(
|
|
this.emitter.trigger('pointerdown', ev);
|
|
}
|
|
else {
|
|
dragging.setIgnoreMove(true);
|
|
}
|
|
};
|
|
this.handleDragStart = (ev) => {
|
|
this.emitter.trigger('dragstart', ev);
|
|
this.handleMove(ev, true); // force = fire even if initially null
|
|
};
|
|
this.handleDragMove = (ev) => {
|
|
this.emitter.trigger('dragmove', ev);
|
|
this.handleMove(ev);
|
|
};
|
|
this.handlePointerUp = (ev) => {
|
|
this.releaseHits();
|
|
this.emitter.trigger('pointerup', ev);
|
|
};
|
|
this.handleDragEnd = (ev) => {
|
|
if (this.movingHit) {
|
|
this.emitter.trigger('hitupdate', null, true, ev);
|
|
}
|
|
this.finalHit = this.movingHit;
|
|
this.movingHit = null;
|
|
this.emitter.trigger('dragend', ev);
|
|
};
|
|
this.droppableStore = droppableStore;
|
|
dragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
dragging.emitter.on('dragstart', this.handleDragStart);
|
|
dragging.emitter.on('dragmove', this.handleDragMove);
|
|
dragging.emitter.on('pointerup', this.handlePointerUp);
|
|
dragging.emitter.on('dragend', this.handleDragEnd);
|
|
this.dragging = dragging;
|
|
this.emitter = new Emitter();
|
|
}
|
|
// sets initialHit
|
|
// sets coordAdjust
|
|
processFirstCoord(ev) {
|
|
let origPoint = { left: ev.pageX, top: ev.pageY };
|
|
let adjustedPoint = origPoint;
|
|
let subjectEl = ev.subjectEl;
|
|
let subjectRect;
|
|
if (subjectEl instanceof HTMLElement) { // i.e. not a Document/ShadowRoot
|
|
subjectRect = computeRect(subjectEl);
|
|
adjustedPoint = constrainPoint(adjustedPoint, subjectRect);
|
|
}
|
|
let initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top);
|
|
if (initialHit) {
|
|
if (this.useSubjectCenter && subjectRect) {
|
|
let slicedSubjectRect = intersectRects(subjectRect, initialHit.rect);
|
|
if (slicedSubjectRect) {
|
|
adjustedPoint = getRectCenter(slicedSubjectRect);
|
|
}
|
|
}
|
|
this.coordAdjust = diffPoints(adjustedPoint, origPoint);
|
|
}
|
|
else {
|
|
this.coordAdjust = { left: 0, top: 0 };
|
|
}
|
|
}
|
|
handleMove(ev, forceHandle) {
|
|
let hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top);
|
|
if (forceHandle || !isHitsEqual(this.movingHit, hit)) {
|
|
this.movingHit = hit;
|
|
this.emitter.trigger('hitupdate', hit, false, ev);
|
|
}
|
|
}
|
|
prepareHits() {
|
|
this.offsetTrackers = mapHash(this.droppableStore, (interactionSettings) => {
|
|
interactionSettings.component.prepareHits();
|
|
return new OffsetTracker(interactionSettings.el);
|
|
});
|
|
}
|
|
releaseHits() {
|
|
let { offsetTrackers } = this;
|
|
for (let id in offsetTrackers) {
|
|
offsetTrackers[id].destroy();
|
|
}
|
|
this.offsetTrackers = {};
|
|
}
|
|
queryHitForOffset(offsetLeft, offsetTop) {
|
|
let { droppableStore, offsetTrackers } = this;
|
|
let bestHit = null;
|
|
for (let id in droppableStore) {
|
|
let component = droppableStore[id].component;
|
|
let offsetTracker = offsetTrackers[id];
|
|
if (offsetTracker && // wasn't destroyed mid-drag
|
|
offsetTracker.isWithinClipping(offsetLeft, offsetTop)) {
|
|
let originLeft = offsetTracker.computeLeft();
|
|
let originTop = offsetTracker.computeTop();
|
|
let positionLeft = offsetLeft - originLeft;
|
|
let positionTop = offsetTop - originTop;
|
|
let { origRect } = offsetTracker;
|
|
let width = origRect.right - origRect.left;
|
|
let height = origRect.bottom - origRect.top;
|
|
if (
|
|
// must be within the element's bounds
|
|
positionLeft >= 0 && positionLeft < width &&
|
|
positionTop >= 0 && positionTop < height) {
|
|
let hit = component.queryHit(positionLeft, positionTop, width, height);
|
|
if (hit && (
|
|
// make sure the hit is within activeRange, meaning it's not a dead cell
|
|
rangeContainsRange(hit.dateProfile.activeRange, hit.dateSpan.range)) &&
|
|
// Ensure the component we are querying for the hit is accessibly my the pointer
|
|
// Prevents obscured calendars (ex: under a modal dialog) from accepting hit
|
|
// https://github.com/fullcalendar/fullcalendar/issues/5026
|
|
(this.disablePointCheck ||
|
|
offsetTracker.el.contains(offsetTracker.el.getRootNode().elementFromPoint(
|
|
// add-back origins to get coordinate relative to top-left of window viewport
|
|
positionLeft + originLeft - window.scrollX, positionTop + originTop - window.scrollY))) &&
|
|
(!bestHit || hit.layer > bestHit.layer)) {
|
|
hit.componentId = id;
|
|
hit.context = component.context;
|
|
// TODO: better way to re-orient rectangle
|
|
hit.rect.left += originLeft;
|
|
hit.rect.right += originLeft;
|
|
hit.rect.top += originTop;
|
|
hit.rect.bottom += originTop;
|
|
bestHit = hit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return bestHit;
|
|
}
|
|
}
|
|
function isHitsEqual(hit0, hit1) {
|
|
if (!hit0 && !hit1) {
|
|
return true;
|
|
}
|
|
if (Boolean(hit0) !== Boolean(hit1)) {
|
|
return false;
|
|
}
|
|
return isDateSpansEqual(hit0.dateSpan, hit1.dateSpan);
|
|
}
|
|
|
|
function buildDatePointApiWithContext(dateSpan, context) {
|
|
let props = {};
|
|
for (let transform of context.pluginHooks.datePointTransforms) {
|
|
Object.assign(props, transform(dateSpan, context));
|
|
}
|
|
Object.assign(props, buildDatePointApi(dateSpan, context.dateEnv));
|
|
return props;
|
|
}
|
|
function buildDatePointApi(span, dateEnv) {
|
|
return {
|
|
date: dateEnv.toDate(span.range.start),
|
|
dateStr: dateEnv.formatIso(span.range.start, { omitTime: span.allDay }),
|
|
allDay: span.allDay,
|
|
};
|
|
}
|
|
|
|
/*
|
|
Monitors when the user clicks on a specific date/time of a component.
|
|
A pointerdown+pointerup on the same "hit" constitutes a click.
|
|
*/
|
|
class DateClicking extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
this.handlePointerDown = (pev) => {
|
|
let { dragging } = this;
|
|
let downEl = pev.origEvent.target;
|
|
// do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired
|
|
dragging.setIgnoreMove(!this.component.isValidDateDownEl(downEl));
|
|
};
|
|
// won't even fire if moving was ignored
|
|
this.handleDragEnd = (ev) => {
|
|
let { component } = this;
|
|
let { pointer } = this.dragging;
|
|
if (!pointer.wasTouchScroll) {
|
|
let { initialHit, finalHit } = this.hitDragging;
|
|
if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) {
|
|
let { context } = component;
|
|
let arg = Object.assign(Object.assign({}, buildDatePointApiWithContext(initialHit.dateSpan, context)), { dayEl: initialHit.dayEl, jsEvent: ev.origEvent, view: context.viewApi || context.calendarApi.view });
|
|
context.emitter.trigger('dateClick', arg);
|
|
}
|
|
}
|
|
};
|
|
// we DO want to watch pointer moves because otherwise finalHit won't get populated
|
|
this.dragging = new FeaturefulElementDragging(settings.el);
|
|
this.dragging.autoScroller.isEnabled = false;
|
|
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
|
|
hitDragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
hitDragging.emitter.on('dragend', this.handleDragEnd);
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
}
|
|
|
|
/*
|
|
Tracks when the user selects a portion of time of a component,
|
|
constituted by a drag over date cells, with a possible delay at the beginning of the drag.
|
|
*/
|
|
class DateSelecting extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
this.dragSelection = null;
|
|
this.handlePointerDown = (ev) => {
|
|
let { component, dragging } = this;
|
|
let { options } = component.context;
|
|
let canSelect = options.selectable &&
|
|
component.isValidDateDownEl(ev.origEvent.target);
|
|
// don't bother to watch expensive moves if component won't do selection
|
|
dragging.setIgnoreMove(!canSelect);
|
|
// if touch, require user to hold down
|
|
dragging.delay = ev.isTouch ? getComponentTouchDelay$1(component) : null;
|
|
};
|
|
this.handleDragStart = (ev) => {
|
|
this.component.context.calendarApi.unselect(ev); // unselect previous selections
|
|
};
|
|
this.handleHitUpdate = (hit, isFinal) => {
|
|
let { context } = this.component;
|
|
let dragSelection = null;
|
|
let isInvalid = false;
|
|
if (hit) {
|
|
let initialHit = this.hitDragging.initialHit;
|
|
let disallowed = hit.componentId === initialHit.componentId
|
|
&& this.isHitComboAllowed
|
|
&& !this.isHitComboAllowed(initialHit, hit);
|
|
if (!disallowed) {
|
|
dragSelection = joinHitsIntoSelection(initialHit, hit, context.pluginHooks.dateSelectionTransformers);
|
|
}
|
|
if (!dragSelection || !isDateSelectionValid(dragSelection, hit.dateProfile, context)) {
|
|
isInvalid = true;
|
|
dragSelection = null;
|
|
}
|
|
}
|
|
if (dragSelection) {
|
|
context.dispatch({ type: 'SELECT_DATES', selection: dragSelection });
|
|
}
|
|
else if (!isFinal) { // only unselect if moved away while dragging
|
|
context.dispatch({ type: 'UNSELECT_DATES' });
|
|
}
|
|
if (!isInvalid) {
|
|
enableCursor();
|
|
}
|
|
else {
|
|
disableCursor();
|
|
}
|
|
if (!isFinal) {
|
|
this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging
|
|
}
|
|
};
|
|
this.handlePointerUp = (pev) => {
|
|
if (this.dragSelection) {
|
|
// selection is already rendered, so just need to report selection
|
|
triggerDateSelect(this.dragSelection, pev, this.component.context);
|
|
this.dragSelection = null;
|
|
}
|
|
};
|
|
let { component } = settings;
|
|
let { options } = component.context;
|
|
let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
|
|
dragging.touchScrollAllowed = false;
|
|
dragging.minDistance = options.selectMinDistance || 0;
|
|
dragging.autoScroller.isEnabled = options.dragScroll;
|
|
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
|
|
hitDragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
hitDragging.emitter.on('dragstart', this.handleDragStart);
|
|
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
|
|
hitDragging.emitter.on('pointerup', this.handlePointerUp);
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
}
|
|
function getComponentTouchDelay$1(component) {
|
|
let { options } = component.context;
|
|
let delay = options.selectLongPressDelay;
|
|
if (delay == null) {
|
|
delay = options.longPressDelay;
|
|
}
|
|
return delay;
|
|
}
|
|
function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) {
|
|
let dateSpan0 = hit0.dateSpan;
|
|
let dateSpan1 = hit1.dateSpan;
|
|
let ms = [
|
|
dateSpan0.range.start,
|
|
dateSpan0.range.end,
|
|
dateSpan1.range.start,
|
|
dateSpan1.range.end,
|
|
];
|
|
ms.sort(compareNumbers);
|
|
let props = {};
|
|
for (let transformer of dateSelectionTransformers) {
|
|
let res = transformer(hit0, hit1);
|
|
if (res === false) {
|
|
return null;
|
|
}
|
|
if (res) {
|
|
Object.assign(props, res);
|
|
}
|
|
}
|
|
props.range = { start: ms[0], end: ms[3] };
|
|
props.allDay = dateSpan0.allDay;
|
|
return props;
|
|
}
|
|
|
|
class EventDragging extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
// internal state
|
|
this.subjectEl = null;
|
|
this.subjectSeg = null; // the seg being selected/dragged
|
|
this.isDragging = false;
|
|
this.eventRange = null;
|
|
this.relevantEvents = null; // the events being dragged
|
|
this.receivingContext = null;
|
|
this.validMutation = null;
|
|
this.mutatedRelevantEvents = null;
|
|
this.handlePointerDown = (ev) => {
|
|
let origTarget = ev.origEvent.target;
|
|
let { component, dragging } = this;
|
|
let { mirror } = dragging;
|
|
let { options } = component.context;
|
|
let initialContext = component.context;
|
|
this.subjectEl = ev.subjectEl;
|
|
let subjectSeg = this.subjectSeg = getElSeg(ev.subjectEl);
|
|
let eventRange = this.eventRange = subjectSeg.eventRange;
|
|
let eventInstanceId = eventRange.instance.instanceId;
|
|
this.relevantEvents = getRelevantEvents(initialContext.getCurrentData().eventStore, eventInstanceId);
|
|
dragging.minDistance = ev.isTouch ? 0 : options.eventDragMinDistance;
|
|
dragging.delay =
|
|
// only do a touch delay if touch and this event hasn't been selected yet
|
|
(ev.isTouch && eventInstanceId !== component.props.eventSelection) ?
|
|
getComponentTouchDelay(component) :
|
|
null;
|
|
if (options.fixedMirrorParent) {
|
|
mirror.parentNode = options.fixedMirrorParent;
|
|
}
|
|
else {
|
|
mirror.parentNode = elementClosest(origTarget, '.fc');
|
|
}
|
|
mirror.revertDuration = options.dragRevertDuration;
|
|
let isValid = component.isValidSegDownEl(origTarget) &&
|
|
!elementClosest(origTarget, '.fc-event-resizer'); // NOT on a resizer
|
|
dragging.setIgnoreMove(!isValid);
|
|
// disable dragging for elements that are resizable (ie, selectable)
|
|
// but are not draggable
|
|
this.isDragging = isValid &&
|
|
ev.subjectEl.classList.contains('fc-event-draggable');
|
|
};
|
|
this.handleDragStart = (ev) => {
|
|
let initialContext = this.component.context;
|
|
let eventRange = this.eventRange;
|
|
let eventInstanceId = eventRange.instance.instanceId;
|
|
if (ev.isTouch) {
|
|
// need to select a different event?
|
|
if (eventInstanceId !== this.component.props.eventSelection) {
|
|
initialContext.dispatch({ type: 'SELECT_EVENT', eventInstanceId });
|
|
}
|
|
}
|
|
else {
|
|
// if now using mouse, but was previous touch interaction, clear selected event
|
|
initialContext.dispatch({ type: 'UNSELECT_EVENT' });
|
|
}
|
|
if (this.isDragging) {
|
|
initialContext.calendarApi.unselect(ev); // unselect *date* selection
|
|
initialContext.emitter.trigger('eventDragStart', {
|
|
el: this.subjectEl,
|
|
event: new EventImpl(initialContext, eventRange.def, eventRange.instance),
|
|
jsEvent: ev.origEvent,
|
|
view: initialContext.viewApi,
|
|
});
|
|
}
|
|
};
|
|
this.handleHitUpdate = (hit, isFinal) => {
|
|
if (!this.isDragging) {
|
|
return;
|
|
}
|
|
let relevantEvents = this.relevantEvents;
|
|
let initialHit = this.hitDragging.initialHit;
|
|
let initialContext = this.component.context;
|
|
// states based on new hit
|
|
let receivingContext = null;
|
|
let mutation = null;
|
|
let mutatedRelevantEvents = null;
|
|
let isInvalid = false;
|
|
let interaction = {
|
|
affectedEvents: relevantEvents,
|
|
mutatedEvents: createEmptyEventStore(),
|
|
isEvent: true,
|
|
};
|
|
if (hit) {
|
|
receivingContext = hit.context;
|
|
let receivingOptions = receivingContext.options;
|
|
if (initialContext === receivingContext ||
|
|
(receivingOptions.editable && receivingOptions.droppable)) {
|
|
mutation = computeEventMutation(initialHit, hit, this.eventRange.instance.range.start, receivingContext.getCurrentData().pluginHooks.eventDragMutationMassagers);
|
|
if (mutation) {
|
|
mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, receivingContext.getCurrentData().eventUiBases, mutation, receivingContext);
|
|
interaction.mutatedEvents = mutatedRelevantEvents;
|
|
if (!isInteractionValid(interaction, hit.dateProfile, receivingContext)) {
|
|
isInvalid = true;
|
|
mutation = null;
|
|
mutatedRelevantEvents = null;
|
|
interaction.mutatedEvents = createEmptyEventStore();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
receivingContext = null;
|
|
}
|
|
}
|
|
this.displayDrag(receivingContext, interaction);
|
|
if (!isInvalid) {
|
|
enableCursor();
|
|
}
|
|
else {
|
|
disableCursor();
|
|
}
|
|
if (!isFinal) {
|
|
if (initialContext === receivingContext && // TODO: write test for this
|
|
isHitsEqual(initialHit, hit)) {
|
|
mutation = null;
|
|
}
|
|
this.dragging.setMirrorNeedsRevert(!mutation);
|
|
// render the mirror if no already-rendered mirror
|
|
// TODO: wish we could somehow wait for dispatch to guarantee render
|
|
this.dragging.setMirrorIsVisible(!hit || !this.subjectEl.getRootNode().querySelector('.fc-event-mirror'));
|
|
// assign states based on new hit
|
|
this.receivingContext = receivingContext;
|
|
this.validMutation = mutation;
|
|
this.mutatedRelevantEvents = mutatedRelevantEvents;
|
|
}
|
|
};
|
|
this.handlePointerUp = () => {
|
|
if (!this.isDragging) {
|
|
this.cleanup(); // because handleDragEnd won't fire
|
|
}
|
|
};
|
|
this.handleDragEnd = (ev) => {
|
|
if (this.isDragging) {
|
|
let initialContext = this.component.context;
|
|
let initialView = initialContext.viewApi;
|
|
let { receivingContext, validMutation } = this;
|
|
let eventDef = this.eventRange.def;
|
|
let eventInstance = this.eventRange.instance;
|
|
let eventApi = new EventImpl(initialContext, eventDef, eventInstance);
|
|
let relevantEvents = this.relevantEvents;
|
|
let mutatedRelevantEvents = this.mutatedRelevantEvents;
|
|
let { finalHit } = this.hitDragging;
|
|
this.clearDrag(); // must happen after revert animation
|
|
initialContext.emitter.trigger('eventDragStop', {
|
|
el: this.subjectEl,
|
|
event: eventApi,
|
|
jsEvent: ev.origEvent,
|
|
view: initialView,
|
|
});
|
|
if (validMutation) {
|
|
// dropped within same calendar
|
|
if (receivingContext === initialContext) {
|
|
let updatedEventApi = new EventImpl(initialContext, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
|
|
initialContext.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: mutatedRelevantEvents,
|
|
});
|
|
let eventChangeArg = {
|
|
oldEvent: eventApi,
|
|
event: updatedEventApi,
|
|
relatedEvents: buildEventApis(mutatedRelevantEvents, initialContext, eventInstance),
|
|
revert() {
|
|
initialContext.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: relevantEvents, // the pre-change data
|
|
});
|
|
},
|
|
};
|
|
let transformed = {};
|
|
for (let transformer of initialContext.getCurrentData().pluginHooks.eventDropTransformers) {
|
|
Object.assign(transformed, transformer(validMutation, initialContext));
|
|
}
|
|
initialContext.emitter.trigger('eventDrop', Object.assign(Object.assign(Object.assign({}, eventChangeArg), transformed), { el: ev.subjectEl, delta: validMutation.datesDelta, jsEvent: ev.origEvent, view: initialView }));
|
|
initialContext.emitter.trigger('eventChange', eventChangeArg);
|
|
// dropped in different calendar
|
|
}
|
|
else if (receivingContext) {
|
|
let eventRemoveArg = {
|
|
event: eventApi,
|
|
relatedEvents: buildEventApis(relevantEvents, initialContext, eventInstance),
|
|
revert() {
|
|
initialContext.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: relevantEvents,
|
|
});
|
|
},
|
|
};
|
|
initialContext.emitter.trigger('eventLeave', Object.assign(Object.assign({}, eventRemoveArg), { draggedEl: ev.subjectEl, view: initialView }));
|
|
initialContext.dispatch({
|
|
type: 'REMOVE_EVENTS',
|
|
eventStore: relevantEvents,
|
|
});
|
|
initialContext.emitter.trigger('eventRemove', eventRemoveArg);
|
|
let addedEventDef = mutatedRelevantEvents.defs[eventDef.defId];
|
|
let addedEventInstance = mutatedRelevantEvents.instances[eventInstance.instanceId];
|
|
let addedEventApi = new EventImpl(receivingContext, addedEventDef, addedEventInstance);
|
|
receivingContext.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: mutatedRelevantEvents,
|
|
});
|
|
let eventAddArg = {
|
|
event: addedEventApi,
|
|
relatedEvents: buildEventApis(mutatedRelevantEvents, receivingContext, addedEventInstance),
|
|
revert() {
|
|
receivingContext.dispatch({
|
|
type: 'REMOVE_EVENTS',
|
|
eventStore: mutatedRelevantEvents,
|
|
});
|
|
},
|
|
};
|
|
receivingContext.emitter.trigger('eventAdd', eventAddArg);
|
|
if (ev.isTouch) {
|
|
receivingContext.dispatch({
|
|
type: 'SELECT_EVENT',
|
|
eventInstanceId: eventInstance.instanceId,
|
|
});
|
|
}
|
|
receivingContext.emitter.trigger('drop', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: ev.subjectEl, jsEvent: ev.origEvent, view: finalHit.context.viewApi }));
|
|
receivingContext.emitter.trigger('eventReceive', Object.assign(Object.assign({}, eventAddArg), { draggedEl: ev.subjectEl, view: finalHit.context.viewApi }));
|
|
}
|
|
}
|
|
else {
|
|
initialContext.emitter.trigger('_noEventDrop');
|
|
}
|
|
}
|
|
this.cleanup();
|
|
};
|
|
let { component } = this;
|
|
let { options } = component.context;
|
|
let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
|
|
dragging.pointer.selector = EventDragging.SELECTOR;
|
|
dragging.touchScrollAllowed = false;
|
|
dragging.autoScroller.isEnabled = options.dragScroll;
|
|
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsStore);
|
|
hitDragging.useSubjectCenter = settings.useEventCenter;
|
|
hitDragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
hitDragging.emitter.on('dragstart', this.handleDragStart);
|
|
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
|
|
hitDragging.emitter.on('pointerup', this.handlePointerUp);
|
|
hitDragging.emitter.on('dragend', this.handleDragEnd);
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
// render a drag state on the next receivingCalendar
|
|
displayDrag(nextContext, state) {
|
|
let initialContext = this.component.context;
|
|
let prevContext = this.receivingContext;
|
|
// does the previous calendar need to be cleared?
|
|
if (prevContext && prevContext !== nextContext) {
|
|
// does the initial calendar need to be cleared?
|
|
// if so, don't clear all the way. we still need to to hide the affectedEvents
|
|
if (prevContext === initialContext) {
|
|
prevContext.dispatch({
|
|
type: 'SET_EVENT_DRAG',
|
|
state: {
|
|
affectedEvents: state.affectedEvents,
|
|
mutatedEvents: createEmptyEventStore(),
|
|
isEvent: true,
|
|
},
|
|
});
|
|
// completely clear the old calendar if it wasn't the initial
|
|
}
|
|
else {
|
|
prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
|
|
}
|
|
}
|
|
if (nextContext) {
|
|
nextContext.dispatch({ type: 'SET_EVENT_DRAG', state });
|
|
}
|
|
}
|
|
clearDrag() {
|
|
let initialCalendar = this.component.context;
|
|
let { receivingContext } = this;
|
|
if (receivingContext) {
|
|
receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
|
|
}
|
|
// the initial calendar might have an dummy drag state from displayDrag
|
|
if (initialCalendar !== receivingContext) {
|
|
initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' });
|
|
}
|
|
}
|
|
cleanup() {
|
|
this.subjectSeg = null;
|
|
this.isDragging = false;
|
|
this.eventRange = null;
|
|
this.relevantEvents = null;
|
|
this.receivingContext = null;
|
|
this.validMutation = null;
|
|
this.mutatedRelevantEvents = null;
|
|
}
|
|
}
|
|
// TODO: test this in IE11
|
|
// QUESTION: why do we need it on the resizable???
|
|
EventDragging.SELECTOR = '.fc-event-draggable, .fc-event-resizable';
|
|
function computeEventMutation(hit0, hit1, eventInstanceStart, massagers) {
|
|
let dateSpan0 = hit0.dateSpan;
|
|
let dateSpan1 = hit1.dateSpan;
|
|
let date0 = dateSpan0.range.start;
|
|
let date1 = dateSpan1.range.start;
|
|
let standardProps = {};
|
|
if (dateSpan0.allDay !== dateSpan1.allDay) {
|
|
standardProps.allDay = dateSpan1.allDay;
|
|
standardProps.hasEnd = hit1.context.options.allDayMaintainDuration;
|
|
if (dateSpan1.allDay) {
|
|
// means date1 is already start-of-day,
|
|
// but date0 needs to be converted
|
|
date0 = startOfDay(eventInstanceStart);
|
|
}
|
|
else {
|
|
// Moving from allDate->timed
|
|
// Doesn't matter where on the event the drag began, mutate the event's start-date to date1
|
|
date0 = eventInstanceStart;
|
|
}
|
|
}
|
|
let delta = diffDates(date0, date1, hit0.context.dateEnv, hit0.componentId === hit1.componentId ?
|
|
hit0.largeUnit :
|
|
null);
|
|
if (delta.milliseconds) { // has hours/minutes/seconds
|
|
standardProps.allDay = false;
|
|
}
|
|
let mutation = {
|
|
datesDelta: delta,
|
|
standardProps,
|
|
};
|
|
for (let massager of massagers) {
|
|
massager(mutation, hit0, hit1);
|
|
}
|
|
return mutation;
|
|
}
|
|
function getComponentTouchDelay(component) {
|
|
let { options } = component.context;
|
|
let delay = options.eventLongPressDelay;
|
|
if (delay == null) {
|
|
delay = options.longPressDelay;
|
|
}
|
|
return delay;
|
|
}
|
|
|
|
class EventResizing extends internal_common_Interaction {
|
|
constructor(settings) {
|
|
super(settings);
|
|
// internal state
|
|
this.draggingSegEl = null;
|
|
this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg?
|
|
this.eventRange = null;
|
|
this.relevantEvents = null;
|
|
this.validMutation = null;
|
|
this.mutatedRelevantEvents = null;
|
|
this.handlePointerDown = (ev) => {
|
|
let { component } = this;
|
|
let segEl = this.querySegEl(ev);
|
|
let seg = getElSeg(segEl);
|
|
let eventRange = this.eventRange = seg.eventRange;
|
|
this.dragging.minDistance = component.context.options.eventDragMinDistance;
|
|
// if touch, need to be working with a selected event
|
|
this.dragging.setIgnoreMove(!this.component.isValidSegDownEl(ev.origEvent.target) ||
|
|
(ev.isTouch && this.component.props.eventSelection !== eventRange.instance.instanceId));
|
|
};
|
|
this.handleDragStart = (ev) => {
|
|
let { context } = this.component;
|
|
let eventRange = this.eventRange;
|
|
this.relevantEvents = getRelevantEvents(context.getCurrentData().eventStore, this.eventRange.instance.instanceId);
|
|
let segEl = this.querySegEl(ev);
|
|
this.draggingSegEl = segEl;
|
|
this.draggingSeg = getElSeg(segEl);
|
|
context.calendarApi.unselect();
|
|
context.emitter.trigger('eventResizeStart', {
|
|
el: segEl,
|
|
event: new EventImpl(context, eventRange.def, eventRange.instance),
|
|
jsEvent: ev.origEvent,
|
|
view: context.viewApi,
|
|
});
|
|
};
|
|
this.handleHitUpdate = (hit, isFinal, ev) => {
|
|
let { context } = this.component;
|
|
let relevantEvents = this.relevantEvents;
|
|
let initialHit = this.hitDragging.initialHit;
|
|
let eventInstance = this.eventRange.instance;
|
|
let mutation = null;
|
|
let mutatedRelevantEvents = null;
|
|
let isInvalid = false;
|
|
let interaction = {
|
|
affectedEvents: relevantEvents,
|
|
mutatedEvents: createEmptyEventStore(),
|
|
isEvent: true,
|
|
};
|
|
if (hit) {
|
|
let disallowed = hit.componentId === initialHit.componentId
|
|
&& this.isHitComboAllowed
|
|
&& !this.isHitComboAllowed(initialHit, hit);
|
|
if (!disallowed) {
|
|
mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-event-resizer-start'), eventInstance.range);
|
|
}
|
|
}
|
|
if (mutation) {
|
|
mutatedRelevantEvents = applyMutationToEventStore(relevantEvents, context.getCurrentData().eventUiBases, mutation, context);
|
|
interaction.mutatedEvents = mutatedRelevantEvents;
|
|
if (!isInteractionValid(interaction, hit.dateProfile, context)) {
|
|
isInvalid = true;
|
|
mutation = null;
|
|
mutatedRelevantEvents = null;
|
|
interaction.mutatedEvents = null;
|
|
}
|
|
}
|
|
if (mutatedRelevantEvents) {
|
|
context.dispatch({
|
|
type: 'SET_EVENT_RESIZE',
|
|
state: interaction,
|
|
});
|
|
}
|
|
else {
|
|
context.dispatch({ type: 'UNSET_EVENT_RESIZE' });
|
|
}
|
|
if (!isInvalid) {
|
|
enableCursor();
|
|
}
|
|
else {
|
|
disableCursor();
|
|
}
|
|
if (!isFinal) {
|
|
if (mutation && isHitsEqual(initialHit, hit)) {
|
|
mutation = null;
|
|
}
|
|
this.validMutation = mutation;
|
|
this.mutatedRelevantEvents = mutatedRelevantEvents;
|
|
}
|
|
};
|
|
this.handleDragEnd = (ev) => {
|
|
let { context } = this.component;
|
|
let eventDef = this.eventRange.def;
|
|
let eventInstance = this.eventRange.instance;
|
|
let eventApi = new EventImpl(context, eventDef, eventInstance);
|
|
let relevantEvents = this.relevantEvents;
|
|
let mutatedRelevantEvents = this.mutatedRelevantEvents;
|
|
context.emitter.trigger('eventResizeStop', {
|
|
el: this.draggingSegEl,
|
|
event: eventApi,
|
|
jsEvent: ev.origEvent,
|
|
view: context.viewApi,
|
|
});
|
|
if (this.validMutation) {
|
|
let updatedEventApi = new EventImpl(context, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null);
|
|
context.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: mutatedRelevantEvents,
|
|
});
|
|
let eventChangeArg = {
|
|
oldEvent: eventApi,
|
|
event: updatedEventApi,
|
|
relatedEvents: buildEventApis(mutatedRelevantEvents, context, eventInstance),
|
|
revert() {
|
|
context.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: relevantEvents, // the pre-change events
|
|
});
|
|
},
|
|
};
|
|
context.emitter.trigger('eventResize', Object.assign(Object.assign({}, eventChangeArg), { el: this.draggingSegEl, startDelta: this.validMutation.startDelta || createDuration(0), endDelta: this.validMutation.endDelta || createDuration(0), jsEvent: ev.origEvent, view: context.viewApi }));
|
|
context.emitter.trigger('eventChange', eventChangeArg);
|
|
}
|
|
else {
|
|
context.emitter.trigger('_noEventResize');
|
|
}
|
|
// reset all internal state
|
|
this.draggingSeg = null;
|
|
this.relevantEvents = null;
|
|
this.validMutation = null;
|
|
// okay to keep eventInstance around. useful to set it in handlePointerDown
|
|
};
|
|
let { component } = settings;
|
|
let dragging = this.dragging = new FeaturefulElementDragging(settings.el);
|
|
dragging.pointer.selector = '.fc-event-resizer';
|
|
dragging.touchScrollAllowed = false;
|
|
dragging.autoScroller.isEnabled = component.context.options.dragScroll;
|
|
let hitDragging = this.hitDragging = new HitDragging(this.dragging, interactionSettingsToStore(settings));
|
|
hitDragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
hitDragging.emitter.on('dragstart', this.handleDragStart);
|
|
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
|
|
hitDragging.emitter.on('dragend', this.handleDragEnd);
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
querySegEl(ev) {
|
|
return elementClosest(ev.subjectEl, '.fc-event');
|
|
}
|
|
}
|
|
function computeMutation(hit0, hit1, isFromStart, instanceRange) {
|
|
let dateEnv = hit0.context.dateEnv;
|
|
let date0 = hit0.dateSpan.range.start;
|
|
let date1 = hit1.dateSpan.range.start;
|
|
let delta = diffDates(date0, date1, dateEnv, hit0.largeUnit);
|
|
if (isFromStart) {
|
|
if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) {
|
|
return { startDelta: delta };
|
|
}
|
|
}
|
|
else if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) {
|
|
return { endDelta: delta };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
class UnselectAuto {
|
|
constructor(context) {
|
|
this.context = context;
|
|
this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system
|
|
this.matchesCancel = false;
|
|
this.matchesEvent = false;
|
|
this.onSelect = (selectInfo) => {
|
|
if (selectInfo.jsEvent) {
|
|
this.isRecentPointerDateSelect = true;
|
|
}
|
|
};
|
|
this.onDocumentPointerDown = (pev) => {
|
|
let unselectCancel = this.context.options.unselectCancel;
|
|
let downEl = getEventTargetViaRoot(pev.origEvent);
|
|
this.matchesCancel = !!elementClosest(downEl, unselectCancel);
|
|
this.matchesEvent = !!elementClosest(downEl, EventDragging.SELECTOR); // interaction started on an event?
|
|
};
|
|
this.onDocumentPointerUp = (pev) => {
|
|
let { context } = this;
|
|
let { documentPointer } = this;
|
|
let calendarState = context.getCurrentData();
|
|
// touch-scrolling should never unfocus any type of selection
|
|
if (!documentPointer.wasTouchScroll) {
|
|
if (calendarState.dateSelection && // an existing date selection?
|
|
!this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp?
|
|
) {
|
|
let unselectAuto = context.options.unselectAuto;
|
|
if (unselectAuto && (!unselectAuto || !this.matchesCancel)) {
|
|
context.calendarApi.unselect(pev);
|
|
}
|
|
}
|
|
if (calendarState.eventSelection && // an existing event selected?
|
|
!this.matchesEvent // interaction DIDN'T start on an event
|
|
) {
|
|
context.dispatch({ type: 'UNSELECT_EVENT' });
|
|
}
|
|
}
|
|
this.isRecentPointerDateSelect = false;
|
|
};
|
|
let documentPointer = this.documentPointer = new PointerDragging(document);
|
|
documentPointer.shouldIgnoreMove = true;
|
|
documentPointer.shouldWatchScroll = false;
|
|
documentPointer.emitter.on('pointerdown', this.onDocumentPointerDown);
|
|
documentPointer.emitter.on('pointerup', this.onDocumentPointerUp);
|
|
/*
|
|
TODO: better way to know about whether there was a selection with the pointer
|
|
*/
|
|
context.emitter.on('select', this.onSelect);
|
|
}
|
|
destroy() {
|
|
this.context.emitter.off('select', this.onSelect);
|
|
this.documentPointer.destroy();
|
|
}
|
|
}
|
|
|
|
const OPTION_REFINERS = {
|
|
fixedMirrorParent: identity,
|
|
};
|
|
const LISTENER_REFINERS = {
|
|
dateClick: identity,
|
|
eventDragStart: identity,
|
|
eventDragStop: identity,
|
|
eventDrop: identity,
|
|
eventResizeStart: identity,
|
|
eventResizeStop: identity,
|
|
eventResize: identity,
|
|
drop: identity,
|
|
eventReceive: identity,
|
|
eventLeave: identity,
|
|
};
|
|
|
|
/*
|
|
Given an already instantiated draggable object for one-or-more elements,
|
|
Interprets any dragging as an attempt to drag an events that lives outside
|
|
of a calendar onto a calendar.
|
|
*/
|
|
class ExternalElementDragging {
|
|
constructor(dragging, suppliedDragMeta) {
|
|
this.receivingContext = null;
|
|
this.droppableEvent = null; // will exist for all drags, even if create:false
|
|
this.suppliedDragMeta = null;
|
|
this.dragMeta = null;
|
|
this.handleDragStart = (ev) => {
|
|
this.dragMeta = this.buildDragMeta(ev.subjectEl);
|
|
};
|
|
this.handleHitUpdate = (hit, isFinal, ev) => {
|
|
let { dragging } = this.hitDragging;
|
|
let receivingContext = null;
|
|
let droppableEvent = null;
|
|
let isInvalid = false;
|
|
let interaction = {
|
|
affectedEvents: interaction_createEmptyEventStore(),
|
|
mutatedEvents: interaction_createEmptyEventStore(),
|
|
isEvent: this.dragMeta.create,
|
|
};
|
|
if (hit) {
|
|
receivingContext = hit.context;
|
|
if (this.canDropElOnCalendar(ev.subjectEl, receivingContext)) {
|
|
droppableEvent = computeEventForDateSpan(hit.dateSpan, this.dragMeta, receivingContext);
|
|
interaction.mutatedEvents = interaction_eventTupleToStore(droppableEvent);
|
|
isInvalid = !interaction_isInteractionValid(interaction, hit.dateProfile, receivingContext);
|
|
if (isInvalid) {
|
|
interaction.mutatedEvents = interaction_createEmptyEventStore();
|
|
droppableEvent = null;
|
|
}
|
|
}
|
|
}
|
|
this.displayDrag(receivingContext, interaction);
|
|
// show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?)
|
|
// TODO: wish we could somehow wait for dispatch to guarantee render
|
|
dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-event-mirror'));
|
|
if (!isInvalid) {
|
|
interaction_enableCursor();
|
|
}
|
|
else {
|
|
interaction_disableCursor();
|
|
}
|
|
if (!isFinal) {
|
|
dragging.setMirrorNeedsRevert(!droppableEvent);
|
|
this.receivingContext = receivingContext;
|
|
this.droppableEvent = droppableEvent;
|
|
}
|
|
};
|
|
this.handleDragEnd = (pev) => {
|
|
let { receivingContext, droppableEvent } = this;
|
|
this.clearDrag();
|
|
if (receivingContext && droppableEvent) {
|
|
let finalHit = this.hitDragging.finalHit;
|
|
let finalView = finalHit.context.viewApi;
|
|
let dragMeta = this.dragMeta;
|
|
receivingContext.emitter.trigger('drop', Object.assign(Object.assign({}, buildDatePointApiWithContext(finalHit.dateSpan, receivingContext)), { draggedEl: pev.subjectEl, jsEvent: pev.origEvent, view: finalView }));
|
|
if (dragMeta.create) {
|
|
let addingEvents = interaction_eventTupleToStore(droppableEvent);
|
|
receivingContext.dispatch({
|
|
type: 'MERGE_EVENTS',
|
|
eventStore: addingEvents,
|
|
});
|
|
if (pev.isTouch) {
|
|
receivingContext.dispatch({
|
|
type: 'SELECT_EVENT',
|
|
eventInstanceId: droppableEvent.instance.instanceId,
|
|
});
|
|
}
|
|
// signal that an external event landed
|
|
receivingContext.emitter.trigger('eventReceive', {
|
|
event: new interaction_EventImpl(receivingContext, droppableEvent.def, droppableEvent.instance),
|
|
relatedEvents: [],
|
|
revert() {
|
|
receivingContext.dispatch({
|
|
type: 'REMOVE_EVENTS',
|
|
eventStore: addingEvents,
|
|
});
|
|
},
|
|
draggedEl: pev.subjectEl,
|
|
view: finalView,
|
|
});
|
|
}
|
|
}
|
|
this.receivingContext = null;
|
|
this.droppableEvent = null;
|
|
};
|
|
let hitDragging = this.hitDragging = new HitDragging(dragging, interaction_interactionSettingsStore);
|
|
hitDragging.requireInitial = false; // will start outside of a component
|
|
hitDragging.emitter.on('dragstart', this.handleDragStart);
|
|
hitDragging.emitter.on('hitupdate', this.handleHitUpdate);
|
|
hitDragging.emitter.on('dragend', this.handleDragEnd);
|
|
this.suppliedDragMeta = suppliedDragMeta;
|
|
}
|
|
buildDragMeta(subjectEl) {
|
|
if (typeof this.suppliedDragMeta === 'object') {
|
|
return interaction_parseDragMeta(this.suppliedDragMeta);
|
|
}
|
|
if (typeof this.suppliedDragMeta === 'function') {
|
|
return interaction_parseDragMeta(this.suppliedDragMeta(subjectEl));
|
|
}
|
|
return getDragMetaFromEl(subjectEl);
|
|
}
|
|
displayDrag(nextContext, state) {
|
|
let prevContext = this.receivingContext;
|
|
if (prevContext && prevContext !== nextContext) {
|
|
prevContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
|
|
}
|
|
if (nextContext) {
|
|
nextContext.dispatch({ type: 'SET_EVENT_DRAG', state });
|
|
}
|
|
}
|
|
clearDrag() {
|
|
if (this.receivingContext) {
|
|
this.receivingContext.dispatch({ type: 'UNSET_EVENT_DRAG' });
|
|
}
|
|
}
|
|
canDropElOnCalendar(el, receivingContext) {
|
|
let dropAccept = receivingContext.options.dropAccept;
|
|
if (typeof dropAccept === 'function') {
|
|
return dropAccept.call(receivingContext.calendarApi, el);
|
|
}
|
|
if (typeof dropAccept === 'string' && dropAccept) {
|
|
return Boolean(interaction_elementMatches(el, dropAccept));
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
// Utils for computing event store from the DragMeta
|
|
// ----------------------------------------------------------------------------------------------------
|
|
function computeEventForDateSpan(dateSpan, dragMeta, context) {
|
|
let defProps = Object.assign({}, dragMeta.leftoverProps);
|
|
for (let transform of context.pluginHooks.externalDefTransforms) {
|
|
Object.assign(defProps, transform(dateSpan, dragMeta));
|
|
}
|
|
let { refined, extra } = interaction_refineEventDef(defProps, context);
|
|
let def = interaction_parseEventDef(refined, extra, dragMeta.sourceId, dateSpan.allDay, context.options.forceEventDuration || Boolean(dragMeta.duration), // hasEnd
|
|
context);
|
|
let start = dateSpan.range.start;
|
|
// only rely on time info if drop zone is all-day,
|
|
// otherwise, we already know the time
|
|
if (dateSpan.allDay && dragMeta.startTime) {
|
|
start = context.dateEnv.add(start, dragMeta.startTime);
|
|
}
|
|
let end = dragMeta.duration ?
|
|
context.dateEnv.add(start, dragMeta.duration) :
|
|
interaction_getDefaultEventEnd(dateSpan.allDay, start, context);
|
|
let instance = interaction_createEventInstance(def.defId, { start, end });
|
|
return { def, instance };
|
|
}
|
|
// Utils for extracting data from element
|
|
// ----------------------------------------------------------------------------------------------------
|
|
function getDragMetaFromEl(el) {
|
|
let str = getEmbeddedElData(el, 'event');
|
|
let obj = str ?
|
|
JSON.parse(str) :
|
|
{ create: false }; // if no embedded data, assume no event creation
|
|
return interaction_parseDragMeta(obj);
|
|
}
|
|
config.dataAttrPrefix = '';
|
|
function getEmbeddedElData(el, name) {
|
|
let prefix = interaction_config.dataAttrPrefix;
|
|
let prefixedName = (prefix ? prefix + '-' : '') + name;
|
|
return el.getAttribute('data-' + prefixedName) || '';
|
|
}
|
|
|
|
/*
|
|
Makes an element (that is *external* to any calendar) draggable.
|
|
Can pass in data that determines how an event will be created when dropped onto a calendar.
|
|
Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system.
|
|
*/
|
|
class ExternalDraggable {
|
|
constructor(el, settings = {}) {
|
|
this.handlePointerDown = (ev) => {
|
|
let { dragging } = this;
|
|
let { minDistance, longPressDelay } = this.settings;
|
|
dragging.minDistance =
|
|
minDistance != null ?
|
|
minDistance :
|
|
(ev.isTouch ? 0 : interaction_BASE_OPTION_DEFAULTS.eventDragMinDistance);
|
|
dragging.delay =
|
|
ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv
|
|
(longPressDelay != null ? longPressDelay : interaction_BASE_OPTION_DEFAULTS.longPressDelay) :
|
|
0;
|
|
};
|
|
this.handleDragStart = (ev) => {
|
|
if (ev.isTouch &&
|
|
this.dragging.delay &&
|
|
ev.subjectEl.classList.contains('fc-event')) {
|
|
this.dragging.mirror.getMirrorEl().classList.add('fc-event-selected');
|
|
}
|
|
};
|
|
this.settings = settings;
|
|
let dragging = this.dragging = new FeaturefulElementDragging(el);
|
|
dragging.touchScrollAllowed = false;
|
|
if (settings.itemSelector != null) {
|
|
dragging.pointer.selector = settings.itemSelector;
|
|
}
|
|
if (settings.appendTo != null) {
|
|
dragging.mirror.parentNode = settings.appendTo; // TODO: write tests
|
|
}
|
|
dragging.emitter.on('pointerdown', this.handlePointerDown);
|
|
dragging.emitter.on('dragstart', this.handleDragStart);
|
|
new ExternalElementDragging(dragging, settings.eventData); // eslint-disable-line no-new
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
}
|
|
|
|
/*
|
|
Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements.
|
|
The third-party system is responsible for drawing the visuals effects of the drag.
|
|
This class simply monitors for pointer movements and fires events.
|
|
It also has the ability to hide the moving element (the "mirror") during the drag.
|
|
*/
|
|
class InferredElementDragging extends ElementDragging {
|
|
constructor(containerEl) {
|
|
super(containerEl);
|
|
this.shouldIgnoreMove = false;
|
|
this.mirrorSelector = '';
|
|
this.currentMirrorEl = null;
|
|
this.handlePointerDown = (ev) => {
|
|
this.emitter.trigger('pointerdown', ev);
|
|
if (!this.shouldIgnoreMove) {
|
|
// fire dragstart right away. does not support delay or min-distance
|
|
this.emitter.trigger('dragstart', ev);
|
|
}
|
|
};
|
|
this.handlePointerMove = (ev) => {
|
|
if (!this.shouldIgnoreMove) {
|
|
this.emitter.trigger('dragmove', ev);
|
|
}
|
|
};
|
|
this.handlePointerUp = (ev) => {
|
|
this.emitter.trigger('pointerup', ev);
|
|
if (!this.shouldIgnoreMove) {
|
|
// fire dragend right away. does not support a revert animation
|
|
this.emitter.trigger('dragend', ev);
|
|
}
|
|
};
|
|
let pointer = this.pointer = new PointerDragging(containerEl);
|
|
pointer.emitter.on('pointerdown', this.handlePointerDown);
|
|
pointer.emitter.on('pointermove', this.handlePointerMove);
|
|
pointer.emitter.on('pointerup', this.handlePointerUp);
|
|
}
|
|
destroy() {
|
|
this.pointer.destroy();
|
|
}
|
|
setIgnoreMove(bool) {
|
|
this.shouldIgnoreMove = bool;
|
|
}
|
|
setMirrorIsVisible(bool) {
|
|
if (bool) {
|
|
// restore a previously hidden element.
|
|
// use the reference in case the selector class has already been removed.
|
|
if (this.currentMirrorEl) {
|
|
this.currentMirrorEl.style.visibility = '';
|
|
this.currentMirrorEl = null;
|
|
}
|
|
}
|
|
else {
|
|
let mirrorEl = this.mirrorSelector
|
|
// TODO: somehow query FullCalendars WITHIN shadow-roots
|
|
? document.querySelector(this.mirrorSelector)
|
|
: null;
|
|
if (mirrorEl) {
|
|
this.currentMirrorEl = mirrorEl;
|
|
mirrorEl.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Bridges third-party drag-n-drop systems with FullCalendar.
|
|
Must be instantiated and destroyed by caller.
|
|
*/
|
|
class ThirdPartyDraggable {
|
|
constructor(containerOrSettings, settings) {
|
|
let containerEl = document;
|
|
if (
|
|
// wish we could just test instanceof EventTarget, but doesn't work in IE11
|
|
containerOrSettings === document ||
|
|
containerOrSettings instanceof Element) {
|
|
containerEl = containerOrSettings;
|
|
settings = settings || {};
|
|
}
|
|
else {
|
|
settings = (containerOrSettings || {});
|
|
}
|
|
let dragging = this.dragging = new InferredElementDragging(containerEl);
|
|
if (typeof settings.itemSelector === 'string') {
|
|
dragging.pointer.selector = settings.itemSelector;
|
|
}
|
|
else if (containerEl === document) {
|
|
dragging.pointer.selector = '[data-event]';
|
|
}
|
|
if (typeof settings.mirrorSelector === 'string') {
|
|
dragging.mirrorSelector = settings.mirrorSelector;
|
|
}
|
|
let externalDragging = new ExternalElementDragging(dragging, settings.eventData);
|
|
// The hit-detection system requires that the dnd-mirror-element be pointer-events:none,
|
|
// but this can't be guaranteed for third-party draggables, so disable
|
|
externalDragging.hitDragging.disablePointCheck = true;
|
|
}
|
|
destroy() {
|
|
this.dragging.destroy();
|
|
}
|
|
}
|
|
|
|
var interaction_index = createPlugin({
|
|
name: '@fullcalendar/interaction',
|
|
componentInteractions: [DateClicking, DateSelecting, EventDragging, EventResizing],
|
|
calendarInteractions: [UnselectAuto],
|
|
elementDraggingImpl: FeaturefulElementDragging,
|
|
optionRefiners: OPTION_REFINERS,
|
|
listenerRefiners: LISTENER_REFINERS,
|
|
});
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/daygrid/internal.js
|
|
|
|
|
|
|
|
/* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells.
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
// It is a manager for a Table subcomponent, which does most of the heavy lifting.
|
|
// It is responsible for managing width/height.
|
|
class TableView extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.headerElRef = d();
|
|
}
|
|
renderSimpleLayout(headerRowContent, bodyContent) {
|
|
let { props, context } = this;
|
|
let sections = [];
|
|
let stickyHeaderDates = getStickyHeaderDates(context.options);
|
|
if (headerRowContent) {
|
|
sections.push({
|
|
type: 'header',
|
|
key: 'header',
|
|
isSticky: stickyHeaderDates,
|
|
chunk: {
|
|
elRef: this.headerElRef,
|
|
tableClassName: 'fc-col-header',
|
|
rowContent: headerRowContent,
|
|
},
|
|
});
|
|
}
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'body',
|
|
liquid: true,
|
|
chunk: { content: bodyContent },
|
|
});
|
|
return (y(ViewContainer, { elClasses: ['fc-daygrid'], viewSpec: context.viewSpec },
|
|
y(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, cols: [] /* TODO: make optional? */, sections: sections })));
|
|
}
|
|
renderHScrollLayout(headerRowContent, bodyContent, colCnt, dayMinWidth) {
|
|
let ScrollGrid = this.context.pluginHooks.scrollGridImpl;
|
|
if (!ScrollGrid) {
|
|
throw new Error('No ScrollGrid implementation');
|
|
}
|
|
let { props, context } = this;
|
|
let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options);
|
|
let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options);
|
|
let sections = [];
|
|
if (headerRowContent) {
|
|
sections.push({
|
|
type: 'header',
|
|
key: 'header',
|
|
isSticky: stickyHeaderDates,
|
|
chunks: [{
|
|
key: 'main',
|
|
elRef: this.headerElRef,
|
|
tableClassName: 'fc-col-header',
|
|
rowContent: headerRowContent,
|
|
}],
|
|
});
|
|
}
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'body',
|
|
liquid: true,
|
|
chunks: [{
|
|
key: 'main',
|
|
content: bodyContent,
|
|
}],
|
|
});
|
|
if (stickyFooterScrollbar) {
|
|
sections.push({
|
|
type: 'footer',
|
|
key: 'footer',
|
|
isSticky: true,
|
|
chunks: [{
|
|
key: 'main',
|
|
content: renderScrollShim,
|
|
}],
|
|
});
|
|
}
|
|
return (y(ViewContainer, { elClasses: ['fc-daygrid'], viewSpec: context.viewSpec },
|
|
y(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, forPrint: props.forPrint, collapsibleWidth: props.forPrint, colGroups: [{ cols: [{ span: colCnt, minWidth: dayMinWidth }] }], sections: sections })));
|
|
}
|
|
}
|
|
|
|
function splitSegsByRow(segs, rowCnt) {
|
|
let byRow = [];
|
|
for (let i = 0; i < rowCnt; i += 1) {
|
|
byRow[i] = [];
|
|
}
|
|
for (let seg of segs) {
|
|
byRow[seg.row].push(seg);
|
|
}
|
|
return byRow;
|
|
}
|
|
function splitSegsByFirstCol(segs, colCnt) {
|
|
let byCol = [];
|
|
for (let i = 0; i < colCnt; i += 1) {
|
|
byCol[i] = [];
|
|
}
|
|
for (let seg of segs) {
|
|
byCol[seg.firstCol].push(seg);
|
|
}
|
|
return byCol;
|
|
}
|
|
function splitInteractionByRow(ui, rowCnt) {
|
|
let byRow = [];
|
|
if (!ui) {
|
|
for (let i = 0; i < rowCnt; i += 1) {
|
|
byRow[i] = null;
|
|
}
|
|
}
|
|
else {
|
|
for (let i = 0; i < rowCnt; i += 1) {
|
|
byRow[i] = {
|
|
affectedInstances: ui.affectedInstances,
|
|
isEvent: ui.isEvent,
|
|
segs: [],
|
|
};
|
|
}
|
|
for (let seg of ui.segs) {
|
|
byRow[seg.row].segs.push(seg);
|
|
}
|
|
}
|
|
return byRow;
|
|
}
|
|
|
|
const DEFAULT_TABLE_EVENT_TIME_FORMAT = createFormatter({
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
omitZeroMinute: true,
|
|
meridiem: 'narrow',
|
|
});
|
|
function hasListItemDisplay(seg) {
|
|
let { display } = seg.eventRange.ui;
|
|
return display === 'list-item' || (display === 'auto' &&
|
|
!seg.eventRange.def.allDay &&
|
|
seg.firstCol === seg.lastCol && // can't be multi-day
|
|
seg.isStart && // "
|
|
seg.isEnd // "
|
|
);
|
|
}
|
|
|
|
class TableBlockEvent extends BaseComponent {
|
|
render() {
|
|
let { props } = this;
|
|
return (y(StandardEvent, Object.assign({}, props, { elClasses: ['fc-daygrid-event', 'fc-daygrid-block-event', 'fc-h-event'], defaultTimeFormat: DEFAULT_TABLE_EVENT_TIME_FORMAT, defaultDisplayEventEnd: props.defaultDisplayEventEnd, disableResizing: !props.seg.eventRange.def.allDay })));
|
|
}
|
|
}
|
|
|
|
class TableListItemEvent extends BaseComponent {
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let { seg } = props;
|
|
let timeFormat = options.eventTimeFormat || DEFAULT_TABLE_EVENT_TIME_FORMAT;
|
|
let timeText = buildSegTimeText(seg, timeFormat, context, true, props.defaultDisplayEventEnd);
|
|
return (y(EventContainer, Object.assign({}, props, { elTag: "a", elClasses: ['fc-daygrid-event', 'fc-daygrid-dot-event'], elAttrs: getSegAnchorAttrs(props.seg, context), defaultGenerator: internal_renderInnerContent, timeText: timeText, isResizing: false, isDateSelecting: false })));
|
|
}
|
|
}
|
|
function internal_renderInnerContent(renderProps) {
|
|
return (y(_, null,
|
|
y("div", { className: "fc-daygrid-event-dot", style: { borderColor: renderProps.borderColor || renderProps.backgroundColor } }),
|
|
renderProps.timeText && (y("div", { className: "fc-event-time" }, renderProps.timeText)),
|
|
y("div", { className: "fc-event-title" }, renderProps.event.title || y(_, null, "\u00A0"))));
|
|
}
|
|
|
|
class TableCellMoreLink extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.compileSegs = memoize(compileSegs);
|
|
}
|
|
render() {
|
|
let { props } = this;
|
|
let { allSegs, invisibleSegs } = this.compileSegs(props.singlePlacements);
|
|
return (y(MoreLinkContainer, { elClasses: ['fc-daygrid-more-link'], dateProfile: props.dateProfile, todayRange: props.todayRange, allDayDate: props.allDayDate, moreCnt: props.moreCnt, allSegs: allSegs, hiddenSegs: invisibleSegs, alignmentElRef: props.alignmentElRef, alignGridTop: props.alignGridTop, extraDateSpan: props.extraDateSpan, popoverContent: () => {
|
|
let isForcedInvisible = (props.eventDrag ? props.eventDrag.affectedInstances : null) ||
|
|
(props.eventResize ? props.eventResize.affectedInstances : null) ||
|
|
{};
|
|
return (y(_, null, allSegs.map((seg) => {
|
|
let instanceId = seg.eventRange.instance.instanceId;
|
|
return (y("div", { className: "fc-daygrid-event-harness", key: instanceId, style: {
|
|
visibility: isForcedInvisible[instanceId] ? 'hidden' : '',
|
|
} }, hasListItemDisplay(seg) ? (y(TableListItemEvent, Object.assign({ seg: seg, isDragging: false, isSelected: instanceId === props.eventSelection, defaultDisplayEventEnd: false }, getSegMeta(seg, props.todayRange)))) : (y(TableBlockEvent, Object.assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === props.eventSelection, defaultDisplayEventEnd: false }, getSegMeta(seg, props.todayRange))))));
|
|
})));
|
|
} }));
|
|
}
|
|
}
|
|
function compileSegs(singlePlacements) {
|
|
let allSegs = [];
|
|
let invisibleSegs = [];
|
|
for (let placement of singlePlacements) {
|
|
allSegs.push(placement.seg);
|
|
if (!placement.isVisible) {
|
|
invisibleSegs.push(placement.seg);
|
|
}
|
|
}
|
|
return { allSegs, invisibleSegs };
|
|
}
|
|
|
|
const DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'narrow' });
|
|
class TableCell extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.rootElRef = d();
|
|
this.state = {
|
|
dayNumberId: getUniqueDomId(),
|
|
};
|
|
this.handleRootEl = (el) => {
|
|
setRef(this.rootElRef, el);
|
|
setRef(this.props.elRef, el);
|
|
};
|
|
}
|
|
render() {
|
|
let { context, props, state, rootElRef } = this;
|
|
let { options, dateEnv } = context;
|
|
let { date, dateProfile } = props;
|
|
// TODO: memoize this?
|
|
const isMonthStart = props.showDayNumber &&
|
|
shouldDisplayMonthStart(date, dateProfile.currentRange, dateEnv);
|
|
return (y(DayCellContainer, { elTag: "td", elRef: this.handleRootEl, elClasses: [
|
|
'fc-daygrid-day',
|
|
...(props.extraClassNames || []),
|
|
], elAttrs: Object.assign(Object.assign(Object.assign({}, props.extraDataAttrs), (props.showDayNumber ? { 'aria-labelledby': state.dayNumberId } : {})), { role: 'gridcell' }), defaultGenerator: renderTopInner, date: date, dateProfile: dateProfile, todayRange: props.todayRange, showDayNumber: props.showDayNumber, isMonthStart: isMonthStart, extraRenderProps: props.extraRenderProps }, (InnerContent, renderProps) => (y("div", { ref: props.innerElRef, className: "fc-daygrid-day-frame fc-scrollgrid-sync-inner", style: { minHeight: props.minHeight } },
|
|
props.showWeekNumber && (y(WeekNumberContainer, { elTag: "a", elClasses: ['fc-daygrid-week-number'], elAttrs: buildNavLinkAttrs(context, date, 'week'), date: date, defaultFormat: DEFAULT_WEEK_NUM_FORMAT })),
|
|
!renderProps.isDisabled &&
|
|
(props.showDayNumber || hasCustomDayCellContent(options) || props.forceDayTop) ? (y("div", { className: "fc-daygrid-day-top" },
|
|
y(InnerContent, { elTag: "a", elClasses: [
|
|
'fc-daygrid-day-number',
|
|
isMonthStart && 'fc-daygrid-month-start',
|
|
], elAttrs: Object.assign(Object.assign({}, buildNavLinkAttrs(context, date)), { id: state.dayNumberId }) }))) : props.showDayNumber ? (
|
|
// for creating correct amount of space (see issue #7162)
|
|
y("div", { className: "fc-daygrid-day-top", style: { visibility: 'hidden' } },
|
|
y("a", { className: "fc-daygrid-day-number" }, "\u00A0"))) : undefined,
|
|
y("div", { className: "fc-daygrid-day-events", ref: props.fgContentElRef },
|
|
props.fgContent,
|
|
y("div", { className: "fc-daygrid-day-bottom", style: { marginTop: props.moreMarginTop } },
|
|
y(TableCellMoreLink, { allDayDate: date, singlePlacements: props.singlePlacements, moreCnt: props.moreCnt, alignmentElRef: rootElRef, alignGridTop: !props.showDayNumber, extraDateSpan: props.extraDateSpan, dateProfile: props.dateProfile, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange }))),
|
|
y("div", { className: "fc-daygrid-day-bg" }, props.bgContent)))));
|
|
}
|
|
}
|
|
function renderTopInner(props) {
|
|
return props.dayNumberText || y(_, null, "\u00A0");
|
|
}
|
|
function shouldDisplayMonthStart(date, currentRange, dateEnv) {
|
|
const { start: currentStart, end: currentEnd } = currentRange;
|
|
const currentEndIncl = addMs(currentEnd, -1);
|
|
const currentFirstYear = dateEnv.getYear(currentStart);
|
|
const currentFirstMonth = dateEnv.getMonth(currentStart);
|
|
const currentLastYear = dateEnv.getYear(currentEndIncl);
|
|
const currentLastMonth = dateEnv.getMonth(currentEndIncl);
|
|
// spans more than one month?
|
|
return !(currentFirstYear === currentLastYear && currentFirstMonth === currentLastMonth) &&
|
|
Boolean(
|
|
// first date in current view?
|
|
date.valueOf() === currentStart.valueOf() ||
|
|
// a month-start that's within the current range?
|
|
(dateEnv.getDay(date) === 1 && date.valueOf() < currentEnd.valueOf()));
|
|
}
|
|
|
|
function generateSegKey(seg) {
|
|
return seg.eventRange.instance.instanceId + ':' + seg.firstCol;
|
|
}
|
|
function generateSegUid(seg) {
|
|
return generateSegKey(seg) + ':' + seg.lastCol;
|
|
}
|
|
function computeFgSegPlacement(segs, // assumed already sorted
|
|
dayMaxEvents, dayMaxEventRows, strictOrder, segHeights, maxContentHeight, cells) {
|
|
let hierarchy = new DayGridSegHierarchy((segEntry) => {
|
|
// TODO: more DRY with generateSegUid
|
|
let segUid = segs[segEntry.index].eventRange.instance.instanceId +
|
|
':' + segEntry.span.start +
|
|
':' + (segEntry.span.end - 1);
|
|
// if no thickness known, assume 1 (if 0, so small it always fits)
|
|
return segHeights[segUid] || 1;
|
|
});
|
|
hierarchy.allowReslicing = true;
|
|
hierarchy.strictOrder = strictOrder;
|
|
if (dayMaxEvents === true || dayMaxEventRows === true) {
|
|
hierarchy.maxCoord = maxContentHeight;
|
|
hierarchy.hiddenConsumes = true;
|
|
}
|
|
else if (typeof dayMaxEvents === 'number') {
|
|
hierarchy.maxStackCnt = dayMaxEvents;
|
|
}
|
|
else if (typeof dayMaxEventRows === 'number') {
|
|
hierarchy.maxStackCnt = dayMaxEventRows;
|
|
hierarchy.hiddenConsumes = true;
|
|
}
|
|
// create segInputs only for segs with known heights
|
|
let segInputs = [];
|
|
let unknownHeightSegs = [];
|
|
for (let i = 0; i < segs.length; i += 1) {
|
|
let seg = segs[i];
|
|
let segUid = generateSegUid(seg);
|
|
let eventHeight = segHeights[segUid];
|
|
if (eventHeight != null) {
|
|
segInputs.push({
|
|
index: i,
|
|
span: {
|
|
start: seg.firstCol,
|
|
end: seg.lastCol + 1,
|
|
},
|
|
});
|
|
}
|
|
else {
|
|
unknownHeightSegs.push(seg);
|
|
}
|
|
}
|
|
let hiddenEntries = hierarchy.addSegs(segInputs);
|
|
let segRects = hierarchy.toRects();
|
|
let { singleColPlacements, multiColPlacements, leftoverMargins } = placeRects(segRects, segs, cells);
|
|
let moreCnts = [];
|
|
let moreMarginTops = [];
|
|
// add segs with unknown heights
|
|
for (let seg of unknownHeightSegs) {
|
|
multiColPlacements[seg.firstCol].push({
|
|
seg,
|
|
isVisible: false,
|
|
isAbsolute: true,
|
|
absoluteTop: 0,
|
|
marginTop: 0,
|
|
});
|
|
for (let col = seg.firstCol; col <= seg.lastCol; col += 1) {
|
|
singleColPlacements[col].push({
|
|
seg: resliceSeg(seg, col, col + 1, cells),
|
|
isVisible: false,
|
|
isAbsolute: false,
|
|
absoluteTop: 0,
|
|
marginTop: 0,
|
|
});
|
|
}
|
|
}
|
|
// add the hidden entries
|
|
for (let col = 0; col < cells.length; col += 1) {
|
|
moreCnts.push(0);
|
|
}
|
|
for (let hiddenEntry of hiddenEntries) {
|
|
let seg = segs[hiddenEntry.index];
|
|
let hiddenSpan = hiddenEntry.span;
|
|
multiColPlacements[hiddenSpan.start].push({
|
|
seg: resliceSeg(seg, hiddenSpan.start, hiddenSpan.end, cells),
|
|
isVisible: false,
|
|
isAbsolute: true,
|
|
absoluteTop: 0,
|
|
marginTop: 0,
|
|
});
|
|
for (let col = hiddenSpan.start; col < hiddenSpan.end; col += 1) {
|
|
moreCnts[col] += 1;
|
|
singleColPlacements[col].push({
|
|
seg: resliceSeg(seg, col, col + 1, cells),
|
|
isVisible: false,
|
|
isAbsolute: false,
|
|
absoluteTop: 0,
|
|
marginTop: 0,
|
|
});
|
|
}
|
|
}
|
|
// deal with leftover margins
|
|
for (let col = 0; col < cells.length; col += 1) {
|
|
moreMarginTops.push(leftoverMargins[col]);
|
|
}
|
|
return { singleColPlacements, multiColPlacements, moreCnts, moreMarginTops };
|
|
}
|
|
// rects ordered by top coord, then left
|
|
function placeRects(allRects, segs, cells) {
|
|
let rectsByEachCol = groupRectsByEachCol(allRects, cells.length);
|
|
let singleColPlacements = [];
|
|
let multiColPlacements = [];
|
|
let leftoverMargins = [];
|
|
for (let col = 0; col < cells.length; col += 1) {
|
|
let rects = rectsByEachCol[col];
|
|
// compute all static segs in singlePlacements
|
|
let singlePlacements = [];
|
|
let currentHeight = 0;
|
|
let currentMarginTop = 0;
|
|
for (let rect of rects) {
|
|
let seg = segs[rect.index];
|
|
singlePlacements.push({
|
|
seg: resliceSeg(seg, col, col + 1, cells),
|
|
isVisible: true,
|
|
isAbsolute: false,
|
|
absoluteTop: rect.levelCoord,
|
|
marginTop: rect.levelCoord - currentHeight,
|
|
});
|
|
currentHeight = rect.levelCoord + rect.thickness;
|
|
}
|
|
// compute mixed static/absolute segs in multiPlacements
|
|
let multiPlacements = [];
|
|
currentHeight = 0;
|
|
currentMarginTop = 0;
|
|
for (let rect of rects) {
|
|
let seg = segs[rect.index];
|
|
let isAbsolute = rect.span.end - rect.span.start > 1; // multi-column?
|
|
let isFirstCol = rect.span.start === col;
|
|
currentMarginTop += rect.levelCoord - currentHeight; // amount of space since bottom of previous seg
|
|
currentHeight = rect.levelCoord + rect.thickness; // height will now be bottom of current seg
|
|
if (isAbsolute) {
|
|
currentMarginTop += rect.thickness;
|
|
if (isFirstCol) {
|
|
multiPlacements.push({
|
|
seg: resliceSeg(seg, rect.span.start, rect.span.end, cells),
|
|
isVisible: true,
|
|
isAbsolute: true,
|
|
absoluteTop: rect.levelCoord,
|
|
marginTop: 0,
|
|
});
|
|
}
|
|
}
|
|
else if (isFirstCol) {
|
|
multiPlacements.push({
|
|
seg: resliceSeg(seg, rect.span.start, rect.span.end, cells),
|
|
isVisible: true,
|
|
isAbsolute: false,
|
|
absoluteTop: rect.levelCoord,
|
|
marginTop: currentMarginTop, // claim the margin
|
|
});
|
|
currentMarginTop = 0;
|
|
}
|
|
}
|
|
singleColPlacements.push(singlePlacements);
|
|
multiColPlacements.push(multiPlacements);
|
|
leftoverMargins.push(currentMarginTop);
|
|
}
|
|
return { singleColPlacements, multiColPlacements, leftoverMargins };
|
|
}
|
|
function groupRectsByEachCol(rects, colCnt) {
|
|
let rectsByEachCol = [];
|
|
for (let col = 0; col < colCnt; col += 1) {
|
|
rectsByEachCol.push([]);
|
|
}
|
|
for (let rect of rects) {
|
|
for (let col = rect.span.start; col < rect.span.end; col += 1) {
|
|
rectsByEachCol[col].push(rect);
|
|
}
|
|
}
|
|
return rectsByEachCol;
|
|
}
|
|
function resliceSeg(seg, spanStart, spanEnd, cells) {
|
|
if (seg.firstCol === spanStart && seg.lastCol === spanEnd - 1) {
|
|
return seg;
|
|
}
|
|
let eventRange = seg.eventRange;
|
|
let origRange = eventRange.range;
|
|
let slicedRange = intersectRanges(origRange, {
|
|
start: cells[spanStart].date,
|
|
end: addDays(cells[spanEnd - 1].date, 1),
|
|
});
|
|
return Object.assign(Object.assign({}, seg), { firstCol: spanStart, lastCol: spanEnd - 1, eventRange: {
|
|
def: eventRange.def,
|
|
ui: Object.assign(Object.assign({}, eventRange.ui), { durationEditable: false }),
|
|
instance: eventRange.instance,
|
|
range: slicedRange,
|
|
}, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() });
|
|
}
|
|
class DayGridSegHierarchy extends SegHierarchy {
|
|
constructor() {
|
|
super(...arguments);
|
|
// config
|
|
this.hiddenConsumes = false;
|
|
// allows us to keep hidden entries in the hierarchy so they take up space
|
|
this.forceHidden = {};
|
|
}
|
|
addSegs(segInputs) {
|
|
const hiddenSegs = super.addSegs(segInputs);
|
|
const { entriesByLevel } = this;
|
|
const excludeHidden = (entry) => !this.forceHidden[buildEntryKey(entry)];
|
|
// remove the forced-hidden segs
|
|
for (let level = 0; level < entriesByLevel.length; level += 1) {
|
|
entriesByLevel[level] = entriesByLevel[level].filter(excludeHidden);
|
|
}
|
|
return hiddenSegs;
|
|
}
|
|
handleInvalidInsertion(insertion, entry, hiddenEntries) {
|
|
const { entriesByLevel, forceHidden } = this;
|
|
const { touchingEntry, touchingLevel, touchingLateral } = insertion;
|
|
// the entry that the new insertion is touching must be hidden
|
|
if (this.hiddenConsumes && touchingEntry) {
|
|
const touchingEntryId = buildEntryKey(touchingEntry);
|
|
if (!forceHidden[touchingEntryId]) {
|
|
if (this.allowReslicing) {
|
|
// split up the touchingEntry, reinsert it
|
|
const hiddenEntry = Object.assign(Object.assign({}, touchingEntry), { span: intersectSpans(touchingEntry.span, entry.span) });
|
|
// reinsert the area that turned into a "more" link (so no other entries try to
|
|
// occupy the space) but mark it forced-hidden
|
|
const hiddenEntryId = buildEntryKey(hiddenEntry);
|
|
forceHidden[hiddenEntryId] = true;
|
|
entriesByLevel[touchingLevel][touchingLateral] = hiddenEntry;
|
|
hiddenEntries.push(hiddenEntry);
|
|
this.splitEntry(touchingEntry, entry, hiddenEntries);
|
|
}
|
|
else {
|
|
forceHidden[touchingEntryId] = true;
|
|
hiddenEntries.push(touchingEntry);
|
|
}
|
|
}
|
|
}
|
|
// will try to reslice...
|
|
super.handleInvalidInsertion(insertion, entry, hiddenEntries);
|
|
}
|
|
}
|
|
|
|
class TableRow extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.cellElRefs = new RefMap(); // the <td>
|
|
this.frameElRefs = new RefMap(); // the fc-daygrid-day-frame
|
|
this.fgElRefs = new RefMap(); // the fc-daygrid-day-events
|
|
this.segHarnessRefs = new RefMap(); // indexed by "instanceId:firstCol"
|
|
this.rootElRef = d();
|
|
this.state = {
|
|
framePositions: null,
|
|
maxContentHeight: null,
|
|
segHeights: {},
|
|
};
|
|
this.handleResize = (isForced) => {
|
|
if (isForced) {
|
|
this.updateSizing(true); // isExternal=true
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state, context } = this;
|
|
let { options } = context;
|
|
let colCnt = props.cells.length;
|
|
let businessHoursByCol = splitSegsByFirstCol(props.businessHourSegs, colCnt);
|
|
let bgEventSegsByCol = splitSegsByFirstCol(props.bgEventSegs, colCnt);
|
|
let highlightSegsByCol = splitSegsByFirstCol(this.getHighlightSegs(), colCnt);
|
|
let mirrorSegsByCol = splitSegsByFirstCol(this.getMirrorSegs(), colCnt);
|
|
let { singleColPlacements, multiColPlacements, moreCnts, moreMarginTops } = computeFgSegPlacement(sortEventSegs(props.fgEventSegs, options.eventOrder), props.dayMaxEvents, props.dayMaxEventRows, options.eventOrderStrict, state.segHeights, state.maxContentHeight, props.cells);
|
|
let isForcedInvisible = // TODO: messy way to compute this
|
|
(props.eventDrag && props.eventDrag.affectedInstances) ||
|
|
(props.eventResize && props.eventResize.affectedInstances) ||
|
|
{};
|
|
return (y("tr", { ref: this.rootElRef, role: "row" },
|
|
props.renderIntro && props.renderIntro(),
|
|
props.cells.map((cell, col) => {
|
|
let normalFgNodes = this.renderFgSegs(col, props.forPrint ? singleColPlacements[col] : multiColPlacements[col], props.todayRange, isForcedInvisible);
|
|
let mirrorFgNodes = this.renderFgSegs(col, buildMirrorPlacements(mirrorSegsByCol[col], multiColPlacements), props.todayRange, {}, Boolean(props.eventDrag), Boolean(props.eventResize), false);
|
|
return (y(TableCell, { key: cell.key, elRef: this.cellElRefs.createRef(cell.key), innerElRef: this.frameElRefs.createRef(cell.key) /* FF <td> problem, but okay to use for left/right. TODO: rename prop */, dateProfile: props.dateProfile, date: cell.date, showDayNumber: props.showDayNumbers, showWeekNumber: props.showWeekNumbers && col === 0, forceDayTop: props.showWeekNumbers /* even displaying weeknum for row, not necessarily day */, todayRange: props.todayRange, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, extraRenderProps: cell.extraRenderProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, extraDateSpan: cell.extraDateSpan, moreCnt: moreCnts[col], moreMarginTop: moreMarginTops[col], singlePlacements: singleColPlacements[col], fgContentElRef: this.fgElRefs.createRef(cell.key), fgContent: ( // Fragment scopes the keys
|
|
y(_, null,
|
|
y(_, null, normalFgNodes),
|
|
y(_, null, mirrorFgNodes))), bgContent: ( // Fragment scopes the keys
|
|
y(_, null,
|
|
this.renderFillSegs(highlightSegsByCol[col], 'highlight'),
|
|
this.renderFillSegs(businessHoursByCol[col], 'non-business'),
|
|
this.renderFillSegs(bgEventSegsByCol[col], 'bg-event'))), minHeight: props.cellMinHeight }));
|
|
})));
|
|
}
|
|
componentDidMount() {
|
|
this.updateSizing(true);
|
|
this.context.addResizeHandler(this.handleResize);
|
|
}
|
|
componentDidUpdate(prevProps, prevState) {
|
|
let currentProps = this.props;
|
|
this.updateSizing(!isPropsEqual(prevProps, currentProps));
|
|
}
|
|
componentWillUnmount() {
|
|
this.context.removeResizeHandler(this.handleResize);
|
|
}
|
|
getHighlightSegs() {
|
|
let { props } = this;
|
|
if (props.eventDrag && props.eventDrag.segs.length) { // messy check
|
|
return props.eventDrag.segs;
|
|
}
|
|
if (props.eventResize && props.eventResize.segs.length) { // messy check
|
|
return props.eventResize.segs;
|
|
}
|
|
return props.dateSelectionSegs;
|
|
}
|
|
getMirrorSegs() {
|
|
let { props } = this;
|
|
if (props.eventResize && props.eventResize.segs.length) { // messy check
|
|
return props.eventResize.segs;
|
|
}
|
|
return [];
|
|
}
|
|
renderFgSegs(col, segPlacements, todayRange, isForcedInvisible, isDragging, isResizing, isDateSelecting) {
|
|
let { context } = this;
|
|
let { eventSelection } = this.props;
|
|
let { framePositions } = this.state;
|
|
let defaultDisplayEventEnd = this.props.cells.length === 1; // colCnt === 1
|
|
let isMirror = isDragging || isResizing || isDateSelecting;
|
|
let nodes = [];
|
|
if (framePositions) {
|
|
for (let placement of segPlacements) {
|
|
let { seg } = placement;
|
|
let { instanceId } = seg.eventRange.instance;
|
|
let isVisible = placement.isVisible && !isForcedInvisible[instanceId];
|
|
let isAbsolute = placement.isAbsolute;
|
|
let left = '';
|
|
let right = '';
|
|
if (isAbsolute) {
|
|
if (context.isRtl) {
|
|
right = 0;
|
|
left = framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol];
|
|
}
|
|
else {
|
|
left = 0;
|
|
right = framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol];
|
|
}
|
|
}
|
|
/*
|
|
known bug: events that are force to be list-item but span multiple days still take up space in later columns
|
|
todo: in print view, for multi-day events, don't display title within non-start/end segs
|
|
*/
|
|
nodes.push(y("div", { className: 'fc-daygrid-event-harness' + (isAbsolute ? ' fc-daygrid-event-harness-abs' : ''), key: generateSegKey(seg), ref: isMirror ? null : this.segHarnessRefs.createRef(generateSegUid(seg)), style: {
|
|
visibility: isVisible ? '' : 'hidden',
|
|
marginTop: isAbsolute ? '' : placement.marginTop,
|
|
top: isAbsolute ? placement.absoluteTop : '',
|
|
left,
|
|
right,
|
|
} }, hasListItemDisplay(seg) ? (y(TableListItemEvent, Object.assign({ seg: seg, isDragging: isDragging, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange)))) : (y(TableBlockEvent, Object.assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, defaultDisplayEventEnd: defaultDisplayEventEnd }, getSegMeta(seg, todayRange))))));
|
|
}
|
|
}
|
|
return nodes;
|
|
}
|
|
renderFillSegs(segs, fillType) {
|
|
let { isRtl } = this.context;
|
|
let { todayRange } = this.props;
|
|
let { framePositions } = this.state;
|
|
let nodes = [];
|
|
if (framePositions) {
|
|
for (let seg of segs) {
|
|
let leftRightCss = isRtl ? {
|
|
right: 0,
|
|
left: framePositions.lefts[seg.lastCol] - framePositions.lefts[seg.firstCol],
|
|
} : {
|
|
left: 0,
|
|
right: framePositions.rights[seg.firstCol] - framePositions.rights[seg.lastCol],
|
|
};
|
|
nodes.push(y("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-daygrid-bg-harness", style: leftRightCss }, fillType === 'bg-event' ?
|
|
y(BgEvent, Object.assign({ seg: seg }, getSegMeta(seg, todayRange))) :
|
|
renderFill(fillType)));
|
|
}
|
|
}
|
|
return y(_, {}, ...nodes);
|
|
}
|
|
updateSizing(isExternalSizingChange) {
|
|
let { props, state, frameElRefs } = this;
|
|
if (!props.forPrint &&
|
|
props.clientWidth !== null // positioning ready?
|
|
) {
|
|
if (isExternalSizingChange) {
|
|
let frameEls = props.cells.map((cell) => frameElRefs.currentMap[cell.key]);
|
|
if (frameEls.length) {
|
|
let originEl = this.rootElRef.current;
|
|
let newPositionCache = new PositionCache(originEl, frameEls, true, // isHorizontal
|
|
false);
|
|
if (!state.framePositions || !state.framePositions.similarTo(newPositionCache)) {
|
|
this.setState({
|
|
framePositions: new PositionCache(originEl, frameEls, true, // isHorizontal
|
|
false),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const oldSegHeights = this.state.segHeights;
|
|
const newSegHeights = this.querySegHeights();
|
|
const limitByContentHeight = props.dayMaxEvents === true || props.dayMaxEventRows === true;
|
|
this.safeSetState({
|
|
// HACK to prevent oscillations of events being shown/hidden from max-event-rows
|
|
// Essentially, once you compute an element's height, never null-out.
|
|
// TODO: always display all events, as visibility:hidden?
|
|
segHeights: Object.assign(Object.assign({}, oldSegHeights), newSegHeights),
|
|
maxContentHeight: limitByContentHeight ? this.computeMaxContentHeight() : null,
|
|
});
|
|
}
|
|
}
|
|
querySegHeights() {
|
|
let segElMap = this.segHarnessRefs.currentMap;
|
|
let segHeights = {};
|
|
// get the max height amongst instance segs
|
|
for (let segUid in segElMap) {
|
|
let height = Math.round(segElMap[segUid].getBoundingClientRect().height);
|
|
segHeights[segUid] = Math.max(segHeights[segUid] || 0, height);
|
|
}
|
|
return segHeights;
|
|
}
|
|
computeMaxContentHeight() {
|
|
let firstKey = this.props.cells[0].key;
|
|
let cellEl = this.cellElRefs.currentMap[firstKey];
|
|
let fcContainerEl = this.fgElRefs.currentMap[firstKey];
|
|
return cellEl.getBoundingClientRect().bottom - fcContainerEl.getBoundingClientRect().top;
|
|
}
|
|
getCellEls() {
|
|
let elMap = this.cellElRefs.currentMap;
|
|
return this.props.cells.map((cell) => elMap[cell.key]);
|
|
}
|
|
}
|
|
TableRow.addStateEquality({
|
|
segHeights: isPropsEqual,
|
|
});
|
|
function buildMirrorPlacements(mirrorSegs, colPlacements) {
|
|
if (!mirrorSegs.length) {
|
|
return [];
|
|
}
|
|
let topsByInstanceId = buildAbsoluteTopHash(colPlacements); // TODO: cache this at first render?
|
|
return mirrorSegs.map((seg) => ({
|
|
seg,
|
|
isVisible: true,
|
|
isAbsolute: true,
|
|
absoluteTop: topsByInstanceId[seg.eventRange.instance.instanceId],
|
|
marginTop: 0,
|
|
}));
|
|
}
|
|
function buildAbsoluteTopHash(colPlacements) {
|
|
let topsByInstanceId = {};
|
|
for (let placements of colPlacements) {
|
|
for (let placement of placements) {
|
|
topsByInstanceId[placement.seg.eventRange.instance.instanceId] = placement.absoluteTop;
|
|
}
|
|
}
|
|
return topsByInstanceId;
|
|
}
|
|
|
|
class TableRows extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.splitBusinessHourSegs = memoize(splitSegsByRow);
|
|
this.splitBgEventSegs = memoize(splitAllDaySegsByRow);
|
|
this.splitFgEventSegs = memoize(splitSegsByRow);
|
|
this.splitDateSelectionSegs = memoize(splitSegsByRow);
|
|
this.splitEventDrag = memoize(splitInteractionByRow);
|
|
this.splitEventResize = memoize(splitInteractionByRow);
|
|
this.rowRefs = new RefMap();
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
let rowCnt = props.cells.length;
|
|
let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, rowCnt);
|
|
let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, rowCnt);
|
|
let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, rowCnt);
|
|
let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, rowCnt);
|
|
let eventDragByRow = this.splitEventDrag(props.eventDrag, rowCnt);
|
|
let eventResizeByRow = this.splitEventResize(props.eventResize, rowCnt);
|
|
// for DayGrid view with many rows, force a min-height on cells so doesn't appear squished
|
|
// choose 7 because a month view will have max 6 rows
|
|
let cellMinHeight = (rowCnt >= 7 && props.clientWidth) ?
|
|
props.clientWidth / context.options.aspectRatio / 6 :
|
|
null;
|
|
return (y(NowTimer, { unit: "day" }, (nowDate, todayRange) => (y(_, null, props.cells.map((cells, row) => (y(TableRow, { ref: this.rowRefs.createRef(row), key: cells.length
|
|
? cells[0].date.toISOString() /* best? or put key on cell? or use diff formatter? */
|
|
: row // in case there are no cells (like when resource view is loading)
|
|
, showDayNumbers: rowCnt > 1, showWeekNumbers: props.showWeekNumbers, todayRange: todayRange, dateProfile: props.dateProfile, cells: cells, renderIntro: props.renderRowIntro, businessHourSegs: businessHourSegsByRow[row], eventSelection: props.eventSelection, bgEventSegs: bgEventSegsByRow[row], fgEventSegs: fgEventSegsByRow[row], dateSelectionSegs: dateSelectionSegsByRow[row], eventDrag: eventDragByRow[row], eventResize: eventResizeByRow[row], dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows, clientWidth: props.clientWidth, clientHeight: props.clientHeight, cellMinHeight: cellMinHeight, forPrint: props.forPrint })))))));
|
|
}
|
|
componentDidMount() {
|
|
this.registerInteractiveComponent();
|
|
}
|
|
componentDidUpdate() {
|
|
// for if started with zero cells
|
|
this.registerInteractiveComponent();
|
|
}
|
|
registerInteractiveComponent() {
|
|
if (!this.rootEl) {
|
|
// HACK: need a daygrid wrapper parent to do positioning
|
|
// NOTE: a daygrid resource view w/o resources can have zero cells
|
|
const firstCellEl = this.rowRefs.currentMap[0].getCellEls()[0];
|
|
const rootEl = firstCellEl ? firstCellEl.closest('.fc-daygrid-body') : null;
|
|
if (rootEl) {
|
|
this.rootEl = rootEl;
|
|
this.context.registerInteractiveComponent(this, {
|
|
el: rootEl,
|
|
isHitComboAllowed: this.props.isHitComboAllowed,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
componentWillUnmount() {
|
|
if (this.rootEl) {
|
|
this.context.unregisterInteractiveComponent(this);
|
|
this.rootEl = null;
|
|
}
|
|
}
|
|
// Hit System
|
|
// ----------------------------------------------------------------------------------------------------
|
|
prepareHits() {
|
|
this.rowPositions = new PositionCache(this.rootEl, this.rowRefs.collect().map((rowObj) => rowObj.getCellEls()[0]), // first cell el in each row. TODO: not optimal
|
|
false, true);
|
|
this.colPositions = new PositionCache(this.rootEl, this.rowRefs.currentMap[0].getCellEls(), // cell els in first row
|
|
true, // horizontal
|
|
false);
|
|
}
|
|
queryHit(positionLeft, positionTop) {
|
|
let { colPositions, rowPositions } = this;
|
|
let col = colPositions.leftToIndex(positionLeft);
|
|
let row = rowPositions.topToIndex(positionTop);
|
|
if (row != null && col != null) {
|
|
let cell = this.props.cells[row][col];
|
|
return {
|
|
dateProfile: this.props.dateProfile,
|
|
dateSpan: Object.assign({ range: this.getCellRange(row, col), allDay: true }, cell.extraDateSpan),
|
|
dayEl: this.getCellEl(row, col),
|
|
rect: {
|
|
left: colPositions.lefts[col],
|
|
right: colPositions.rights[col],
|
|
top: rowPositions.tops[row],
|
|
bottom: rowPositions.bottoms[row],
|
|
},
|
|
layer: 0,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
getCellEl(row, col) {
|
|
return this.rowRefs.currentMap[row].getCellEls()[col]; // TODO: not optimal
|
|
}
|
|
getCellRange(row, col) {
|
|
let start = this.props.cells[row][col].date;
|
|
let end = addDays(start, 1);
|
|
return { start, end };
|
|
}
|
|
}
|
|
function splitAllDaySegsByRow(segs, rowCnt) {
|
|
return splitSegsByRow(segs.filter(isSegAllDay), rowCnt);
|
|
}
|
|
function isSegAllDay(seg) {
|
|
return seg.eventRange.def.allDay;
|
|
}
|
|
|
|
class Table extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.elRef = d();
|
|
this.needsScrollReset = false;
|
|
}
|
|
render() {
|
|
let { props } = this;
|
|
let { dayMaxEventRows, dayMaxEvents, expandRows } = props;
|
|
let limitViaBalanced = dayMaxEvents === true || dayMaxEventRows === true;
|
|
// if rows can't expand to fill fixed height, can't do balanced-height event limit
|
|
// TODO: best place to normalize these options?
|
|
if (limitViaBalanced && !expandRows) {
|
|
limitViaBalanced = false;
|
|
dayMaxEventRows = null;
|
|
dayMaxEvents = null;
|
|
}
|
|
let classNames = [
|
|
'fc-daygrid-body',
|
|
limitViaBalanced ? 'fc-daygrid-body-balanced' : 'fc-daygrid-body-unbalanced',
|
|
expandRows ? '' : 'fc-daygrid-body-natural', // will height of one row depend on the others?
|
|
];
|
|
return (y("div", { ref: this.elRef, className: classNames.join(' '), style: {
|
|
// these props are important to give this wrapper correct dimensions for interactions
|
|
// TODO: if we set it here, can we avoid giving to inner tables?
|
|
width: props.clientWidth,
|
|
minWidth: props.tableMinWidth,
|
|
} },
|
|
y("table", { role: "presentation", className: "fc-scrollgrid-sync-table", style: {
|
|
width: props.clientWidth,
|
|
minWidth: props.tableMinWidth,
|
|
height: expandRows ? props.clientHeight : '',
|
|
} },
|
|
props.colGroupNode,
|
|
y("tbody", { role: "presentation" },
|
|
y(TableRows, { dateProfile: props.dateProfile, cells: props.cells, renderRowIntro: props.renderRowIntro, showWeekNumbers: props.showWeekNumbers, clientWidth: props.clientWidth, clientHeight: props.clientHeight, businessHourSegs: props.businessHourSegs, bgEventSegs: props.bgEventSegs, fgEventSegs: props.fgEventSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, dayMaxEvents: dayMaxEvents, dayMaxEventRows: dayMaxEventRows, forPrint: props.forPrint, isHitComboAllowed: props.isHitComboAllowed })))));
|
|
}
|
|
componentDidMount() {
|
|
this.requestScrollReset();
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
if (prevProps.dateProfile !== this.props.dateProfile) {
|
|
this.requestScrollReset();
|
|
}
|
|
else {
|
|
this.flushScrollReset();
|
|
}
|
|
}
|
|
requestScrollReset() {
|
|
this.needsScrollReset = true;
|
|
this.flushScrollReset();
|
|
}
|
|
flushScrollReset() {
|
|
if (this.needsScrollReset &&
|
|
this.props.clientWidth // sizes computed?
|
|
) {
|
|
const subjectEl = getScrollSubjectEl(this.elRef.current, this.props.dateProfile);
|
|
if (subjectEl) {
|
|
const originEl = subjectEl.closest('.fc-daygrid-body');
|
|
const scrollEl = originEl.closest('.fc-scroller');
|
|
const scrollTop = subjectEl.getBoundingClientRect().top -
|
|
originEl.getBoundingClientRect().top;
|
|
scrollEl.scrollTop = scrollTop ? (scrollTop + 1) : 0; // overcome border
|
|
}
|
|
this.needsScrollReset = false;
|
|
}
|
|
}
|
|
}
|
|
function getScrollSubjectEl(containerEl, dateProfile) {
|
|
let el;
|
|
if (dateProfile.currentRangeUnit.match(/year|month/)) {
|
|
el = containerEl.querySelector(`[data-date="${formatIsoMonthStr(dateProfile.currentDate)}-01"]`);
|
|
// even if view is month-based, first-of-month might be hidden...
|
|
}
|
|
if (!el) {
|
|
el = containerEl.querySelector(`[data-date="${formatDayString(dateProfile.currentDate)}"]`);
|
|
// could still be hidden if an interior-view hidden day
|
|
}
|
|
return el;
|
|
}
|
|
|
|
class DayTableSlicer extends Slicer {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.forceDayIfListItem = true;
|
|
}
|
|
sliceRange(dateRange, dayTableModel) {
|
|
return dayTableModel.sliceRange(dateRange);
|
|
}
|
|
}
|
|
|
|
class DayTable extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.slicer = new DayTableSlicer();
|
|
this.tableRef = d();
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
return (y(Table, Object.assign({ ref: this.tableRef }, this.slicer.sliceProps(props, props.dateProfile, props.nextDayThreshold, context, props.dayTableModel), { dateProfile: props.dateProfile, cells: props.dayTableModel.cells, colGroupNode: props.colGroupNode, tableMinWidth: props.tableMinWidth, renderRowIntro: props.renderRowIntro, dayMaxEvents: props.dayMaxEvents, dayMaxEventRows: props.dayMaxEventRows, showWeekNumbers: props.showWeekNumbers, expandRows: props.expandRows, headerAlignElRef: props.headerAlignElRef, clientWidth: props.clientWidth, clientHeight: props.clientHeight, forPrint: props.forPrint })));
|
|
}
|
|
}
|
|
|
|
class DayTableView extends TableView {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buildDayTableModel = memoize(buildDayTableModel);
|
|
this.headerRef = d();
|
|
this.tableRef = d();
|
|
// can't override any lifecycle methods from parent
|
|
}
|
|
render() {
|
|
let { options, dateProfileGenerator } = this.context;
|
|
let { props } = this;
|
|
let dayTableModel = this.buildDayTableModel(props.dateProfile, dateProfileGenerator);
|
|
let headerContent = options.dayHeaders && (y(DayHeader, { ref: this.headerRef, dateProfile: props.dateProfile, dates: dayTableModel.headerDates, datesRepDistinctDays: dayTableModel.rowCnt === 1 }));
|
|
let bodyContent = (contentArg) => (y(DayTable, { ref: this.tableRef, dateProfile: props.dateProfile, dayTableModel: dayTableModel, businessHours: props.businessHours, dateSelection: props.dateSelection, eventStore: props.eventStore, eventUiBases: props.eventUiBases, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, nextDayThreshold: options.nextDayThreshold, colGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, dayMaxEvents: options.dayMaxEvents, dayMaxEventRows: options.dayMaxEventRows, showWeekNumbers: options.weekNumbers, expandRows: !props.isHeightAuto, headerAlignElRef: this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint }));
|
|
return options.dayMinWidth
|
|
? this.renderHScrollLayout(headerContent, bodyContent, dayTableModel.colCnt, options.dayMinWidth)
|
|
: this.renderSimpleLayout(headerContent, bodyContent);
|
|
}
|
|
}
|
|
function buildDayTableModel(dateProfile, dateProfileGenerator) {
|
|
let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
|
|
return new DayTableModel(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit));
|
|
}
|
|
|
|
class TableDateProfileGenerator extends DateProfileGenerator {
|
|
// Computes the date range that will be rendered
|
|
buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay) {
|
|
let renderRange = super.buildRenderRange(currentRange, currentRangeUnit, isRangeAllDay);
|
|
let { props } = this;
|
|
return buildDayTableRenderRange({
|
|
currentRange: renderRange,
|
|
snapToWeek: /^(year|month)$/.test(currentRangeUnit),
|
|
fixedWeekCount: props.fixedWeekCount,
|
|
dateEnv: props.dateEnv,
|
|
});
|
|
}
|
|
}
|
|
function buildDayTableRenderRange(props) {
|
|
let { dateEnv, currentRange } = props;
|
|
let { start, end } = currentRange;
|
|
let endOfWeek;
|
|
// year and month views should be aligned with weeks. this is already done for week
|
|
if (props.snapToWeek) {
|
|
start = dateEnv.startOfWeek(start);
|
|
// make end-of-week if not already
|
|
endOfWeek = dateEnv.startOfWeek(end);
|
|
if (endOfWeek.valueOf() !== end.valueOf()) {
|
|
end = addWeeks(endOfWeek, 1);
|
|
}
|
|
}
|
|
// ensure 6 weeks
|
|
if (props.fixedWeekCount) {
|
|
// TODO: instead of these date-math gymnastics (for multimonth view),
|
|
// compute dateprofiles of all months, then use start of first and end of last.
|
|
let lastMonthRenderStart = dateEnv.startOfWeek(dateEnv.startOfMonth(addDays(currentRange.end, -1)));
|
|
let rowCnt = Math.ceil(// could be partial weeks due to hiddenDays
|
|
diffWeeks(lastMonthRenderStart, end));
|
|
end = addWeeks(end, 6 - rowCnt);
|
|
}
|
|
return { start, end };
|
|
}
|
|
|
|
var internal_css_248z = ":root{--fc-daygrid-event-dot-width:8px}.fc-daygrid-day-events:after,.fc-daygrid-day-events:before,.fc-daygrid-day-frame:after,.fc-daygrid-day-frame:before,.fc-daygrid-event-harness:after,.fc-daygrid-event-harness:before{clear:both;content:\"\";display:table}.fc .fc-daygrid-body{position:relative;z-index:1}.fc .fc-daygrid-day.fc-day-today{background-color:var(--fc-today-bg-color)}.fc .fc-daygrid-day-frame{min-height:100%;position:relative}.fc .fc-daygrid-day-top{display:flex;flex-direction:row-reverse}.fc .fc-day-other .fc-daygrid-day-top{opacity:.3}.fc .fc-daygrid-day-number{padding:4px;position:relative;z-index:4}.fc .fc-daygrid-month-start{font-size:1.1em;font-weight:700}.fc .fc-daygrid-day-events{margin-top:1px}.fc .fc-daygrid-body-balanced .fc-daygrid-day-events{left:0;position:absolute;right:0}.fc .fc-daygrid-body-unbalanced .fc-daygrid-day-events{min-height:2em;position:relative}.fc .fc-daygrid-body-natural .fc-daygrid-day-events{margin-bottom:1em}.fc .fc-daygrid-event-harness{position:relative}.fc .fc-daygrid-event-harness-abs{left:0;position:absolute;right:0;top:0}.fc .fc-daygrid-bg-harness{bottom:0;position:absolute;top:0}.fc .fc-daygrid-day-bg .fc-non-business{z-index:1}.fc .fc-daygrid-day-bg .fc-bg-event{z-index:2}.fc .fc-daygrid-day-bg .fc-highlight{z-index:3}.fc .fc-daygrid-event{margin-top:1px;z-index:6}.fc .fc-daygrid-event.fc-event-mirror{z-index:7}.fc .fc-daygrid-day-bottom{font-size:.85em;margin:0 2px}.fc .fc-daygrid-day-bottom:after,.fc .fc-daygrid-day-bottom:before{clear:both;content:\"\";display:table}.fc .fc-daygrid-more-link{border-radius:3px;cursor:pointer;line-height:1;margin-top:1px;max-width:100%;overflow:hidden;padding:2px;position:relative;white-space:nowrap;z-index:4}.fc .fc-daygrid-more-link:hover{background-color:rgba(0,0,0,.1)}.fc .fc-daygrid-week-number{background-color:var(--fc-neutral-bg-color);color:var(--fc-neutral-text-color);min-width:1.5em;padding:2px;position:absolute;text-align:center;top:0;z-index:5}.fc .fc-more-popover .fc-popover-body{min-width:220px;padding:10px}.fc-direction-ltr .fc-daygrid-event.fc-event-start,.fc-direction-rtl .fc-daygrid-event.fc-event-end{margin-left:2px}.fc-direction-ltr .fc-daygrid-event.fc-event-end,.fc-direction-rtl .fc-daygrid-event.fc-event-start{margin-right:2px}.fc-direction-ltr .fc-daygrid-more-link{float:left}.fc-direction-ltr .fc-daygrid-week-number{border-radius:0 0 3px 0;left:0}.fc-direction-rtl .fc-daygrid-more-link{float:right}.fc-direction-rtl .fc-daygrid-week-number{border-radius:0 0 0 3px;right:0}.fc-liquid-hack .fc-daygrid-day-frame{position:static}.fc-daygrid-event{border-radius:3px;font-size:var(--fc-small-font-size);position:relative;white-space:nowrap}.fc-daygrid-block-event .fc-event-time{font-weight:700}.fc-daygrid-block-event .fc-event-time,.fc-daygrid-block-event .fc-event-title{padding:1px}.fc-daygrid-dot-event{align-items:center;display:flex;padding:2px 0}.fc-daygrid-dot-event .fc-event-title{flex-grow:1;flex-shrink:1;font-weight:700;min-width:0;overflow:hidden}.fc-daygrid-dot-event.fc-event-mirror,.fc-daygrid-dot-event:hover{background:rgba(0,0,0,.1)}.fc-daygrid-dot-event.fc-event-selected:before{bottom:-10px;top:-10px}.fc-daygrid-event-dot{border:calc(var(--fc-daygrid-event-dot-width)/2) solid var(--fc-event-border-color);border-radius:calc(var(--fc-daygrid-event-dot-width)/2);box-sizing:content-box;height:0;margin:0 4px;width:0}.fc-direction-ltr .fc-daygrid-event .fc-event-time{margin-right:3px}.fc-direction-rtl .fc-daygrid-event .fc-event-time{margin-left:3px}";
|
|
injectStyles(internal_css_248z);
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/daygrid/index.js
|
|
|
|
|
|
|
|
|
|
|
|
var daygrid_index = createPlugin({
|
|
name: '@fullcalendar/daygrid',
|
|
initialView: 'dayGridMonth',
|
|
views: {
|
|
dayGrid: {
|
|
component: DayTableView,
|
|
dateProfileGeneratorClass: TableDateProfileGenerator,
|
|
},
|
|
dayGridDay: {
|
|
type: 'dayGrid',
|
|
duration: { days: 1 },
|
|
},
|
|
dayGridWeek: {
|
|
type: 'dayGrid',
|
|
duration: { weeks: 1 },
|
|
},
|
|
dayGridMonth: {
|
|
type: 'dayGrid',
|
|
duration: { months: 1 },
|
|
fixedWeekCount: true,
|
|
},
|
|
dayGridYear: {
|
|
type: 'dayGrid',
|
|
duration: { years: 1 },
|
|
},
|
|
},
|
|
});
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/timegrid/internal.js
|
|
|
|
|
|
|
|
|
|
class AllDaySplitter extends Splitter {
|
|
getKeyInfo() {
|
|
return {
|
|
allDay: {},
|
|
timed: {},
|
|
};
|
|
}
|
|
getKeysForDateSpan(dateSpan) {
|
|
if (dateSpan.allDay) {
|
|
return ['allDay'];
|
|
}
|
|
return ['timed'];
|
|
}
|
|
getKeysForEventDef(eventDef) {
|
|
if (!eventDef.allDay) {
|
|
return ['timed'];
|
|
}
|
|
if (hasBgRendering(eventDef)) {
|
|
return ['timed', 'allDay'];
|
|
}
|
|
return ['allDay'];
|
|
}
|
|
}
|
|
|
|
const DEFAULT_SLAT_LABEL_FORMAT = createFormatter({
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
omitZeroMinute: true,
|
|
meridiem: 'short',
|
|
});
|
|
function TimeColsAxisCell(props) {
|
|
let classNames = [
|
|
'fc-timegrid-slot',
|
|
'fc-timegrid-slot-label',
|
|
props.isLabeled ? 'fc-scrollgrid-shrink' : 'fc-timegrid-slot-minor',
|
|
];
|
|
return (y(ViewContextType.Consumer, null, (context) => {
|
|
if (!props.isLabeled) {
|
|
return (y("td", { className: classNames.join(' '), "data-time": props.isoTimeStr }));
|
|
}
|
|
let { dateEnv, options, viewApi } = context;
|
|
let labelFormat = // TODO: fully pre-parse
|
|
options.slotLabelFormat == null ? DEFAULT_SLAT_LABEL_FORMAT :
|
|
Array.isArray(options.slotLabelFormat) ? createFormatter(options.slotLabelFormat[0]) :
|
|
createFormatter(options.slotLabelFormat);
|
|
let renderProps = {
|
|
level: 0,
|
|
time: props.time,
|
|
date: dateEnv.toDate(props.date),
|
|
view: viewApi,
|
|
text: dateEnv.format(props.date, labelFormat),
|
|
};
|
|
return (y(ContentContainer, { elTag: "td", elClasses: classNames, elAttrs: {
|
|
'data-time': props.isoTimeStr,
|
|
}, renderProps: renderProps, generatorName: "slotLabelContent", customGenerator: options.slotLabelContent, defaultGenerator: timegrid_internal_renderInnerContent, classNameGenerator: options.slotLabelClassNames, didMount: options.slotLabelDidMount, willUnmount: options.slotLabelWillUnmount }, (InnerContent) => (y("div", { className: "fc-timegrid-slot-label-frame fc-scrollgrid-shrink-frame" },
|
|
y(InnerContent, { elTag: "div", elClasses: [
|
|
'fc-timegrid-slot-label-cushion',
|
|
'fc-scrollgrid-shrink-cushion',
|
|
] })))));
|
|
}));
|
|
}
|
|
function timegrid_internal_renderInnerContent(props) {
|
|
return props.text;
|
|
}
|
|
|
|
class TimeBodyAxis extends BaseComponent {
|
|
render() {
|
|
return this.props.slatMetas.map((slatMeta) => (y("tr", { key: slatMeta.key },
|
|
y(TimeColsAxisCell, Object.assign({}, slatMeta)))));
|
|
}
|
|
}
|
|
|
|
const internal_DEFAULT_WEEK_NUM_FORMAT = createFormatter({ week: 'short' });
|
|
const AUTO_ALL_DAY_MAX_EVENT_ROWS = 5;
|
|
class TimeColsView extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.allDaySplitter = new AllDaySplitter(); // for use by subclasses
|
|
this.headerElRef = d();
|
|
this.rootElRef = d();
|
|
this.scrollerElRef = d();
|
|
this.state = {
|
|
slatCoords: null,
|
|
};
|
|
this.handleScrollTopRequest = (scrollTop) => {
|
|
let scrollerEl = this.scrollerElRef.current;
|
|
if (scrollerEl) { // TODO: not sure how this could ever be null. weirdness with the reducer
|
|
scrollerEl.scrollTop = scrollTop;
|
|
}
|
|
};
|
|
/* Header Render Methods
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
this.renderHeadAxis = (rowKey, frameHeight = '') => {
|
|
let { options } = this.context;
|
|
let { dateProfile } = this.props;
|
|
let range = dateProfile.renderRange;
|
|
let dayCnt = diffDays(range.start, range.end);
|
|
// only do in day views (to avoid doing in week views that dont need it)
|
|
let navLinkAttrs = (dayCnt === 1)
|
|
? buildNavLinkAttrs(this.context, range.start, 'week')
|
|
: {};
|
|
if (options.weekNumbers && rowKey === 'day') {
|
|
return (y(WeekNumberContainer, { elTag: "th", elClasses: [
|
|
'fc-timegrid-axis',
|
|
'fc-scrollgrid-shrink',
|
|
], elAttrs: {
|
|
'aria-hidden': true,
|
|
}, date: range.start, defaultFormat: internal_DEFAULT_WEEK_NUM_FORMAT }, (InnerContent) => (y("div", { className: [
|
|
'fc-timegrid-axis-frame',
|
|
'fc-scrollgrid-shrink-frame',
|
|
'fc-timegrid-axis-frame-liquid',
|
|
].join(' '), style: { height: frameHeight } },
|
|
y(InnerContent, { elTag: "a", elClasses: [
|
|
'fc-timegrid-axis-cushion',
|
|
'fc-scrollgrid-shrink-cushion',
|
|
'fc-scrollgrid-sync-inner',
|
|
], elAttrs: navLinkAttrs })))));
|
|
}
|
|
return (y("th", { "aria-hidden": true, className: "fc-timegrid-axis" },
|
|
y("div", { className: "fc-timegrid-axis-frame", style: { height: frameHeight } })));
|
|
};
|
|
/* Table Component Render Methods
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
// only a one-way height sync. we don't send the axis inner-content height to the DayGrid,
|
|
// but DayGrid still needs to have classNames on inner elements in order to measure.
|
|
this.renderTableRowAxis = (rowHeight) => {
|
|
let { options, viewApi } = this.context;
|
|
let renderProps = {
|
|
text: options.allDayText,
|
|
view: viewApi,
|
|
};
|
|
return (
|
|
// TODO: make reusable hook. used in list view too
|
|
y(ContentContainer, { elTag: "td", elClasses: [
|
|
'fc-timegrid-axis',
|
|
'fc-scrollgrid-shrink',
|
|
], elAttrs: {
|
|
'aria-hidden': true,
|
|
}, renderProps: renderProps, generatorName: "allDayContent", customGenerator: options.allDayContent, defaultGenerator: renderAllDayInner, classNameGenerator: options.allDayClassNames, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }, (InnerContent) => (y("div", { className: [
|
|
'fc-timegrid-axis-frame',
|
|
'fc-scrollgrid-shrink-frame',
|
|
rowHeight == null ? ' fc-timegrid-axis-frame-liquid' : '',
|
|
].join(' '), style: { height: rowHeight } },
|
|
y(InnerContent, { elTag: "span", elClasses: [
|
|
'fc-timegrid-axis-cushion',
|
|
'fc-scrollgrid-shrink-cushion',
|
|
'fc-scrollgrid-sync-inner',
|
|
] })))));
|
|
};
|
|
this.handleSlatCoords = (slatCoords) => {
|
|
this.setState({ slatCoords });
|
|
};
|
|
}
|
|
// rendering
|
|
// ----------------------------------------------------------------------------------------------------
|
|
renderSimpleLayout(headerRowContent, allDayContent, timeContent) {
|
|
let { context, props } = this;
|
|
let sections = [];
|
|
let stickyHeaderDates = getStickyHeaderDates(context.options);
|
|
if (headerRowContent) {
|
|
sections.push({
|
|
type: 'header',
|
|
key: 'header',
|
|
isSticky: stickyHeaderDates,
|
|
chunk: {
|
|
elRef: this.headerElRef,
|
|
tableClassName: 'fc-col-header',
|
|
rowContent: headerRowContent,
|
|
},
|
|
});
|
|
}
|
|
if (allDayContent) {
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'all-day',
|
|
chunk: { content: allDayContent },
|
|
});
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'all-day-divider',
|
|
outerContent: ( // TODO: rename to cellContent so don't need to define <tr>?
|
|
y("tr", { role: "presentation", className: "fc-scrollgrid-section" },
|
|
y("td", { className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
|
|
});
|
|
}
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'body',
|
|
liquid: true,
|
|
expandRows: Boolean(context.options.expandRows),
|
|
chunk: {
|
|
scrollerElRef: this.scrollerElRef,
|
|
content: timeContent,
|
|
},
|
|
});
|
|
return (y(ViewContainer, { elRef: this.rootElRef, elClasses: ['fc-timegrid'], viewSpec: context.viewSpec },
|
|
y(SimpleScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, collapsibleWidth: props.forPrint, cols: [{ width: 'shrink' }], sections: sections })));
|
|
}
|
|
renderHScrollLayout(headerRowContent, allDayContent, timeContent, colCnt, dayMinWidth, slatMetas, slatCoords) {
|
|
let ScrollGrid = this.context.pluginHooks.scrollGridImpl;
|
|
if (!ScrollGrid) {
|
|
throw new Error('No ScrollGrid implementation');
|
|
}
|
|
let { context, props } = this;
|
|
let stickyHeaderDates = !props.forPrint && getStickyHeaderDates(context.options);
|
|
let stickyFooterScrollbar = !props.forPrint && getStickyFooterScrollbar(context.options);
|
|
let sections = [];
|
|
if (headerRowContent) {
|
|
sections.push({
|
|
type: 'header',
|
|
key: 'header',
|
|
isSticky: stickyHeaderDates,
|
|
syncRowHeights: true,
|
|
chunks: [
|
|
{
|
|
key: 'axis',
|
|
rowContent: (arg) => (y("tr", { role: "presentation" }, this.renderHeadAxis('day', arg.rowSyncHeights[0]))),
|
|
},
|
|
{
|
|
key: 'cols',
|
|
elRef: this.headerElRef,
|
|
tableClassName: 'fc-col-header',
|
|
rowContent: headerRowContent,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
if (allDayContent) {
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'all-day',
|
|
syncRowHeights: true,
|
|
chunks: [
|
|
{
|
|
key: 'axis',
|
|
rowContent: (contentArg) => (y("tr", { role: "presentation" }, this.renderTableRowAxis(contentArg.rowSyncHeights[0]))),
|
|
},
|
|
{
|
|
key: 'cols',
|
|
content: allDayContent,
|
|
},
|
|
],
|
|
});
|
|
sections.push({
|
|
key: 'all-day-divider',
|
|
type: 'body',
|
|
outerContent: ( // TODO: rename to cellContent so don't need to define <tr>?
|
|
y("tr", { role: "presentation", className: "fc-scrollgrid-section" },
|
|
y("td", { colSpan: 2, className: 'fc-timegrid-divider ' + context.theme.getClass('tableCellShaded') }))),
|
|
});
|
|
}
|
|
let isNowIndicator = context.options.nowIndicator;
|
|
sections.push({
|
|
type: 'body',
|
|
key: 'body',
|
|
liquid: true,
|
|
expandRows: Boolean(context.options.expandRows),
|
|
chunks: [
|
|
{
|
|
key: 'axis',
|
|
content: (arg) => (
|
|
// TODO: make this now-indicator arrow more DRY with TimeColsContent
|
|
y("div", { className: "fc-timegrid-axis-chunk" },
|
|
y("table", { "aria-hidden": true, style: { height: arg.expandRows ? arg.clientHeight : '' } },
|
|
arg.tableColGroupNode,
|
|
y("tbody", null,
|
|
y(TimeBodyAxis, { slatMetas: slatMetas }))),
|
|
y("div", { className: "fc-timegrid-now-indicator-container" },
|
|
y(NowTimer, { unit: isNowIndicator ? 'minute' : 'day' /* hacky */ }, (nowDate) => {
|
|
let nowIndicatorTop = isNowIndicator &&
|
|
slatCoords &&
|
|
slatCoords.safeComputeTop(nowDate); // might return void
|
|
if (typeof nowIndicatorTop === 'number') {
|
|
return (y(NowIndicatorContainer, { elClasses: ['fc-timegrid-now-indicator-arrow'], elStyle: { top: nowIndicatorTop }, isAxis: true, date: nowDate }));
|
|
}
|
|
return null;
|
|
})))),
|
|
},
|
|
{
|
|
key: 'cols',
|
|
scrollerElRef: this.scrollerElRef,
|
|
content: timeContent,
|
|
},
|
|
],
|
|
});
|
|
if (stickyFooterScrollbar) {
|
|
sections.push({
|
|
key: 'footer',
|
|
type: 'footer',
|
|
isSticky: true,
|
|
chunks: [
|
|
{
|
|
key: 'axis',
|
|
content: renderScrollShim,
|
|
},
|
|
{
|
|
key: 'cols',
|
|
content: renderScrollShim,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
return (y(ViewContainer, { elRef: this.rootElRef, elClasses: ['fc-timegrid'], viewSpec: context.viewSpec },
|
|
y(ScrollGrid, { liquid: !props.isHeightAuto && !props.forPrint, forPrint: props.forPrint, collapsibleWidth: false, colGroups: [
|
|
{ width: 'shrink', cols: [{ width: 'shrink' }] },
|
|
{ cols: [{ span: colCnt, minWidth: dayMinWidth }] },
|
|
], sections: sections })));
|
|
}
|
|
/* Dimensions
|
|
------------------------------------------------------------------------------------------------------------------*/
|
|
getAllDayMaxEventProps() {
|
|
let { dayMaxEvents, dayMaxEventRows } = this.context.options;
|
|
if (dayMaxEvents === true || dayMaxEventRows === true) { // is auto?
|
|
dayMaxEvents = undefined;
|
|
dayMaxEventRows = AUTO_ALL_DAY_MAX_EVENT_ROWS; // make sure "auto" goes to a real number
|
|
}
|
|
return { dayMaxEvents, dayMaxEventRows };
|
|
}
|
|
}
|
|
function renderAllDayInner(renderProps) {
|
|
return renderProps.text;
|
|
}
|
|
|
|
class TimeColsSlatsCoords {
|
|
constructor(positions, dateProfile, slotDuration) {
|
|
this.positions = positions;
|
|
this.dateProfile = dateProfile;
|
|
this.slotDuration = slotDuration;
|
|
}
|
|
safeComputeTop(date) {
|
|
let { dateProfile } = this;
|
|
if (rangeContainsMarker(dateProfile.currentRange, date)) {
|
|
let startOfDayDate = startOfDay(date);
|
|
let timeMs = date.valueOf() - startOfDayDate.valueOf();
|
|
if (timeMs >= asRoughMs(dateProfile.slotMinTime) &&
|
|
timeMs < asRoughMs(dateProfile.slotMaxTime)) {
|
|
return this.computeTimeTop(createDuration(timeMs));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given date.
|
|
// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
|
|
computeDateTop(when, startOfDayDate) {
|
|
if (!startOfDayDate) {
|
|
startOfDayDate = startOfDay(when);
|
|
}
|
|
return this.computeTimeTop(createDuration(when.valueOf() - startOfDayDate.valueOf()));
|
|
}
|
|
// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
|
|
// This is a makeshify way to compute the time-top. Assumes all slatMetas dates are uniform.
|
|
// Eventually allow computation with arbirary slat dates.
|
|
computeTimeTop(duration) {
|
|
let { positions, dateProfile } = this;
|
|
let len = positions.els.length;
|
|
// floating-point value of # of slots covered
|
|
let slatCoverage = (duration.milliseconds - asRoughMs(dateProfile.slotMinTime)) / asRoughMs(this.slotDuration);
|
|
let slatIndex;
|
|
let slatRemainder;
|
|
// compute a floating-point number for how many slats should be progressed through.
|
|
// from 0 to number of slats (inclusive)
|
|
// constrained because slotMinTime/slotMaxTime might be customized.
|
|
slatCoverage = Math.max(0, slatCoverage);
|
|
slatCoverage = Math.min(len, slatCoverage);
|
|
// an integer index of the furthest whole slat
|
|
// from 0 to number slats (*exclusive*, so len-1)
|
|
slatIndex = Math.floor(slatCoverage);
|
|
slatIndex = Math.min(slatIndex, len - 1);
|
|
// how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
|
|
// could be 1.0 if slatCoverage is covering *all* the slots
|
|
slatRemainder = slatCoverage - slatIndex;
|
|
return positions.tops[slatIndex] +
|
|
positions.getHeight(slatIndex) * slatRemainder;
|
|
}
|
|
}
|
|
|
|
class TimeColsSlatsBody extends BaseComponent {
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let { slatElRefs } = props;
|
|
return (y("tbody", null, props.slatMetas.map((slatMeta, i) => {
|
|
let renderProps = {
|
|
time: slatMeta.time,
|
|
date: context.dateEnv.toDate(slatMeta.date),
|
|
view: context.viewApi,
|
|
};
|
|
return (y("tr", { key: slatMeta.key, ref: slatElRefs.createRef(slatMeta.key) },
|
|
props.axis && (y(TimeColsAxisCell, Object.assign({}, slatMeta))),
|
|
y(ContentContainer, { elTag: "td", elClasses: [
|
|
'fc-timegrid-slot',
|
|
'fc-timegrid-slot-lane',
|
|
!slatMeta.isLabeled && 'fc-timegrid-slot-minor',
|
|
], elAttrs: {
|
|
'data-time': slatMeta.isoTimeStr,
|
|
}, renderProps: renderProps, generatorName: "slotLaneContent", customGenerator: options.slotLaneContent, classNameGenerator: options.slotLaneClassNames, didMount: options.slotLaneDidMount, willUnmount: options.slotLaneWillUnmount })));
|
|
})));
|
|
}
|
|
}
|
|
|
|
/*
|
|
for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
|
|
*/
|
|
class TimeColsSlats extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.rootElRef = d();
|
|
this.slatElRefs = new RefMap();
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
return (y("div", { ref: this.rootElRef, className: "fc-timegrid-slots" },
|
|
y("table", { "aria-hidden": true, className: context.theme.getClass('table'), style: {
|
|
minWidth: props.tableMinWidth,
|
|
width: props.clientWidth,
|
|
height: props.minHeight,
|
|
} },
|
|
props.tableColGroupNode /* relies on there only being a single <col> for the axis */,
|
|
y(TimeColsSlatsBody, { slatElRefs: this.slatElRefs, axis: props.axis, slatMetas: props.slatMetas }))));
|
|
}
|
|
componentDidMount() {
|
|
this.updateSizing();
|
|
}
|
|
componentDidUpdate() {
|
|
this.updateSizing();
|
|
}
|
|
componentWillUnmount() {
|
|
if (this.props.onCoords) {
|
|
this.props.onCoords(null);
|
|
}
|
|
}
|
|
updateSizing() {
|
|
let { context, props } = this;
|
|
if (props.onCoords &&
|
|
props.clientWidth !== null // means sizing has stabilized
|
|
) {
|
|
let rootEl = this.rootElRef.current;
|
|
if (rootEl.offsetHeight) { // not hidden by css
|
|
props.onCoords(new TimeColsSlatsCoords(new PositionCache(this.rootElRef.current, collectSlatEls(this.slatElRefs.currentMap, props.slatMetas), false, true), this.props.dateProfile, context.options.slotDuration));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function collectSlatEls(elMap, slatMetas) {
|
|
return slatMetas.map((slatMeta) => elMap[slatMeta.key]);
|
|
}
|
|
|
|
function splitSegsByCol(segs, colCnt) {
|
|
let segsByCol = [];
|
|
let i;
|
|
for (i = 0; i < colCnt; i += 1) {
|
|
segsByCol.push([]);
|
|
}
|
|
if (segs) {
|
|
for (i = 0; i < segs.length; i += 1) {
|
|
segsByCol[segs[i].col].push(segs[i]);
|
|
}
|
|
}
|
|
return segsByCol;
|
|
}
|
|
function splitInteractionByCol(ui, colCnt) {
|
|
let byRow = [];
|
|
if (!ui) {
|
|
for (let i = 0; i < colCnt; i += 1) {
|
|
byRow[i] = null;
|
|
}
|
|
}
|
|
else {
|
|
for (let i = 0; i < colCnt; i += 1) {
|
|
byRow[i] = {
|
|
affectedInstances: ui.affectedInstances,
|
|
isEvent: ui.isEvent,
|
|
segs: [],
|
|
};
|
|
}
|
|
for (let seg of ui.segs) {
|
|
byRow[seg.col].segs.push(seg);
|
|
}
|
|
}
|
|
return byRow;
|
|
}
|
|
|
|
class TimeColMoreLink extends BaseComponent {
|
|
render() {
|
|
let { props } = this;
|
|
return (y(MoreLinkContainer, { elClasses: ['fc-timegrid-more-link'], elStyle: {
|
|
top: props.top,
|
|
bottom: props.bottom,
|
|
}, allDayDate: null, moreCnt: props.hiddenSegs.length, allSegs: props.hiddenSegs, hiddenSegs: props.hiddenSegs, extraDateSpan: props.extraDateSpan, dateProfile: props.dateProfile, todayRange: props.todayRange, popoverContent: () => renderPlainFgSegs(props.hiddenSegs, props), defaultGenerator: internal_renderMoreLinkInner, forceTimed: true }, (InnerContent) => (y(InnerContent, { elTag: "div", elClasses: ['fc-timegrid-more-link-inner', 'fc-sticky'] }))));
|
|
}
|
|
}
|
|
function internal_renderMoreLinkInner(props) {
|
|
return props.shortText;
|
|
}
|
|
|
|
// segInputs assumed sorted
|
|
function buildPositioning(segInputs, strictOrder, maxStackCnt) {
|
|
let hierarchy = new SegHierarchy();
|
|
if (strictOrder != null) {
|
|
hierarchy.strictOrder = strictOrder;
|
|
}
|
|
if (maxStackCnt != null) {
|
|
hierarchy.maxStackCnt = maxStackCnt;
|
|
}
|
|
let hiddenEntries = hierarchy.addSegs(segInputs);
|
|
let hiddenGroups = groupIntersectingEntries(hiddenEntries);
|
|
let web = buildWeb(hierarchy);
|
|
web = stretchWeb(web, 1); // all levelCoords/thickness will have 0.0-1.0
|
|
let segRects = webToRects(web);
|
|
return { segRects, hiddenGroups };
|
|
}
|
|
function buildWeb(hierarchy) {
|
|
const { entriesByLevel } = hierarchy;
|
|
const buildNode = cacheable((level, lateral) => level + ':' + lateral, (level, lateral) => {
|
|
let siblingRange = findNextLevelSegs(hierarchy, level, lateral);
|
|
let nextLevelRes = buildNodes(siblingRange, buildNode);
|
|
let entry = entriesByLevel[level][lateral];
|
|
return [
|
|
Object.assign(Object.assign({}, entry), { nextLevelNodes: nextLevelRes[0] }),
|
|
entry.thickness + nextLevelRes[1], // the pressure builds
|
|
];
|
|
});
|
|
return buildNodes(entriesByLevel.length
|
|
? { level: 0, lateralStart: 0, lateralEnd: entriesByLevel[0].length }
|
|
: null, buildNode)[0];
|
|
}
|
|
function buildNodes(siblingRange, buildNode) {
|
|
if (!siblingRange) {
|
|
return [[], 0];
|
|
}
|
|
let { level, lateralStart, lateralEnd } = siblingRange;
|
|
let lateral = lateralStart;
|
|
let pairs = [];
|
|
while (lateral < lateralEnd) {
|
|
pairs.push(buildNode(level, lateral));
|
|
lateral += 1;
|
|
}
|
|
pairs.sort(cmpDescPressures);
|
|
return [
|
|
pairs.map(extractNode),
|
|
pairs[0][1], // first item's pressure
|
|
];
|
|
}
|
|
function cmpDescPressures(a, b) {
|
|
return b[1] - a[1];
|
|
}
|
|
function extractNode(a) {
|
|
return a[0];
|
|
}
|
|
function findNextLevelSegs(hierarchy, subjectLevel, subjectLateral) {
|
|
let { levelCoords, entriesByLevel } = hierarchy;
|
|
let subjectEntry = entriesByLevel[subjectLevel][subjectLateral];
|
|
let afterSubject = levelCoords[subjectLevel] + subjectEntry.thickness;
|
|
let levelCnt = levelCoords.length;
|
|
let level = subjectLevel;
|
|
// skip past levels that are too high up
|
|
for (; level < levelCnt && levelCoords[level] < afterSubject; level += 1)
|
|
; // do nothing
|
|
for (; level < levelCnt; level += 1) {
|
|
let entries = entriesByLevel[level];
|
|
let entry;
|
|
let searchIndex = internal_common_binarySearch(entries, subjectEntry.span.start, getEntrySpanEnd);
|
|
let lateralStart = searchIndex[0] + searchIndex[1]; // if exact match (which doesn't collide), go to next one
|
|
let lateralEnd = lateralStart;
|
|
while ( // loop through entries that horizontally intersect
|
|
(entry = entries[lateralEnd]) && // but not past the whole seg list
|
|
entry.span.start < subjectEntry.span.end) {
|
|
lateralEnd += 1;
|
|
}
|
|
if (lateralStart < lateralEnd) {
|
|
return { level, lateralStart, lateralEnd };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
function stretchWeb(topLevelNodes, totalThickness) {
|
|
const stretchNode = cacheable((node, startCoord, prevThickness) => buildEntryKey(node), (node, startCoord, prevThickness) => {
|
|
let { nextLevelNodes, thickness } = node;
|
|
let allThickness = thickness + prevThickness;
|
|
let thicknessFraction = thickness / allThickness;
|
|
let endCoord;
|
|
let newChildren = [];
|
|
if (!nextLevelNodes.length) {
|
|
endCoord = totalThickness;
|
|
}
|
|
else {
|
|
for (let childNode of nextLevelNodes) {
|
|
if (endCoord === undefined) {
|
|
let res = stretchNode(childNode, startCoord, allThickness);
|
|
endCoord = res[0];
|
|
newChildren.push(res[1]);
|
|
}
|
|
else {
|
|
let res = stretchNode(childNode, endCoord, 0);
|
|
newChildren.push(res[1]);
|
|
}
|
|
}
|
|
}
|
|
let newThickness = (endCoord - startCoord) * thicknessFraction;
|
|
return [endCoord - newThickness, Object.assign(Object.assign({}, node), { thickness: newThickness, nextLevelNodes: newChildren })];
|
|
});
|
|
return topLevelNodes.map((node) => stretchNode(node, 0, 0)[1]);
|
|
}
|
|
// not sorted in any particular order
|
|
function webToRects(topLevelNodes) {
|
|
let rects = [];
|
|
const processNode = cacheable((node, levelCoord, stackDepth) => buildEntryKey(node), (node, levelCoord, stackDepth) => {
|
|
let rect = Object.assign(Object.assign({}, node), { levelCoord,
|
|
stackDepth, stackForward: 0 });
|
|
rects.push(rect);
|
|
return (rect.stackForward = processNodes(node.nextLevelNodes, levelCoord + node.thickness, stackDepth + 1) + 1);
|
|
});
|
|
function processNodes(nodes, levelCoord, stackDepth) {
|
|
let stackForward = 0;
|
|
for (let node of nodes) {
|
|
stackForward = Math.max(processNode(node, levelCoord, stackDepth), stackForward);
|
|
}
|
|
return stackForward;
|
|
}
|
|
processNodes(topLevelNodes, 0, 0);
|
|
return rects; // TODO: sort rects by levelCoord to be consistent with toRects?
|
|
}
|
|
// TODO: move to general util
|
|
function cacheable(keyFunc, workFunc) {
|
|
const cache = {};
|
|
return (...args) => {
|
|
let key = keyFunc(...args);
|
|
return (key in cache)
|
|
? cache[key]
|
|
: (cache[key] = workFunc(...args));
|
|
};
|
|
}
|
|
|
|
function computeSegVCoords(segs, colDate, slatCoords = null, eventMinHeight = 0) {
|
|
let vcoords = [];
|
|
if (slatCoords) {
|
|
for (let i = 0; i < segs.length; i += 1) {
|
|
let seg = segs[i];
|
|
let spanStart = slatCoords.computeDateTop(seg.start, colDate);
|
|
let spanEnd = Math.max(spanStart + (eventMinHeight || 0), // :(
|
|
slatCoords.computeDateTop(seg.end, colDate));
|
|
vcoords.push({
|
|
start: Math.round(spanStart),
|
|
end: Math.round(spanEnd), //
|
|
});
|
|
}
|
|
}
|
|
return vcoords;
|
|
}
|
|
function computeFgSegPlacements(segs, segVCoords, // might not have for every seg
|
|
eventOrderStrict, eventMaxStack) {
|
|
let segInputs = [];
|
|
let dumbSegs = []; // segs without coords
|
|
for (let i = 0; i < segs.length; i += 1) {
|
|
let vcoords = segVCoords[i];
|
|
if (vcoords) {
|
|
segInputs.push({
|
|
index: i,
|
|
thickness: 1,
|
|
span: vcoords,
|
|
});
|
|
}
|
|
else {
|
|
dumbSegs.push(segs[i]);
|
|
}
|
|
}
|
|
let { segRects, hiddenGroups } = buildPositioning(segInputs, eventOrderStrict, eventMaxStack);
|
|
let segPlacements = [];
|
|
for (let segRect of segRects) {
|
|
segPlacements.push({
|
|
seg: segs[segRect.index],
|
|
rect: segRect,
|
|
});
|
|
}
|
|
for (let dumbSeg of dumbSegs) {
|
|
segPlacements.push({ seg: dumbSeg, rect: null });
|
|
}
|
|
return { segPlacements, hiddenGroups };
|
|
}
|
|
|
|
const DEFAULT_TIME_FORMAT = createFormatter({
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
meridiem: false,
|
|
});
|
|
class TimeColEvent extends BaseComponent {
|
|
render() {
|
|
return (y(StandardEvent, Object.assign({}, this.props, { elClasses: [
|
|
'fc-timegrid-event',
|
|
'fc-v-event',
|
|
this.props.isShort && 'fc-timegrid-event-short',
|
|
], defaultTimeFormat: DEFAULT_TIME_FORMAT })));
|
|
}
|
|
}
|
|
|
|
class TimeCol extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.sortEventSegs = memoize(sortEventSegs);
|
|
}
|
|
// TODO: memoize event-placement?
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let isSelectMirror = options.selectMirror;
|
|
let mirrorSegs = // yuck
|
|
(props.eventDrag && props.eventDrag.segs) ||
|
|
(props.eventResize && props.eventResize.segs) ||
|
|
(isSelectMirror && props.dateSelectionSegs) ||
|
|
[];
|
|
let interactionAffectedInstances = // TODO: messy way to compute this
|
|
(props.eventDrag && props.eventDrag.affectedInstances) ||
|
|
(props.eventResize && props.eventResize.affectedInstances) ||
|
|
{};
|
|
let sortedFgSegs = this.sortEventSegs(props.fgEventSegs, options.eventOrder);
|
|
return (y(DayCellContainer, { elTag: "td", elRef: props.elRef, elClasses: [
|
|
'fc-timegrid-col',
|
|
...(props.extraClassNames || []),
|
|
], elAttrs: Object.assign({ role: 'gridcell' }, props.extraDataAttrs), date: props.date, dateProfile: props.dateProfile, todayRange: props.todayRange, extraRenderProps: props.extraRenderProps }, (InnerContent) => (y("div", { className: "fc-timegrid-col-frame" },
|
|
y("div", { className: "fc-timegrid-col-bg" },
|
|
this.renderFillSegs(props.businessHourSegs, 'non-business'),
|
|
this.renderFillSegs(props.bgEventSegs, 'bg-event'),
|
|
this.renderFillSegs(props.dateSelectionSegs, 'highlight')),
|
|
y("div", { className: "fc-timegrid-col-events" }, this.renderFgSegs(sortedFgSegs, interactionAffectedInstances, false, false, false)),
|
|
y("div", { className: "fc-timegrid-col-events" }, this.renderFgSegs(mirrorSegs, {}, Boolean(props.eventDrag), Boolean(props.eventResize), Boolean(isSelectMirror), 'mirror')),
|
|
y("div", { className: "fc-timegrid-now-indicator-container" }, this.renderNowIndicator(props.nowIndicatorSegs)),
|
|
hasCustomDayCellContent(options) && (y(InnerContent, { elTag: "div", elClasses: ['fc-timegrid-col-misc'] }))))));
|
|
}
|
|
renderFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey) {
|
|
let { props } = this;
|
|
if (props.forPrint) {
|
|
return renderPlainFgSegs(sortedFgSegs, props);
|
|
}
|
|
return this.renderPositionedFgSegs(sortedFgSegs, segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey);
|
|
}
|
|
renderPositionedFgSegs(segs, // if not mirror, needs to be sorted
|
|
segIsInvisible, isDragging, isResizing, isDateSelecting, forcedKey) {
|
|
let { eventMaxStack, eventShortHeight, eventOrderStrict, eventMinHeight } = this.context.options;
|
|
let { date, slatCoords, eventSelection, todayRange, nowDate } = this.props;
|
|
let isMirror = isDragging || isResizing || isDateSelecting;
|
|
let segVCoords = computeSegVCoords(segs, date, slatCoords, eventMinHeight);
|
|
let { segPlacements, hiddenGroups } = computeFgSegPlacements(segs, segVCoords, eventOrderStrict, eventMaxStack);
|
|
return (y(_, null,
|
|
this.renderHiddenGroups(hiddenGroups, segs),
|
|
segPlacements.map((segPlacement) => {
|
|
let { seg, rect } = segPlacement;
|
|
let instanceId = seg.eventRange.instance.instanceId;
|
|
let isVisible = isMirror || Boolean(!segIsInvisible[instanceId] && rect);
|
|
let vStyle = computeSegVStyle(rect && rect.span);
|
|
let hStyle = (!isMirror && rect) ? this.computeSegHStyle(rect) : { left: 0, right: 0 };
|
|
let isInset = Boolean(rect) && rect.stackForward > 0;
|
|
let isShort = Boolean(rect) && (rect.span.end - rect.span.start) < eventShortHeight; // look at other places for this problem
|
|
return (y("div", { className: 'fc-timegrid-event-harness' +
|
|
(isInset ? ' fc-timegrid-event-harness-inset' : ''), key: forcedKey || instanceId, style: Object.assign(Object.assign({ visibility: isVisible ? '' : 'hidden' }, vStyle), hStyle) },
|
|
y(TimeColEvent, Object.assign({ seg: seg, isDragging: isDragging, isResizing: isResizing, isDateSelecting: isDateSelecting, isSelected: instanceId === eventSelection, isShort: isShort }, getSegMeta(seg, todayRange, nowDate)))));
|
|
})));
|
|
}
|
|
// will already have eventMinHeight applied because segInputs already had it
|
|
renderHiddenGroups(hiddenGroups, segs) {
|
|
let { extraDateSpan, dateProfile, todayRange, nowDate, eventSelection, eventDrag, eventResize } = this.props;
|
|
return (y(_, null, hiddenGroups.map((hiddenGroup) => {
|
|
let positionCss = computeSegVStyle(hiddenGroup.span);
|
|
let hiddenSegs = compileSegsFromEntries(hiddenGroup.entries, segs);
|
|
return (y(TimeColMoreLink, { key: buildIsoString(computeEarliestSegStart(hiddenSegs)), hiddenSegs: hiddenSegs, top: positionCss.top, bottom: positionCss.bottom, extraDateSpan: extraDateSpan, dateProfile: dateProfile, todayRange: todayRange, nowDate: nowDate, eventSelection: eventSelection, eventDrag: eventDrag, eventResize: eventResize }));
|
|
})));
|
|
}
|
|
renderFillSegs(segs, fillType) {
|
|
let { props, context } = this;
|
|
let segVCoords = computeSegVCoords(segs, props.date, props.slatCoords, context.options.eventMinHeight); // don't assume all populated
|
|
let children = segVCoords.map((vcoords, i) => {
|
|
let seg = segs[i];
|
|
return (y("div", { key: buildEventRangeKey(seg.eventRange), className: "fc-timegrid-bg-harness", style: computeSegVStyle(vcoords) }, fillType === 'bg-event' ?
|
|
y(BgEvent, Object.assign({ seg: seg }, getSegMeta(seg, props.todayRange, props.nowDate))) :
|
|
renderFill(fillType)));
|
|
});
|
|
return y(_, null, children);
|
|
}
|
|
renderNowIndicator(segs) {
|
|
let { slatCoords, date } = this.props;
|
|
if (!slatCoords) {
|
|
return null;
|
|
}
|
|
return segs.map((seg, i) => (y(NowIndicatorContainer
|
|
// key doesn't matter. will only ever be one
|
|
, {
|
|
// key doesn't matter. will only ever be one
|
|
key: i, elClasses: ['fc-timegrid-now-indicator-line'], elStyle: {
|
|
top: slatCoords.computeDateTop(seg.start, date),
|
|
}, isAxis: false, date: date })));
|
|
}
|
|
computeSegHStyle(segHCoords) {
|
|
let { isRtl, options } = this.context;
|
|
let shouldOverlap = options.slotEventOverlap;
|
|
let nearCoord = segHCoords.levelCoord; // the left side if LTR. the right side if RTL. floating-point
|
|
let farCoord = segHCoords.levelCoord + segHCoords.thickness; // the right side if LTR. the left side if RTL. floating-point
|
|
let left; // amount of space from left edge, a fraction of the total width
|
|
let right; // amount of space from right edge, a fraction of the total width
|
|
if (shouldOverlap) {
|
|
// double the width, but don't go beyond the maximum forward coordinate (1.0)
|
|
farCoord = Math.min(1, nearCoord + (farCoord - nearCoord) * 2);
|
|
}
|
|
if (isRtl) {
|
|
left = 1 - farCoord;
|
|
right = nearCoord;
|
|
}
|
|
else {
|
|
left = nearCoord;
|
|
right = 1 - farCoord;
|
|
}
|
|
let props = {
|
|
zIndex: segHCoords.stackDepth + 1,
|
|
left: left * 100 + '%',
|
|
right: right * 100 + '%',
|
|
};
|
|
if (shouldOverlap && !segHCoords.stackForward) {
|
|
// add padding to the edge so that forward stacked events don't cover the resizer's icon
|
|
props[isRtl ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
|
|
}
|
|
return props;
|
|
}
|
|
}
|
|
function renderPlainFgSegs(sortedFgSegs, { todayRange, nowDate, eventSelection, eventDrag, eventResize }) {
|
|
let hiddenInstances = (eventDrag ? eventDrag.affectedInstances : null) ||
|
|
(eventResize ? eventResize.affectedInstances : null) ||
|
|
{};
|
|
return (y(_, null, sortedFgSegs.map((seg) => {
|
|
let instanceId = seg.eventRange.instance.instanceId;
|
|
return (y("div", { key: instanceId, style: { visibility: hiddenInstances[instanceId] ? 'hidden' : '' } },
|
|
y(TimeColEvent, Object.assign({ seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: instanceId === eventSelection, isShort: false }, getSegMeta(seg, todayRange, nowDate)))));
|
|
})));
|
|
}
|
|
function computeSegVStyle(segVCoords) {
|
|
if (!segVCoords) {
|
|
return { top: '', bottom: '' };
|
|
}
|
|
return {
|
|
top: segVCoords.start,
|
|
bottom: -segVCoords.end,
|
|
};
|
|
}
|
|
function compileSegsFromEntries(segEntries, allSegs) {
|
|
return segEntries.map((segEntry) => allSegs[segEntry.index]);
|
|
}
|
|
|
|
class TimeColsContent extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.splitFgEventSegs = memoize(splitSegsByCol);
|
|
this.splitBgEventSegs = memoize(splitSegsByCol);
|
|
this.splitBusinessHourSegs = memoize(splitSegsByCol);
|
|
this.splitNowIndicatorSegs = memoize(splitSegsByCol);
|
|
this.splitDateSelectionSegs = memoize(splitSegsByCol);
|
|
this.splitEventDrag = memoize(splitInteractionByCol);
|
|
this.splitEventResize = memoize(splitInteractionByCol);
|
|
this.rootElRef = d();
|
|
this.cellElRefs = new RefMap();
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
let nowIndicatorTop = context.options.nowIndicator &&
|
|
props.slatCoords &&
|
|
props.slatCoords.safeComputeTop(props.nowDate); // might return void
|
|
let colCnt = props.cells.length;
|
|
let fgEventSegsByRow = this.splitFgEventSegs(props.fgEventSegs, colCnt);
|
|
let bgEventSegsByRow = this.splitBgEventSegs(props.bgEventSegs, colCnt);
|
|
let businessHourSegsByRow = this.splitBusinessHourSegs(props.businessHourSegs, colCnt);
|
|
let nowIndicatorSegsByRow = this.splitNowIndicatorSegs(props.nowIndicatorSegs, colCnt);
|
|
let dateSelectionSegsByRow = this.splitDateSelectionSegs(props.dateSelectionSegs, colCnt);
|
|
let eventDragByRow = this.splitEventDrag(props.eventDrag, colCnt);
|
|
let eventResizeByRow = this.splitEventResize(props.eventResize, colCnt);
|
|
return (y("div", { className: "fc-timegrid-cols", ref: this.rootElRef },
|
|
y("table", { role: "presentation", style: {
|
|
minWidth: props.tableMinWidth,
|
|
width: props.clientWidth,
|
|
} },
|
|
props.tableColGroupNode,
|
|
y("tbody", { role: "presentation" },
|
|
y("tr", { role: "row" },
|
|
props.axis && (y("td", { "aria-hidden": true, className: "fc-timegrid-col fc-timegrid-axis" },
|
|
y("div", { className: "fc-timegrid-col-frame" },
|
|
y("div", { className: "fc-timegrid-now-indicator-container" }, typeof nowIndicatorTop === 'number' && (y(NowIndicatorContainer, { elClasses: ['fc-timegrid-now-indicator-arrow'], elStyle: { top: nowIndicatorTop }, isAxis: true, date: props.nowDate })))))),
|
|
props.cells.map((cell, i) => (y(TimeCol, { key: cell.key, elRef: this.cellElRefs.createRef(cell.key), dateProfile: props.dateProfile, date: cell.date, nowDate: props.nowDate, todayRange: props.todayRange, extraRenderProps: cell.extraRenderProps, extraDataAttrs: cell.extraDataAttrs, extraClassNames: cell.extraClassNames, extraDateSpan: cell.extraDateSpan, fgEventSegs: fgEventSegsByRow[i], bgEventSegs: bgEventSegsByRow[i], businessHourSegs: businessHourSegsByRow[i], nowIndicatorSegs: nowIndicatorSegsByRow[i], dateSelectionSegs: dateSelectionSegsByRow[i], eventDrag: eventDragByRow[i], eventResize: eventResizeByRow[i], slatCoords: props.slatCoords, eventSelection: props.eventSelection, forPrint: props.forPrint }))))))));
|
|
}
|
|
componentDidMount() {
|
|
this.updateCoords();
|
|
}
|
|
componentDidUpdate() {
|
|
this.updateCoords();
|
|
}
|
|
updateCoords() {
|
|
let { props } = this;
|
|
if (props.onColCoords &&
|
|
props.clientWidth !== null // means sizing has stabilized
|
|
) {
|
|
props.onColCoords(new PositionCache(this.rootElRef.current, collectCellEls(this.cellElRefs.currentMap, props.cells), true, // horizontal
|
|
false));
|
|
}
|
|
}
|
|
}
|
|
function collectCellEls(elMap, cells) {
|
|
return cells.map((cell) => elMap[cell.key]);
|
|
}
|
|
|
|
/* A component that renders one or more columns of vertical time slots
|
|
----------------------------------------------------------------------------------------------------------------------*/
|
|
class TimeCols extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.processSlotOptions = memoize(processSlotOptions);
|
|
this.state = {
|
|
slatCoords: null,
|
|
};
|
|
this.handleRootEl = (el) => {
|
|
if (el) {
|
|
this.context.registerInteractiveComponent(this, {
|
|
el,
|
|
isHitComboAllowed: this.props.isHitComboAllowed,
|
|
});
|
|
}
|
|
else {
|
|
this.context.unregisterInteractiveComponent(this);
|
|
}
|
|
};
|
|
this.handleScrollRequest = (request) => {
|
|
let { onScrollTopRequest } = this.props;
|
|
let { slatCoords } = this.state;
|
|
if (onScrollTopRequest && slatCoords) {
|
|
if (request.time) {
|
|
let top = slatCoords.computeTimeTop(request.time);
|
|
top = Math.ceil(top); // zoom can give weird floating-point values. rather scroll a little bit further
|
|
if (top) {
|
|
top += 1; // to overcome top border that slots beyond the first have. looks better
|
|
}
|
|
onScrollTopRequest(top);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
this.handleColCoords = (colCoords) => {
|
|
this.colCoords = colCoords;
|
|
};
|
|
this.handleSlatCoords = (slatCoords) => {
|
|
this.setState({ slatCoords });
|
|
if (this.props.onSlatCoords) {
|
|
this.props.onSlatCoords(slatCoords);
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
let { props, state } = this;
|
|
return (y("div", { className: "fc-timegrid-body", ref: this.handleRootEl, style: {
|
|
// these props are important to give this wrapper correct dimensions for interactions
|
|
// TODO: if we set it here, can we avoid giving to inner tables?
|
|
width: props.clientWidth,
|
|
minWidth: props.tableMinWidth,
|
|
} },
|
|
y(TimeColsSlats, { axis: props.axis, dateProfile: props.dateProfile, slatMetas: props.slatMetas, clientWidth: props.clientWidth, minHeight: props.expandRows ? props.clientHeight : '', tableMinWidth: props.tableMinWidth, tableColGroupNode: props.axis ? props.tableColGroupNode : null /* axis depends on the colgroup's shrinking */, onCoords: this.handleSlatCoords }),
|
|
y(TimeColsContent, { cells: props.cells, axis: props.axis, dateProfile: props.dateProfile, businessHourSegs: props.businessHourSegs, bgEventSegs: props.bgEventSegs, fgEventSegs: props.fgEventSegs, dateSelectionSegs: props.dateSelectionSegs, eventSelection: props.eventSelection, eventDrag: props.eventDrag, eventResize: props.eventResize, todayRange: props.todayRange, nowDate: props.nowDate, nowIndicatorSegs: props.nowIndicatorSegs, clientWidth: props.clientWidth, tableMinWidth: props.tableMinWidth, tableColGroupNode: props.tableColGroupNode, slatCoords: state.slatCoords, onColCoords: this.handleColCoords, forPrint: props.forPrint })));
|
|
}
|
|
componentDidMount() {
|
|
this.scrollResponder = this.context.createScrollResponder(this.handleScrollRequest);
|
|
}
|
|
componentDidUpdate(prevProps) {
|
|
this.scrollResponder.update(prevProps.dateProfile !== this.props.dateProfile);
|
|
}
|
|
componentWillUnmount() {
|
|
this.scrollResponder.detach();
|
|
}
|
|
queryHit(positionLeft, positionTop) {
|
|
let { dateEnv, options } = this.context;
|
|
let { colCoords } = this;
|
|
let { dateProfile } = this.props;
|
|
let { slatCoords } = this.state;
|
|
let { snapDuration, snapsPerSlot } = this.processSlotOptions(this.props.slotDuration, options.snapDuration);
|
|
let colIndex = colCoords.leftToIndex(positionLeft);
|
|
let slatIndex = slatCoords.positions.topToIndex(positionTop);
|
|
if (colIndex != null && slatIndex != null) {
|
|
let cell = this.props.cells[colIndex];
|
|
let slatTop = slatCoords.positions.tops[slatIndex];
|
|
let slatHeight = slatCoords.positions.getHeight(slatIndex);
|
|
let partial = (positionTop - slatTop) / slatHeight; // floating point number between 0 and 1
|
|
let localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
|
|
let snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
|
|
let dayDate = this.props.cells[colIndex].date;
|
|
let time = addDurations(dateProfile.slotMinTime, multiplyDuration(snapDuration, snapIndex));
|
|
let start = dateEnv.add(dayDate, time);
|
|
let end = dateEnv.add(start, snapDuration);
|
|
return {
|
|
dateProfile,
|
|
dateSpan: Object.assign({ range: { start, end }, allDay: false }, cell.extraDateSpan),
|
|
dayEl: colCoords.els[colIndex],
|
|
rect: {
|
|
left: colCoords.lefts[colIndex],
|
|
right: colCoords.rights[colIndex],
|
|
top: slatTop,
|
|
bottom: slatTop + slatHeight,
|
|
},
|
|
layer: 0,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
function processSlotOptions(slotDuration, snapDurationOverride) {
|
|
let snapDuration = snapDurationOverride || slotDuration;
|
|
let snapsPerSlot = wholeDivideDurations(slotDuration, snapDuration);
|
|
if (snapsPerSlot === null) {
|
|
snapDuration = slotDuration;
|
|
snapsPerSlot = 1;
|
|
// TODO: say warning?
|
|
}
|
|
return { snapDuration, snapsPerSlot };
|
|
}
|
|
|
|
class DayTimeColsSlicer extends Slicer {
|
|
sliceRange(range, dayRanges) {
|
|
let segs = [];
|
|
for (let col = 0; col < dayRanges.length; col += 1) {
|
|
let segRange = intersectRanges(range, dayRanges[col]);
|
|
if (segRange) {
|
|
segs.push({
|
|
start: segRange.start,
|
|
end: segRange.end,
|
|
isStart: segRange.start.valueOf() === range.start.valueOf(),
|
|
isEnd: segRange.end.valueOf() === range.end.valueOf(),
|
|
col,
|
|
});
|
|
}
|
|
}
|
|
return segs;
|
|
}
|
|
}
|
|
|
|
class DayTimeCols extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buildDayRanges = memoize(buildDayRanges);
|
|
this.slicer = new DayTimeColsSlicer();
|
|
this.timeColsRef = d();
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
let { dateProfile, dayTableModel } = props;
|
|
let { nowIndicator, nextDayThreshold } = context.options;
|
|
let dayRanges = this.buildDayRanges(dayTableModel, dateProfile, context.dateEnv);
|
|
// give it the first row of cells
|
|
// TODO: would move this further down hierarchy, but sliceNowDate needs it
|
|
return (y(NowTimer, { unit: nowIndicator ? 'minute' : 'day' }, (nowDate, todayRange) => (y(TimeCols, Object.assign({ ref: this.timeColsRef }, this.slicer.sliceProps(props, dateProfile, null, context, dayRanges), { forPrint: props.forPrint, axis: props.axis, dateProfile: dateProfile, slatMetas: props.slatMetas, slotDuration: props.slotDuration, cells: dayTableModel.cells[0], tableColGroupNode: props.tableColGroupNode, tableMinWidth: props.tableMinWidth, clientWidth: props.clientWidth, clientHeight: props.clientHeight, expandRows: props.expandRows, nowDate: nowDate, nowIndicatorSegs: nowIndicator && this.slicer.sliceNowDate(nowDate, dateProfile, nextDayThreshold, context, dayRanges), todayRange: todayRange, onScrollTopRequest: props.onScrollTopRequest, onSlatCoords: props.onSlatCoords })))));
|
|
}
|
|
}
|
|
function buildDayRanges(dayTableModel, dateProfile, dateEnv) {
|
|
let ranges = [];
|
|
for (let date of dayTableModel.headerDates) {
|
|
ranges.push({
|
|
start: dateEnv.add(date, dateProfile.slotMinTime),
|
|
end: dateEnv.add(date, dateProfile.slotMaxTime),
|
|
});
|
|
}
|
|
return ranges;
|
|
}
|
|
|
|
// potential nice values for the slot-duration and interval-duration
|
|
// from largest to smallest
|
|
const STOCK_SUB_DURATIONS = [
|
|
{ hours: 1 },
|
|
{ minutes: 30 },
|
|
{ minutes: 15 },
|
|
{ seconds: 30 },
|
|
{ seconds: 15 },
|
|
];
|
|
function buildSlatMetas(slotMinTime, slotMaxTime, explicitLabelInterval, slotDuration, dateEnv) {
|
|
let dayStart = new Date(0);
|
|
let slatTime = slotMinTime;
|
|
let slatIterator = createDuration(0);
|
|
let labelInterval = explicitLabelInterval || computeLabelInterval(slotDuration);
|
|
let metas = [];
|
|
while (asRoughMs(slatTime) < asRoughMs(slotMaxTime)) {
|
|
let date = dateEnv.add(dayStart, slatTime);
|
|
let isLabeled = wholeDivideDurations(slatIterator, labelInterval) !== null;
|
|
metas.push({
|
|
date,
|
|
time: slatTime,
|
|
key: date.toISOString(),
|
|
isoTimeStr: formatIsoTimeString(date),
|
|
isLabeled,
|
|
});
|
|
slatTime = addDurations(slatTime, slotDuration);
|
|
slatIterator = addDurations(slatIterator, slotDuration);
|
|
}
|
|
return metas;
|
|
}
|
|
// Computes an automatic value for slotLabelInterval
|
|
function computeLabelInterval(slotDuration) {
|
|
let i;
|
|
let labelInterval;
|
|
let slotsPerLabel;
|
|
// find the smallest stock label interval that results in more than one slots-per-label
|
|
for (i = STOCK_SUB_DURATIONS.length - 1; i >= 0; i -= 1) {
|
|
labelInterval = createDuration(STOCK_SUB_DURATIONS[i]);
|
|
slotsPerLabel = wholeDivideDurations(labelInterval, slotDuration);
|
|
if (slotsPerLabel !== null && slotsPerLabel > 1) {
|
|
return labelInterval;
|
|
}
|
|
}
|
|
return slotDuration; // fall back
|
|
}
|
|
|
|
class DayTimeColsView extends TimeColsView {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buildTimeColsModel = memoize(buildTimeColsModel);
|
|
this.buildSlatMetas = memoize(buildSlatMetas);
|
|
}
|
|
render() {
|
|
let { options, dateEnv, dateProfileGenerator } = this.context;
|
|
let { props } = this;
|
|
let { dateProfile } = props;
|
|
let dayTableModel = this.buildTimeColsModel(dateProfile, dateProfileGenerator);
|
|
let splitProps = this.allDaySplitter.splitProps(props);
|
|
let slatMetas = this.buildSlatMetas(dateProfile.slotMinTime, dateProfile.slotMaxTime, options.slotLabelInterval, options.slotDuration, dateEnv);
|
|
let { dayMinWidth } = options;
|
|
let hasAttachedAxis = !dayMinWidth;
|
|
let hasDetachedAxis = dayMinWidth;
|
|
let headerContent = options.dayHeaders && (y(DayHeader, { dates: dayTableModel.headerDates, dateProfile: dateProfile, datesRepDistinctDays: true, renderIntro: hasAttachedAxis ? this.renderHeadAxis : null }));
|
|
let allDayContent = (options.allDaySlot !== false) && ((contentArg) => (y(DayTable, Object.assign({}, splitProps.allDay, { dateProfile: dateProfile, dayTableModel: dayTableModel, nextDayThreshold: options.nextDayThreshold, tableMinWidth: contentArg.tableMinWidth, colGroupNode: contentArg.tableColGroupNode, renderRowIntro: hasAttachedAxis ? this.renderTableRowAxis : null, showWeekNumbers: false, expandRows: false, headerAlignElRef: this.headerElRef, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, forPrint: props.forPrint }, this.getAllDayMaxEventProps()))));
|
|
let timeGridContent = (contentArg) => (y(DayTimeCols, Object.assign({}, splitProps.timed, { dayTableModel: dayTableModel, dateProfile: dateProfile, axis: hasAttachedAxis, slotDuration: options.slotDuration, slatMetas: slatMetas, forPrint: props.forPrint, tableColGroupNode: contentArg.tableColGroupNode, tableMinWidth: contentArg.tableMinWidth, clientWidth: contentArg.clientWidth, clientHeight: contentArg.clientHeight, onSlatCoords: this.handleSlatCoords, expandRows: contentArg.expandRows, onScrollTopRequest: this.handleScrollTopRequest })));
|
|
return hasDetachedAxis
|
|
? this.renderHScrollLayout(headerContent, allDayContent, timeGridContent, dayTableModel.colCnt, dayMinWidth, slatMetas, this.state.slatCoords)
|
|
: this.renderSimpleLayout(headerContent, allDayContent, timeGridContent);
|
|
}
|
|
}
|
|
function buildTimeColsModel(dateProfile, dateProfileGenerator) {
|
|
let daySeries = new DaySeriesModel(dateProfile.renderRange, dateProfileGenerator);
|
|
return new DayTableModel(daySeries, false);
|
|
}
|
|
|
|
var timegrid_internal_css_248z = ".fc-v-event{background-color:var(--fc-event-bg-color);border:1px solid var(--fc-event-border-color);display:block}.fc-v-event .fc-event-main{color:var(--fc-event-text-color);height:100%}.fc-v-event .fc-event-main-frame{display:flex;flex-direction:column;height:100%}.fc-v-event .fc-event-time{flex-grow:0;flex-shrink:0;max-height:100%;overflow:hidden}.fc-v-event .fc-event-title-container{flex-grow:1;flex-shrink:1;min-height:0}.fc-v-event .fc-event-title{bottom:0;max-height:100%;overflow:hidden;top:0}.fc-v-event:not(.fc-event-start){border-top-left-radius:0;border-top-right-radius:0;border-top-width:0}.fc-v-event:not(.fc-event-end){border-bottom-left-radius:0;border-bottom-right-radius:0;border-bottom-width:0}.fc-v-event.fc-event-selected:before{left:-10px;right:-10px}.fc-v-event .fc-event-resizer-start{cursor:n-resize}.fc-v-event .fc-event-resizer-end{cursor:s-resize}.fc-v-event:not(.fc-event-selected) .fc-event-resizer{height:var(--fc-event-resizer-thickness);left:0;right:0}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-start{top:calc(var(--fc-event-resizer-thickness)/-2)}.fc-v-event:not(.fc-event-selected) .fc-event-resizer-end{bottom:calc(var(--fc-event-resizer-thickness)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer{left:50%;margin-left:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer-start{top:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc-v-event.fc-event-selected .fc-event-resizer-end{bottom:calc(var(--fc-event-resizer-dot-total-width)/-2)}.fc .fc-timegrid .fc-daygrid-body{z-index:2}.fc .fc-timegrid-divider{padding:0 0 2px}.fc .fc-timegrid-body{min-height:100%;position:relative;z-index:1}.fc .fc-timegrid-axis-chunk{position:relative}.fc .fc-timegrid-axis-chunk>table,.fc .fc-timegrid-slots{position:relative;z-index:1}.fc .fc-timegrid-slot{border-bottom:0;height:1.5em}.fc .fc-timegrid-slot:empty:before{content:\"\\00a0\"}.fc .fc-timegrid-slot-minor{border-top-style:dotted}.fc .fc-timegrid-slot-label-cushion{display:inline-block;white-space:nowrap}.fc .fc-timegrid-slot-label{vertical-align:middle}.fc .fc-timegrid-axis-cushion,.fc .fc-timegrid-slot-label-cushion{padding:0 4px}.fc .fc-timegrid-axis-frame-liquid{height:100%}.fc .fc-timegrid-axis-frame{align-items:center;display:flex;justify-content:flex-end;overflow:hidden}.fc .fc-timegrid-axis-cushion{flex-shrink:0;max-width:60px}.fc-direction-ltr .fc-timegrid-slot-label-frame{text-align:right}.fc-direction-rtl .fc-timegrid-slot-label-frame{text-align:left}.fc-liquid-hack .fc-timegrid-axis-frame-liquid{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col.fc-day-today{background-color:var(--fc-today-bg-color)}.fc .fc-timegrid-col-frame{min-height:100%;position:relative}.fc-media-screen.fc-liquid-hack .fc-timegrid-col-frame{bottom:0;height:auto;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols{bottom:0;left:0;position:absolute;right:0;top:0}.fc-media-screen .fc-timegrid-cols>table{height:100%}.fc-media-screen .fc-timegrid-col-bg,.fc-media-screen .fc-timegrid-col-events,.fc-media-screen .fc-timegrid-now-indicator-container{left:0;position:absolute;right:0;top:0}.fc .fc-timegrid-col-bg{z-index:2}.fc .fc-timegrid-col-bg .fc-non-business{z-index:1}.fc .fc-timegrid-col-bg .fc-bg-event{z-index:2}.fc .fc-timegrid-col-bg .fc-highlight{z-index:3}.fc .fc-timegrid-bg-harness{left:0;position:absolute;right:0}.fc .fc-timegrid-col-events{z-index:3}.fc .fc-timegrid-now-indicator-container{bottom:0;overflow:hidden}.fc-direction-ltr .fc-timegrid-col-events{margin:0 2.5% 0 2px}.fc-direction-rtl .fc-timegrid-col-events{margin:0 2px 0 2.5%}.fc-timegrid-event-harness{position:absolute}.fc-timegrid-event-harness>.fc-timegrid-event{bottom:0;left:0;position:absolute;right:0;top:0}.fc-timegrid-event-harness-inset .fc-timegrid-event,.fc-timegrid-event.fc-event-mirror,.fc-timegrid-more-link{box-shadow:0 0 0 1px var(--fc-page-bg-color)}.fc-timegrid-event,.fc-timegrid-more-link{border-radius:3px;font-size:var(--fc-small-font-size)}.fc-timegrid-event{margin-bottom:1px}.fc-timegrid-event .fc-event-main{padding:1px 1px 0}.fc-timegrid-event .fc-event-time{font-size:var(--fc-small-font-size);margin-bottom:1px;white-space:nowrap}.fc-timegrid-event-short .fc-event-main-frame{flex-direction:row;overflow:hidden}.fc-timegrid-event-short .fc-event-time:after{content:\"\\00a0-\\00a0\"}.fc-timegrid-event-short .fc-event-title{font-size:var(--fc-small-font-size)}.fc-timegrid-more-link{background:var(--fc-more-link-bg-color);color:var(--fc-more-link-text-color);cursor:pointer;margin-bottom:1px;position:absolute;z-index:9999}.fc-timegrid-more-link-inner{padding:3px 2px;top:0}.fc-direction-ltr .fc-timegrid-more-link{right:0}.fc-direction-rtl .fc-timegrid-more-link{left:0}.fc .fc-timegrid-now-indicator-arrow,.fc .fc-timegrid-now-indicator-line{pointer-events:none}.fc .fc-timegrid-now-indicator-line{border-color:var(--fc-now-indicator-color);border-style:solid;border-width:1px 0 0;left:0;position:absolute;right:0;z-index:4}.fc .fc-timegrid-now-indicator-arrow{border-color:var(--fc-now-indicator-color);border-style:solid;margin-top:-5px;position:absolute;z-index:4}.fc-direction-ltr .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 0 5px 6px;left:0}.fc-direction-rtl .fc-timegrid-now-indicator-arrow{border-bottom-color:transparent;border-top-color:transparent;border-width:5px 6px 5px 0;right:0}";
|
|
injectStyles(timegrid_internal_css_248z);
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/timegrid/index.js
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const timegrid_OPTION_REFINERS = {
|
|
allDaySlot: Boolean,
|
|
};
|
|
|
|
var timegrid_index = createPlugin({
|
|
name: '@fullcalendar/timegrid',
|
|
initialView: 'timeGridWeek',
|
|
optionRefiners: timegrid_OPTION_REFINERS,
|
|
views: {
|
|
timeGrid: {
|
|
component: DayTimeColsView,
|
|
usesMinMaxTime: true,
|
|
allDaySlot: true,
|
|
slotDuration: '00:30:00',
|
|
slotEventOverlap: true, // a bad name. confused with overlap/constraint system
|
|
},
|
|
timeGridDay: {
|
|
type: 'timeGrid',
|
|
duration: { days: 1 },
|
|
},
|
|
timeGridWeek: {
|
|
type: 'timeGrid',
|
|
duration: { weeks: 1 },
|
|
},
|
|
},
|
|
});
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/list/internal.js
|
|
|
|
|
|
|
|
class ListViewHeaderRow extends BaseComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.state = {
|
|
textId: getUniqueDomId(),
|
|
};
|
|
}
|
|
render() {
|
|
let { theme, dateEnv, options, viewApi } = this.context;
|
|
let { cellId, dayDate, todayRange } = this.props;
|
|
let { textId } = this.state;
|
|
let dayMeta = getDateMeta(dayDate, todayRange);
|
|
// will ever be falsy?
|
|
let text = options.listDayFormat ? dateEnv.format(dayDate, options.listDayFormat) : '';
|
|
// will ever be falsy? also, BAD NAME "alt"
|
|
let sideText = options.listDaySideFormat ? dateEnv.format(dayDate, options.listDaySideFormat) : '';
|
|
let renderProps = Object.assign({ date: dateEnv.toDate(dayDate), view: viewApi, textId,
|
|
text,
|
|
sideText, navLinkAttrs: buildNavLinkAttrs(this.context, dayDate), sideNavLinkAttrs: buildNavLinkAttrs(this.context, dayDate, 'day', false) }, dayMeta);
|
|
// TODO: make a reusable HOC for dayHeader (used in daygrid/timegrid too)
|
|
return (y(ContentContainer, { elTag: "tr", elClasses: [
|
|
'fc-list-day',
|
|
...getDayClassNames(dayMeta, theme),
|
|
], elAttrs: {
|
|
'data-date': formatDayString(dayDate),
|
|
}, renderProps: renderProps, generatorName: "dayHeaderContent", customGenerator: options.dayHeaderContent, defaultGenerator: list_internal_renderInnerContent, classNameGenerator: options.dayHeaderClassNames, didMount: options.dayHeaderDidMount, willUnmount: options.dayHeaderWillUnmount }, (InnerContent) => ( // TODO: force-hide top border based on :first-child
|
|
y("th", { scope: "colgroup", colSpan: 3, id: cellId, "aria-labelledby": textId },
|
|
y(InnerContent, { elTag: "div", elClasses: [
|
|
'fc-list-day-cushion',
|
|
theme.getClass('tableCellShaded'),
|
|
] })))));
|
|
}
|
|
}
|
|
function list_internal_renderInnerContent(props) {
|
|
return (y(_, null,
|
|
props.text && (y("a", Object.assign({ id: props.textId, className: "fc-list-day-text" }, props.navLinkAttrs), props.text)),
|
|
props.sideText && ( /* not keyboard tabbable */y("a", Object.assign({ "aria-hidden": true, className: "fc-list-day-side-text" }, props.sideNavLinkAttrs), props.sideText))));
|
|
}
|
|
|
|
const internal_DEFAULT_TIME_FORMAT = createFormatter({
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
meridiem: 'short',
|
|
});
|
|
class ListViewEventRow extends BaseComponent {
|
|
render() {
|
|
let { props, context } = this;
|
|
let { options } = context;
|
|
let { seg, timeHeaderId, eventHeaderId, dateHeaderId } = props;
|
|
let timeFormat = options.eventTimeFormat || internal_DEFAULT_TIME_FORMAT;
|
|
return (y(EventContainer, Object.assign({}, props, { elTag: "tr", elClasses: [
|
|
'fc-list-event',
|
|
seg.eventRange.def.url && 'fc-event-forced-url',
|
|
], defaultGenerator: () => renderEventInnerContent(seg, context) /* weird */, seg: seg, timeText: "", disableDragging: true, disableResizing: true }), (InnerContent, eventContentArg) => (y(_, null,
|
|
buildTimeContent(seg, timeFormat, context, timeHeaderId, dateHeaderId),
|
|
y("td", { "aria-hidden": true, className: "fc-list-event-graphic" },
|
|
y("span", { className: "fc-list-event-dot", style: {
|
|
borderColor: eventContentArg.borderColor || eventContentArg.backgroundColor,
|
|
} })),
|
|
y(InnerContent, { elTag: "td", elClasses: ['fc-list-event-title'], elAttrs: { headers: `${eventHeaderId} ${dateHeaderId}` } })))));
|
|
}
|
|
}
|
|
function renderEventInnerContent(seg, context) {
|
|
let interactiveAttrs = getSegAnchorAttrs(seg, context);
|
|
return (y("a", Object.assign({}, interactiveAttrs), seg.eventRange.def.title));
|
|
}
|
|
function buildTimeContent(seg, timeFormat, context, timeHeaderId, dateHeaderId) {
|
|
let { options } = context;
|
|
if (options.displayEventTime !== false) {
|
|
let eventDef = seg.eventRange.def;
|
|
let eventInstance = seg.eventRange.instance;
|
|
let doAllDay = false;
|
|
let timeText;
|
|
if (eventDef.allDay) {
|
|
doAllDay = true;
|
|
}
|
|
else if (isMultiDayRange(seg.eventRange.range)) { // TODO: use (!isStart || !isEnd) instead?
|
|
if (seg.isStart) {
|
|
timeText = buildSegTimeText(seg, timeFormat, context, null, null, eventInstance.range.start, seg.end);
|
|
}
|
|
else if (seg.isEnd) {
|
|
timeText = buildSegTimeText(seg, timeFormat, context, null, null, seg.start, eventInstance.range.end);
|
|
}
|
|
else {
|
|
doAllDay = true;
|
|
}
|
|
}
|
|
else {
|
|
timeText = buildSegTimeText(seg, timeFormat, context);
|
|
}
|
|
if (doAllDay) {
|
|
let renderProps = {
|
|
text: context.options.allDayText,
|
|
view: context.viewApi,
|
|
};
|
|
return (y(ContentContainer, { elTag: "td", elClasses: ['fc-list-event-time'], elAttrs: {
|
|
headers: `${timeHeaderId} ${dateHeaderId}`,
|
|
}, renderProps: renderProps, generatorName: "allDayContent", customGenerator: options.allDayContent, defaultGenerator: internal_renderAllDayInner, classNameGenerator: options.allDayClassNames, didMount: options.allDayDidMount, willUnmount: options.allDayWillUnmount }));
|
|
}
|
|
return (y("td", { className: "fc-list-event-time" }, timeText));
|
|
}
|
|
return null;
|
|
}
|
|
function internal_renderAllDayInner(renderProps) {
|
|
return renderProps.text;
|
|
}
|
|
|
|
/*
|
|
Responsible for the scroller, and forwarding event-related actions into the "grid".
|
|
*/
|
|
class ListView extends DateComponent {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.computeDateVars = memoize(computeDateVars);
|
|
this.eventStoreToSegs = memoize(this._eventStoreToSegs);
|
|
this.state = {
|
|
timeHeaderId: getUniqueDomId(),
|
|
eventHeaderId: getUniqueDomId(),
|
|
dateHeaderIdRoot: getUniqueDomId(),
|
|
};
|
|
this.setRootEl = (rootEl) => {
|
|
if (rootEl) {
|
|
this.context.registerInteractiveComponent(this, {
|
|
el: rootEl,
|
|
});
|
|
}
|
|
else {
|
|
this.context.unregisterInteractiveComponent(this);
|
|
}
|
|
};
|
|
}
|
|
render() {
|
|
let { props, context } = this;
|
|
let { dayDates, dayRanges } = this.computeDateVars(props.dateProfile);
|
|
let eventSegs = this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges);
|
|
return (y(ViewContainer, { elRef: this.setRootEl, elClasses: [
|
|
'fc-list',
|
|
context.theme.getClass('table'),
|
|
context.options.stickyHeaderDates !== false ?
|
|
'fc-list-sticky' :
|
|
'',
|
|
], viewSpec: context.viewSpec },
|
|
y(Scroller, { liquid: !props.isHeightAuto, overflowX: props.isHeightAuto ? 'visible' : 'hidden', overflowY: props.isHeightAuto ? 'visible' : 'auto' }, eventSegs.length > 0 ?
|
|
this.renderSegList(eventSegs, dayDates) :
|
|
this.renderEmptyMessage())));
|
|
}
|
|
renderEmptyMessage() {
|
|
let { options, viewApi } = this.context;
|
|
let renderProps = {
|
|
text: options.noEventsText,
|
|
view: viewApi,
|
|
};
|
|
return (y(ContentContainer, { elTag: "div", elClasses: ['fc-list-empty'], renderProps: renderProps, generatorName: "noEventsContent", customGenerator: options.noEventsContent, defaultGenerator: renderNoEventsInner, classNameGenerator: options.noEventsClassNames, didMount: options.noEventsDidMount, willUnmount: options.noEventsWillUnmount }, (InnerContent) => (y(InnerContent, { elTag: "div", elClasses: ['fc-list-empty-cushion'] }))));
|
|
}
|
|
renderSegList(allSegs, dayDates) {
|
|
let { theme, options } = this.context;
|
|
let { timeHeaderId, eventHeaderId, dateHeaderIdRoot } = this.state;
|
|
let segsByDay = groupSegsByDay(allSegs); // sparse array
|
|
return (y(NowTimer, { unit: "day" }, (nowDate, todayRange) => {
|
|
let innerNodes = [];
|
|
for (let dayIndex = 0; dayIndex < segsByDay.length; dayIndex += 1) {
|
|
let daySegs = segsByDay[dayIndex];
|
|
if (daySegs) { // sparse array, so might be undefined
|
|
let dayStr = formatDayString(dayDates[dayIndex]);
|
|
let dateHeaderId = dateHeaderIdRoot + '-' + dayStr;
|
|
// append a day header
|
|
innerNodes.push(y(ListViewHeaderRow, { key: dayStr, cellId: dateHeaderId, dayDate: dayDates[dayIndex], todayRange: todayRange }));
|
|
daySegs = sortEventSegs(daySegs, options.eventOrder);
|
|
for (let seg of daySegs) {
|
|
innerNodes.push(y(ListViewEventRow, Object.assign({ key: dayStr + ':' + seg.eventRange.instance.instanceId /* are multiple segs for an instanceId */, seg: seg, isDragging: false, isResizing: false, isDateSelecting: false, isSelected: false, timeHeaderId: timeHeaderId, eventHeaderId: eventHeaderId, dateHeaderId: dateHeaderId }, getSegMeta(seg, todayRange, nowDate))));
|
|
}
|
|
}
|
|
}
|
|
return (y("table", { className: 'fc-list-table ' + theme.getClass('table') },
|
|
y("thead", null,
|
|
y("tr", null,
|
|
y("th", { scope: "col", id: timeHeaderId }, options.timeHint),
|
|
y("th", { scope: "col", "aria-hidden": true }),
|
|
y("th", { scope: "col", id: eventHeaderId }, options.eventHint))),
|
|
y("tbody", null, innerNodes)));
|
|
}));
|
|
}
|
|
_eventStoreToSegs(eventStore, eventUiBases, dayRanges) {
|
|
return this.eventRangesToSegs(sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.context.options.nextDayThreshold).fg, dayRanges);
|
|
}
|
|
eventRangesToSegs(eventRanges, dayRanges) {
|
|
let segs = [];
|
|
for (let eventRange of eventRanges) {
|
|
segs.push(...this.eventRangeToSegs(eventRange, dayRanges));
|
|
}
|
|
return segs;
|
|
}
|
|
eventRangeToSegs(eventRange, dayRanges) {
|
|
let { dateEnv } = this.context;
|
|
let { nextDayThreshold } = this.context.options;
|
|
let range = eventRange.range;
|
|
let allDay = eventRange.def.allDay;
|
|
let dayIndex;
|
|
let segRange;
|
|
let seg;
|
|
let segs = [];
|
|
for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex += 1) {
|
|
segRange = intersectRanges(range, dayRanges[dayIndex]);
|
|
if (segRange) {
|
|
seg = {
|
|
component: this,
|
|
eventRange,
|
|
start: segRange.start,
|
|
end: segRange.end,
|
|
isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(),
|
|
isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(),
|
|
dayIndex,
|
|
};
|
|
segs.push(seg);
|
|
// detect when range won't go fully into the next day,
|
|
// and mutate the latest seg to the be the end.
|
|
if (!seg.isEnd && !allDay &&
|
|
dayIndex + 1 < dayRanges.length &&
|
|
range.end <
|
|
dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) {
|
|
seg.end = range.end;
|
|
seg.isEnd = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return segs;
|
|
}
|
|
}
|
|
function renderNoEventsInner(renderProps) {
|
|
return renderProps.text;
|
|
}
|
|
function computeDateVars(dateProfile) {
|
|
let dayStart = startOfDay(dateProfile.renderRange.start);
|
|
let viewEnd = dateProfile.renderRange.end;
|
|
let dayDates = [];
|
|
let dayRanges = [];
|
|
while (dayStart < viewEnd) {
|
|
dayDates.push(dayStart);
|
|
dayRanges.push({
|
|
start: dayStart,
|
|
end: addDays(dayStart, 1),
|
|
});
|
|
dayStart = addDays(dayStart, 1);
|
|
}
|
|
return { dayDates, dayRanges };
|
|
}
|
|
// Returns a sparse array of arrays, segs grouped by their dayIndex
|
|
function groupSegsByDay(segs) {
|
|
let segsByDay = []; // sparse array
|
|
let i;
|
|
let seg;
|
|
for (i = 0; i < segs.length; i += 1) {
|
|
seg = segs[i];
|
|
(segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
|
|
.push(seg);
|
|
}
|
|
return segsByDay;
|
|
}
|
|
|
|
var list_internal_css_248z = ":root{--fc-list-event-dot-width:10px;--fc-list-event-hover-bg-color:#f5f5f5}.fc-theme-standard .fc-list{border:1px solid var(--fc-border-color)}.fc .fc-list-empty{align-items:center;background-color:var(--fc-neutral-bg-color);display:flex;height:100%;justify-content:center}.fc .fc-list-empty-cushion{margin:5em 0}.fc .fc-list-table{border-style:hidden;width:100%}.fc .fc-list-table tr>*{border-left:0;border-right:0}.fc .fc-list-sticky .fc-list-day>*{background:var(--fc-page-bg-color);position:sticky;top:0}.fc .fc-list-table thead{left:-10000px;position:absolute}.fc .fc-list-table tbody>tr:first-child th{border-top:0}.fc .fc-list-table th{padding:0}.fc .fc-list-day-cushion,.fc .fc-list-table td{padding:8px 14px}.fc .fc-list-day-cushion:after{clear:both;content:\"\";display:table}.fc-theme-standard .fc-list-day-cushion{background-color:var(--fc-neutral-bg-color)}.fc-direction-ltr .fc-list-day-text,.fc-direction-rtl .fc-list-day-side-text{float:left}.fc-direction-ltr .fc-list-day-side-text,.fc-direction-rtl .fc-list-day-text{float:right}.fc-direction-ltr .fc-list-table .fc-list-event-graphic{padding-right:0}.fc-direction-rtl .fc-list-table .fc-list-event-graphic{padding-left:0}.fc .fc-list-event.fc-event-forced-url{cursor:pointer}.fc .fc-list-event:hover td{background-color:var(--fc-list-event-hover-bg-color)}.fc .fc-list-event-graphic,.fc .fc-list-event-time{white-space:nowrap;width:1px}.fc .fc-list-event-dot{border:calc(var(--fc-list-event-dot-width)/2) solid var(--fc-event-border-color);border-radius:calc(var(--fc-list-event-dot-width)/2);box-sizing:content-box;display:inline-block;height:0;width:0}.fc .fc-list-event-title a{color:inherit;text-decoration:none}.fc .fc-list-event.fc-event-forced-url:hover a{text-decoration:underline}";
|
|
injectStyles(list_internal_css_248z);
|
|
|
|
|
|
|
|
;// ../../node_modules/@fullcalendar/list/index.js
|
|
|
|
|
|
|
|
|
|
|
|
const list_OPTION_REFINERS = {
|
|
listDayFormat: createFalsableFormatter,
|
|
listDaySideFormat: createFalsableFormatter,
|
|
noEventsClassNames: identity,
|
|
noEventsContent: identity,
|
|
noEventsDidMount: identity,
|
|
noEventsWillUnmount: identity,
|
|
// noEventsText is defined in base options
|
|
};
|
|
function createFalsableFormatter(input) {
|
|
return input === false ? null : createFormatter(input);
|
|
}
|
|
|
|
var list_index = createPlugin({
|
|
name: '@fullcalendar/list',
|
|
optionRefiners: list_OPTION_REFINERS,
|
|
views: {
|
|
list: {
|
|
component: ListView,
|
|
buttonTextKey: 'list',
|
|
listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' }, // like "January 1, 2016"
|
|
},
|
|
listDay: {
|
|
type: 'list',
|
|
duration: { days: 1 },
|
|
listDayFormat: { weekday: 'long' }, // day-of-week is all we need. full date is probably in headerToolbar
|
|
},
|
|
listWeek: {
|
|
type: 'list',
|
|
duration: { weeks: 1 },
|
|
listDayFormat: { weekday: 'long' },
|
|
listDaySideFormat: { month: 'long', day: 'numeric', year: 'numeric' },
|
|
},
|
|
listMonth: {
|
|
type: 'list',
|
|
duration: { month: 1 },
|
|
listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have
|
|
},
|
|
listYear: {
|
|
type: 'list',
|
|
duration: { year: 1 },
|
|
listDaySideFormat: { weekday: 'long' }, // day-of-week is nice-to-have
|
|
},
|
|
},
|
|
});
|
|
|
|
|
|
|
|
;// ../assets/scripts/fullcalendar/index.js
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const calendarEl = document.getElementById('calendar');
|
|
|
|
// element found in dom ?
|
|
if (calendarEl == null) {
|
|
return;
|
|
}
|
|
const calendar = new Calendar(calendarEl, {
|
|
plugins: [interaction_index, daygrid_index, timegrid_index, list_index],
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
|
},
|
|
initialDate: date.format(date.now(), 'YYYY-MM-DD'),
|
|
navLinks: true,
|
|
// can click day/week names to navigate views
|
|
editable: true,
|
|
dayMaxEvents: true,
|
|
// allow "more" link when too many events
|
|
events: [{
|
|
title: 'All Day Event',
|
|
start: date.format(date.now(), 'YYYY-MM-DD')
|
|
}, {
|
|
title: 'Long Event',
|
|
start: date.format(date.add(date.now(), 1, 'day'), 'YYYY-MM-DD'),
|
|
end: date.format(date.add(date.now(), 4, 'day'), 'YYYY-MM-DD')
|
|
}, {
|
|
groupId: 999,
|
|
title: 'Repeating Event',
|
|
start: date.format(date.add(date.now(), 2, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T16:00')
|
|
}, {
|
|
groupId: 999,
|
|
title: 'Repeating Event',
|
|
start: date.format(date.add(date.now(), 9, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T16:00')
|
|
}, {
|
|
title: 'Conference',
|
|
start: date.format(date.add(date.now(), 5, 'day'), 'YYYY-MM-DD'),
|
|
end: date.format(date.add(date.now(), 7, 'day'), 'YYYY-MM-DD')
|
|
}, {
|
|
title: 'Meeting',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T10:30'),
|
|
end: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T12:30')
|
|
}, {
|
|
title: 'Lunch',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T12:00')
|
|
}, {
|
|
title: 'Meeting',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T14:30')
|
|
}, {
|
|
title: 'Happy Hour',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T17:30')
|
|
}, {
|
|
title: 'Dinner',
|
|
start: date.format(date.add(date.now(), 3, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T20:00')
|
|
}, {
|
|
title: 'Birthday Party',
|
|
start: date.format(date.add(date.now(), 4, 'day'), 'YYYY-MM-DDTHH:mm:ss').replace(/:\d{2}$/, ':00:00').replace(/T\d{2}:\d{2}/, 'T07:00')
|
|
}, {
|
|
title: 'Click for Google',
|
|
url: 'http://google.com/',
|
|
start: date.format(date.add(date.now(), 14, 'day'), 'YYYY-MM-DD')
|
|
}]
|
|
});
|
|
calendar.render();
|
|
});
|
|
// EXTERNAL MODULE: ../../node_modules/masonry-layout/masonry.js
|
|
var masonry = __webpack_require__(201);
|
|
var masonry_default = /*#__PURE__*/__webpack_require__.n(masonry);
|
|
;// ../assets/scripts/masonry/index.js
|
|
|
|
/* harmony default export */ const scripts_masonry = ((function () {
|
|
window.addEventListener('load', () => {
|
|
const masonryElement = document.querySelector('.masonry');
|
|
if (masonryElement) {
|
|
new (masonry_default())(masonryElement, {
|
|
itemSelector: '.masonry-item',
|
|
columnWidth: '.masonry-sizer',
|
|
percentPosition: true
|
|
});
|
|
}
|
|
});
|
|
})());
|
|
;// ../assets/scripts/popover/index.js
|
|
// Simple vanilla JS tooltip and popover implementation
|
|
/* harmony default export */ const popover = ((function () {
|
|
// Simple tooltip implementation
|
|
function initTooltips() {
|
|
const tooltipElements = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
tooltipElements.forEach(element => {
|
|
const tooltipText = element.getAttribute('data-bs-title') || element.getAttribute('title');
|
|
if (tooltipText) {
|
|
element.addEventListener('mouseenter', function () {
|
|
const tooltip = document.createElement('div');
|
|
tooltip.className = 'custom-tooltip';
|
|
tooltip.textContent = tooltipText;
|
|
tooltip.style.cssText = `
|
|
position: absolute;
|
|
background: #000;
|
|
color: #fff;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
z-index: 1050;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
`;
|
|
document.body.appendChild(tooltip);
|
|
const rect = element.getBoundingClientRect();
|
|
tooltip.style.left = `${rect.left + rect.width / 2 - tooltip.offsetWidth / 2}px`;
|
|
tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`;
|
|
element._tooltip = tooltip;
|
|
});
|
|
element.addEventListener('mouseleave', function () {
|
|
if (element._tooltip) {
|
|
element._tooltip.remove();
|
|
element._tooltip = null;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Simple popover implementation
|
|
function initPopovers() {
|
|
const popoverElements = document.querySelectorAll('[data-bs-toggle="popover"]');
|
|
popoverElements.forEach(element => {
|
|
const popoverContent = element.getAttribute('data-bs-content');
|
|
const popoverTitle = element.getAttribute('data-bs-title');
|
|
if (popoverContent) {
|
|
element.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
|
|
// Remove existing popover
|
|
if (element._popover) {
|
|
element._popover.remove();
|
|
element._popover = null;
|
|
return;
|
|
}
|
|
const popover = document.createElement('div');
|
|
popover.className = 'custom-popover';
|
|
popover.innerHTML = `
|
|
${popoverTitle ? `<div class="popover-title">${popoverTitle}</div>` : ''}
|
|
<div class="popover-content">${popoverContent}</div>
|
|
`;
|
|
popover.style.cssText = `
|
|
position: absolute;
|
|
background: #fff;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
z-index: 1050;
|
|
min-width: 200px;
|
|
max-width: 300px;
|
|
`;
|
|
document.body.appendChild(popover);
|
|
const rect = element.getBoundingClientRect();
|
|
popover.style.left = `${rect.left}px`;
|
|
popover.style.top = `${rect.bottom + 5}px`;
|
|
element._popover = popover;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize both
|
|
initTooltips();
|
|
initPopovers();
|
|
|
|
// Close popovers when clicking outside
|
|
document.addEventListener('click', function (e) {
|
|
const popovers = document.querySelectorAll('.custom-popover');
|
|
popovers.forEach(popover => {
|
|
if (!popover.contains(e.target)) {
|
|
popover.remove();
|
|
}
|
|
});
|
|
});
|
|
})());
|
|
;// ../../node_modules/perfect-scrollbar/dist/perfect-scrollbar.esm.js
|
|
/*!
|
|
* perfect-scrollbar v1.5.6
|
|
* Copyright 2024 Hyunje Jun, MDBootstrap and Contributors
|
|
* Licensed under MIT
|
|
*/
|
|
|
|
function get(element) {
|
|
return getComputedStyle(element);
|
|
}
|
|
|
|
function perfect_scrollbar_esm_set(element, obj) {
|
|
for (var key in obj) {
|
|
var val = obj[key];
|
|
if (typeof val === 'number') {
|
|
val = val + "px";
|
|
}
|
|
element.style[key] = val;
|
|
}
|
|
return element;
|
|
}
|
|
|
|
function div(className) {
|
|
var div = document.createElement('div');
|
|
div.className = className;
|
|
return div;
|
|
}
|
|
|
|
var elMatches =
|
|
typeof Element !== 'undefined' &&
|
|
(Element.prototype.matches ||
|
|
Element.prototype.webkitMatchesSelector ||
|
|
Element.prototype.mozMatchesSelector ||
|
|
Element.prototype.msMatchesSelector);
|
|
|
|
function matches(element, query) {
|
|
if (!elMatches) {
|
|
throw new Error('No element matching method supported');
|
|
}
|
|
|
|
return elMatches.call(element, query);
|
|
}
|
|
|
|
function remove(element) {
|
|
if (element.remove) {
|
|
element.remove();
|
|
} else {
|
|
if (element.parentNode) {
|
|
element.parentNode.removeChild(element);
|
|
}
|
|
}
|
|
}
|
|
|
|
function queryChildren(element, selector) {
|
|
return Array.prototype.filter.call(element.children, function (child) { return matches(child, selector); }
|
|
);
|
|
}
|
|
|
|
var cls = {
|
|
main: 'ps',
|
|
rtl: 'ps__rtl',
|
|
element: {
|
|
thumb: function (x) { return ("ps__thumb-" + x); },
|
|
rail: function (x) { return ("ps__rail-" + x); },
|
|
consuming: 'ps__child--consume',
|
|
},
|
|
state: {
|
|
focus: 'ps--focus',
|
|
clicking: 'ps--clicking',
|
|
active: function (x) { return ("ps--active-" + x); },
|
|
scrolling: function (x) { return ("ps--scrolling-" + x); },
|
|
},
|
|
};
|
|
|
|
/*
|
|
* Helper methods
|
|
*/
|
|
var scrollingClassTimeout = { x: null, y: null };
|
|
|
|
function addScrollingClass(i, x) {
|
|
var classList = i.element.classList;
|
|
var className = cls.state.scrolling(x);
|
|
|
|
if (classList.contains(className)) {
|
|
clearTimeout(scrollingClassTimeout[x]);
|
|
} else {
|
|
classList.add(className);
|
|
}
|
|
}
|
|
|
|
function removeScrollingClass(i, x) {
|
|
scrollingClassTimeout[x] = setTimeout(
|
|
function () { return i.isAlive && i.element.classList.remove(cls.state.scrolling(x)); },
|
|
i.settings.scrollingThreshold
|
|
);
|
|
}
|
|
|
|
function setScrollingClassInstantly(i, x) {
|
|
addScrollingClass(i, x);
|
|
removeScrollingClass(i, x);
|
|
}
|
|
|
|
var EventElement = function EventElement(element) {
|
|
this.element = element;
|
|
this.handlers = {};
|
|
};
|
|
|
|
var prototypeAccessors = { isEmpty: { configurable: true } };
|
|
|
|
EventElement.prototype.bind = function bind (eventName, handler) {
|
|
if (typeof this.handlers[eventName] === 'undefined') {
|
|
this.handlers[eventName] = [];
|
|
}
|
|
this.handlers[eventName].push(handler);
|
|
this.element.addEventListener(eventName, handler, false);
|
|
};
|
|
|
|
EventElement.prototype.unbind = function unbind (eventName, target) {
|
|
var this$1 = this;
|
|
|
|
this.handlers[eventName] = this.handlers[eventName].filter(function (handler) {
|
|
if (target && handler !== target) {
|
|
return true;
|
|
}
|
|
this$1.element.removeEventListener(eventName, handler, false);
|
|
return false;
|
|
});
|
|
};
|
|
|
|
EventElement.prototype.unbindAll = function unbindAll () {
|
|
for (var name in this.handlers) {
|
|
this.unbind(name);
|
|
}
|
|
};
|
|
|
|
prototypeAccessors.isEmpty.get = function () {
|
|
var this$1 = this;
|
|
|
|
return Object.keys(this.handlers).every(
|
|
function (key) { return this$1.handlers[key].length === 0; }
|
|
);
|
|
};
|
|
|
|
Object.defineProperties( EventElement.prototype, prototypeAccessors );
|
|
|
|
var EventManager = function EventManager() {
|
|
this.eventElements = [];
|
|
};
|
|
|
|
EventManager.prototype.eventElement = function eventElement (element) {
|
|
var ee = this.eventElements.filter(function (ee) { return ee.element === element; })[0];
|
|
if (!ee) {
|
|
ee = new EventElement(element);
|
|
this.eventElements.push(ee);
|
|
}
|
|
return ee;
|
|
};
|
|
|
|
EventManager.prototype.bind = function bind (element, eventName, handler) {
|
|
this.eventElement(element).bind(eventName, handler);
|
|
};
|
|
|
|
EventManager.prototype.unbind = function unbind (element, eventName, handler) {
|
|
var ee = this.eventElement(element);
|
|
ee.unbind(eventName, handler);
|
|
|
|
if (ee.isEmpty) {
|
|
// remove
|
|
this.eventElements.splice(this.eventElements.indexOf(ee), 1);
|
|
}
|
|
};
|
|
|
|
EventManager.prototype.unbindAll = function unbindAll () {
|
|
this.eventElements.forEach(function (e) { return e.unbindAll(); });
|
|
this.eventElements = [];
|
|
};
|
|
|
|
EventManager.prototype.once = function once (element, eventName, handler) {
|
|
var ee = this.eventElement(element);
|
|
var onceHandler = function (evt) {
|
|
ee.unbind(eventName, onceHandler);
|
|
handler(evt);
|
|
};
|
|
ee.bind(eventName, onceHandler);
|
|
};
|
|
|
|
function createEvent(name) {
|
|
if (typeof window.CustomEvent === 'function') {
|
|
return new CustomEvent(name);
|
|
}
|
|
|
|
var evt = document.createEvent('CustomEvent');
|
|
evt.initCustomEvent(name, false, false, undefined);
|
|
return evt;
|
|
}
|
|
|
|
function processScrollDiff (i, axis, diff, useScrollingClass, forceFireReachEvent) {
|
|
if ( useScrollingClass === void 0 ) useScrollingClass = true;
|
|
if ( forceFireReachEvent === void 0 ) forceFireReachEvent = false;
|
|
|
|
var fields;
|
|
if (axis === 'top') {
|
|
fields = ['contentHeight', 'containerHeight', 'scrollTop', 'y', 'up', 'down'];
|
|
} else if (axis === 'left') {
|
|
fields = ['contentWidth', 'containerWidth', 'scrollLeft', 'x', 'left', 'right'];
|
|
} else {
|
|
throw new Error('A proper axis should be provided');
|
|
}
|
|
|
|
processScrollDiff$1(i, diff, fields, useScrollingClass, forceFireReachEvent);
|
|
}
|
|
|
|
function processScrollDiff$1(
|
|
i,
|
|
diff,
|
|
ref,
|
|
useScrollingClass,
|
|
forceFireReachEvent
|
|
) {
|
|
var contentHeight = ref[0];
|
|
var containerHeight = ref[1];
|
|
var scrollTop = ref[2];
|
|
var y = ref[3];
|
|
var up = ref[4];
|
|
var down = ref[5];
|
|
if ( useScrollingClass === void 0 ) useScrollingClass = true;
|
|
if ( forceFireReachEvent === void 0 ) forceFireReachEvent = false;
|
|
|
|
var element = i.element;
|
|
|
|
// reset reach
|
|
i.reach[y] = null;
|
|
|
|
// 1 for subpixel rounding
|
|
if (element[scrollTop] < 1) {
|
|
i.reach[y] = 'start';
|
|
}
|
|
|
|
// 1 for subpixel rounding
|
|
if (element[scrollTop] > i[contentHeight] - i[containerHeight] - 1) {
|
|
i.reach[y] = 'end';
|
|
}
|
|
|
|
if (diff) {
|
|
element.dispatchEvent(createEvent(("ps-scroll-" + y)));
|
|
|
|
if (diff < 0) {
|
|
element.dispatchEvent(createEvent(("ps-scroll-" + up)));
|
|
} else if (diff > 0) {
|
|
element.dispatchEvent(createEvent(("ps-scroll-" + down)));
|
|
}
|
|
|
|
if (useScrollingClass) {
|
|
setScrollingClassInstantly(i, y);
|
|
}
|
|
}
|
|
|
|
if (i.reach[y] && (diff || forceFireReachEvent)) {
|
|
element.dispatchEvent(createEvent(("ps-" + y + "-reach-" + (i.reach[y]))));
|
|
}
|
|
}
|
|
|
|
function toInt(x) {
|
|
return parseInt(x, 10) || 0;
|
|
}
|
|
|
|
function isEditable(el) {
|
|
return (
|
|
matches(el, 'input,[contenteditable]') ||
|
|
matches(el, 'select,[contenteditable]') ||
|
|
matches(el, 'textarea,[contenteditable]') ||
|
|
matches(el, 'button,[contenteditable]')
|
|
);
|
|
}
|
|
|
|
function perfect_scrollbar_esm_outerWidth(element) {
|
|
var styles = get(element);
|
|
return (
|
|
toInt(styles.width) +
|
|
toInt(styles.paddingLeft) +
|
|
toInt(styles.paddingRight) +
|
|
toInt(styles.borderLeftWidth) +
|
|
toInt(styles.borderRightWidth)
|
|
);
|
|
}
|
|
|
|
var env = {
|
|
isWebKit:
|
|
typeof document !== 'undefined' &&
|
|
'WebkitAppearance' in document.documentElement.style,
|
|
supportsTouch:
|
|
typeof window !== 'undefined' &&
|
|
('ontouchstart' in window ||
|
|
('maxTouchPoints' in window.navigator &&
|
|
window.navigator.maxTouchPoints > 0) ||
|
|
(window.DocumentTouch && document instanceof window.DocumentTouch)),
|
|
supportsIePointer:
|
|
typeof navigator !== 'undefined' && navigator.msMaxTouchPoints,
|
|
isChrome:
|
|
typeof navigator !== 'undefined' &&
|
|
/Chrome/i.test(navigator && navigator.userAgent),
|
|
};
|
|
|
|
/* eslint-disable no-lonely-if */
|
|
|
|
function updateGeometry (i) {
|
|
var element = i.element;
|
|
var roundedScrollTop = Math.floor(element.scrollTop);
|
|
var rect = element.getBoundingClientRect();
|
|
|
|
i.containerWidth = Math.floor(rect.width);
|
|
i.containerHeight = Math.floor(rect.height);
|
|
|
|
i.contentWidth = element.scrollWidth;
|
|
i.contentHeight = element.scrollHeight;
|
|
|
|
if (!element.contains(i.scrollbarXRail)) {
|
|
// clean up and append
|
|
queryChildren(element, cls.element.rail('x')).forEach(function (el) { return remove(el); });
|
|
element.appendChild(i.scrollbarXRail);
|
|
}
|
|
if (!element.contains(i.scrollbarYRail)) {
|
|
// clean up and append
|
|
queryChildren(element, cls.element.rail('y')).forEach(function (el) { return remove(el); });
|
|
element.appendChild(i.scrollbarYRail);
|
|
}
|
|
|
|
if (
|
|
!i.settings.suppressScrollX &&
|
|
i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth
|
|
) {
|
|
i.scrollbarXActive = true;
|
|
i.railXWidth = i.containerWidth - i.railXMarginWidth;
|
|
i.railXRatio = i.containerWidth / i.railXWidth;
|
|
i.scrollbarXWidth = getThumbSize(i, toInt((i.railXWidth * i.containerWidth) / i.contentWidth));
|
|
i.scrollbarXLeft = toInt(
|
|
((i.negativeScrollAdjustment + element.scrollLeft) * (i.railXWidth - i.scrollbarXWidth)) /
|
|
(i.contentWidth - i.containerWidth)
|
|
);
|
|
} else {
|
|
i.scrollbarXActive = false;
|
|
}
|
|
|
|
if (
|
|
!i.settings.suppressScrollY &&
|
|
i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight
|
|
) {
|
|
i.scrollbarYActive = true;
|
|
i.railYHeight = i.containerHeight - i.railYMarginHeight;
|
|
i.railYRatio = i.containerHeight / i.railYHeight;
|
|
i.scrollbarYHeight = getThumbSize(
|
|
i,
|
|
toInt((i.railYHeight * i.containerHeight) / i.contentHeight)
|
|
);
|
|
i.scrollbarYTop = toInt(
|
|
(roundedScrollTop * (i.railYHeight - i.scrollbarYHeight)) /
|
|
(i.contentHeight - i.containerHeight)
|
|
);
|
|
} else {
|
|
i.scrollbarYActive = false;
|
|
}
|
|
|
|
if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) {
|
|
i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth;
|
|
}
|
|
if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) {
|
|
i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight;
|
|
}
|
|
|
|
updateCss(element, i);
|
|
|
|
if (i.scrollbarXActive) {
|
|
element.classList.add(cls.state.active('x'));
|
|
} else {
|
|
element.classList.remove(cls.state.active('x'));
|
|
i.scrollbarXWidth = 0;
|
|
i.scrollbarXLeft = 0;
|
|
element.scrollLeft = i.isRtl === true ? i.contentWidth : 0;
|
|
}
|
|
if (i.scrollbarYActive) {
|
|
element.classList.add(cls.state.active('y'));
|
|
} else {
|
|
element.classList.remove(cls.state.active('y'));
|
|
i.scrollbarYHeight = 0;
|
|
i.scrollbarYTop = 0;
|
|
element.scrollTop = 0;
|
|
}
|
|
}
|
|
|
|
function getThumbSize(i, thumbSize) {
|
|
if (i.settings.minScrollbarLength) {
|
|
thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength);
|
|
}
|
|
if (i.settings.maxScrollbarLength) {
|
|
thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength);
|
|
}
|
|
return thumbSize;
|
|
}
|
|
|
|
function updateCss(element, i) {
|
|
var xRailOffset = { width: i.railXWidth };
|
|
var roundedScrollTop = Math.floor(element.scrollTop);
|
|
|
|
if (i.isRtl) {
|
|
xRailOffset.left =
|
|
i.negativeScrollAdjustment + element.scrollLeft + i.containerWidth - i.contentWidth;
|
|
} else {
|
|
xRailOffset.left = element.scrollLeft;
|
|
}
|
|
if (i.isScrollbarXUsingBottom) {
|
|
xRailOffset.bottom = i.scrollbarXBottom - roundedScrollTop;
|
|
} else {
|
|
xRailOffset.top = i.scrollbarXTop + roundedScrollTop;
|
|
}
|
|
perfect_scrollbar_esm_set(i.scrollbarXRail, xRailOffset);
|
|
|
|
var yRailOffset = { top: roundedScrollTop, height: i.railYHeight };
|
|
if (i.isScrollbarYUsingRight) {
|
|
if (i.isRtl) {
|
|
yRailOffset.right =
|
|
i.contentWidth -
|
|
(i.negativeScrollAdjustment + element.scrollLeft) -
|
|
i.scrollbarYRight -
|
|
i.scrollbarYOuterWidth -
|
|
9;
|
|
} else {
|
|
yRailOffset.right = i.scrollbarYRight - element.scrollLeft;
|
|
}
|
|
} else {
|
|
if (i.isRtl) {
|
|
yRailOffset.left =
|
|
i.negativeScrollAdjustment +
|
|
element.scrollLeft +
|
|
i.containerWidth * 2 -
|
|
i.contentWidth -
|
|
i.scrollbarYLeft -
|
|
i.scrollbarYOuterWidth;
|
|
} else {
|
|
yRailOffset.left = i.scrollbarYLeft + element.scrollLeft;
|
|
}
|
|
}
|
|
perfect_scrollbar_esm_set(i.scrollbarYRail, yRailOffset);
|
|
|
|
perfect_scrollbar_esm_set(i.scrollbarX, {
|
|
left: i.scrollbarXLeft,
|
|
width: i.scrollbarXWidth - i.railBorderXWidth,
|
|
});
|
|
perfect_scrollbar_esm_set(i.scrollbarY, {
|
|
top: i.scrollbarYTop,
|
|
height: i.scrollbarYHeight - i.railBorderYWidth,
|
|
});
|
|
}
|
|
|
|
/* eslint-disable */
|
|
|
|
function clickRail (i) {
|
|
// const element = i.element;
|
|
|
|
i.event.bind(i.scrollbarY, 'mousedown', function (e) { return e.stopPropagation(); });
|
|
i.event.bind(i.scrollbarYRail, 'mousedown', function (e) {
|
|
var positionTop = e.pageY - window.pageYOffset - i.scrollbarYRail.getBoundingClientRect().top;
|
|
var direction = positionTop > i.scrollbarYTop ? 1 : -1;
|
|
|
|
i.element.scrollTop += direction * i.containerHeight;
|
|
updateGeometry(i);
|
|
|
|
e.stopPropagation();
|
|
});
|
|
|
|
i.event.bind(i.scrollbarX, 'mousedown', function (e) { return e.stopPropagation(); });
|
|
i.event.bind(i.scrollbarXRail, 'mousedown', function (e) {
|
|
var positionLeft =
|
|
e.pageX - window.pageXOffset - i.scrollbarXRail.getBoundingClientRect().left;
|
|
var direction = positionLeft > i.scrollbarXLeft ? 1 : -1;
|
|
|
|
i.element.scrollLeft += direction * i.containerWidth;
|
|
updateGeometry(i);
|
|
|
|
e.stopPropagation();
|
|
});
|
|
}
|
|
|
|
var activeSlider = null; // Variable to track the currently active slider
|
|
|
|
function setupScrollHandlers(i) {
|
|
bindMouseScrollHandler(i, [
|
|
'containerHeight',
|
|
'contentHeight',
|
|
'pageY',
|
|
'railYHeight',
|
|
'scrollbarY',
|
|
'scrollbarYHeight',
|
|
'scrollTop',
|
|
'y',
|
|
'scrollbarYRail' ]);
|
|
|
|
bindMouseScrollHandler(i, [
|
|
'containerWidth',
|
|
'contentWidth',
|
|
'pageX',
|
|
'railXWidth',
|
|
'scrollbarX',
|
|
'scrollbarXWidth',
|
|
'scrollLeft',
|
|
'x',
|
|
'scrollbarXRail' ]);
|
|
}
|
|
|
|
function bindMouseScrollHandler(
|
|
i,
|
|
ref
|
|
) {
|
|
var containerDimension = ref[0];
|
|
var contentDimension = ref[1];
|
|
var pageAxis = ref[2];
|
|
var railDimension = ref[3];
|
|
var scrollbarAxis = ref[4];
|
|
var scrollbarDimension = ref[5];
|
|
var scrollAxis = ref[6];
|
|
var axis = ref[7];
|
|
var scrollbarRail = ref[8];
|
|
|
|
var element = i.element;
|
|
var startingScrollPosition = null;
|
|
var startingMousePagePosition = null;
|
|
var scrollBy = null;
|
|
|
|
function moveHandler(e) {
|
|
if (e.touches && e.touches[0]) {
|
|
e[pageAxis] = e.touches[0][("page" + (axis.toUpperCase()))];
|
|
}
|
|
|
|
// Only move if the active slider is the one we started with
|
|
if (activeSlider === scrollbarAxis) {
|
|
element[scrollAxis] =
|
|
startingScrollPosition + scrollBy * (e[pageAxis] - startingMousePagePosition);
|
|
addScrollingClass(i, axis);
|
|
updateGeometry(i);
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
function endHandler() {
|
|
removeScrollingClass(i, axis);
|
|
i[scrollbarRail].classList.remove(cls.state.clicking);
|
|
document.removeEventListener('mousemove', moveHandler);
|
|
document.removeEventListener('mouseup', endHandler);
|
|
document.removeEventListener('touchmove', moveHandler);
|
|
document.removeEventListener('touchend', endHandler);
|
|
activeSlider = null; // Reset active slider when interaction ends
|
|
}
|
|
|
|
function bindMoves(e) {
|
|
if (activeSlider === null) {
|
|
// Only bind if no slider is currently active
|
|
activeSlider = scrollbarAxis; // Set current slider as active
|
|
|
|
startingScrollPosition = element[scrollAxis];
|
|
if (e.touches) {
|
|
e[pageAxis] = e.touches[0][("page" + (axis.toUpperCase()))];
|
|
}
|
|
startingMousePagePosition = e[pageAxis];
|
|
scrollBy =
|
|
(i[contentDimension] - i[containerDimension]) / (i[railDimension] - i[scrollbarDimension]);
|
|
|
|
if (!e.touches) {
|
|
document.addEventListener('mousemove', moveHandler);
|
|
document.addEventListener('mouseup', endHandler);
|
|
} else {
|
|
document.addEventListener('touchmove', moveHandler, { passive: false });
|
|
document.addEventListener('touchend', endHandler);
|
|
}
|
|
|
|
i[scrollbarRail].classList.add(cls.state.clicking);
|
|
}
|
|
|
|
e.stopPropagation();
|
|
if (e.cancelable) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
i[scrollbarAxis].addEventListener('mousedown', bindMoves);
|
|
i[scrollbarAxis].addEventListener('touchstart', bindMoves);
|
|
}
|
|
|
|
/* eslint-disable */
|
|
|
|
function keyboard (i) {
|
|
var element = i.element;
|
|
|
|
var elementHovered = function () { return matches(element, ':hover'); };
|
|
var scrollbarFocused = function () { return matches(i.scrollbarX, ':focus') || matches(i.scrollbarY, ':focus'); };
|
|
|
|
function shouldPreventDefault(deltaX, deltaY) {
|
|
var scrollTop = Math.floor(element.scrollTop);
|
|
if (deltaX === 0) {
|
|
if (!i.scrollbarYActive) {
|
|
return false;
|
|
}
|
|
if (
|
|
(scrollTop === 0 && deltaY > 0) ||
|
|
(scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0)
|
|
) {
|
|
return !i.settings.wheelPropagation;
|
|
}
|
|
}
|
|
|
|
var scrollLeft = element.scrollLeft;
|
|
if (deltaY === 0) {
|
|
if (!i.scrollbarXActive) {
|
|
return false;
|
|
}
|
|
if (
|
|
(scrollLeft === 0 && deltaX < 0) ||
|
|
(scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0)
|
|
) {
|
|
return !i.settings.wheelPropagation;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
i.event.bind(i.ownerDocument, 'keydown', function (e) {
|
|
if ((e.isDefaultPrevented && e.isDefaultPrevented()) || e.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
if (!elementHovered() && !scrollbarFocused()) {
|
|
return;
|
|
}
|
|
|
|
var activeElement = document.activeElement
|
|
? document.activeElement
|
|
: i.ownerDocument.activeElement;
|
|
if (activeElement) {
|
|
if (activeElement.tagName === 'IFRAME') {
|
|
activeElement = activeElement.contentDocument.activeElement;
|
|
} else {
|
|
// go deeper if element is a webcomponent
|
|
while (activeElement.shadowRoot) {
|
|
activeElement = activeElement.shadowRoot.activeElement;
|
|
}
|
|
}
|
|
if (isEditable(activeElement)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var deltaX = 0;
|
|
var deltaY = 0;
|
|
|
|
switch (e.which) {
|
|
case 37: // left
|
|
if (e.metaKey) {
|
|
deltaX = -i.contentWidth;
|
|
} else if (e.altKey) {
|
|
deltaX = -i.containerWidth;
|
|
} else {
|
|
deltaX = -30;
|
|
}
|
|
break;
|
|
case 38: // up
|
|
if (e.metaKey) {
|
|
deltaY = i.contentHeight;
|
|
} else if (e.altKey) {
|
|
deltaY = i.containerHeight;
|
|
} else {
|
|
deltaY = 30;
|
|
}
|
|
break;
|
|
case 39: // right
|
|
if (e.metaKey) {
|
|
deltaX = i.contentWidth;
|
|
} else if (e.altKey) {
|
|
deltaX = i.containerWidth;
|
|
} else {
|
|
deltaX = 30;
|
|
}
|
|
break;
|
|
case 40: // down
|
|
if (e.metaKey) {
|
|
deltaY = -i.contentHeight;
|
|
} else if (e.altKey) {
|
|
deltaY = -i.containerHeight;
|
|
} else {
|
|
deltaY = -30;
|
|
}
|
|
break;
|
|
case 32: // space bar
|
|
if (e.shiftKey) {
|
|
deltaY = i.containerHeight;
|
|
} else {
|
|
deltaY = -i.containerHeight;
|
|
}
|
|
break;
|
|
case 33: // page up
|
|
deltaY = i.containerHeight;
|
|
break;
|
|
case 34: // page down
|
|
deltaY = -i.containerHeight;
|
|
break;
|
|
case 36: // home
|
|
deltaY = i.contentHeight;
|
|
break;
|
|
case 35: // end
|
|
deltaY = -i.contentHeight;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (i.settings.suppressScrollX && deltaX !== 0) {
|
|
return;
|
|
}
|
|
if (i.settings.suppressScrollY && deltaY !== 0) {
|
|
return;
|
|
}
|
|
|
|
element.scrollTop -= deltaY;
|
|
element.scrollLeft += deltaX;
|
|
updateGeometry(i);
|
|
|
|
if (shouldPreventDefault(deltaX, deltaY)) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
}
|
|
|
|
/* eslint-disable */
|
|
|
|
function wheel (i) {
|
|
var element = i.element;
|
|
|
|
function shouldPreventDefault(deltaX, deltaY) {
|
|
var roundedScrollTop = Math.floor(element.scrollTop);
|
|
var isTop = element.scrollTop === 0;
|
|
var isBottom = roundedScrollTop + element.offsetHeight === element.scrollHeight;
|
|
var isLeft = element.scrollLeft === 0;
|
|
var isRight = element.scrollLeft + element.offsetWidth === element.scrollWidth;
|
|
|
|
var hitsBound;
|
|
|
|
// pick axis with primary direction
|
|
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
|
hitsBound = isTop || isBottom;
|
|
} else {
|
|
hitsBound = isLeft || isRight;
|
|
}
|
|
|
|
return hitsBound ? !i.settings.wheelPropagation : true;
|
|
}
|
|
|
|
function getDeltaFromEvent(e) {
|
|
var deltaX = e.deltaX;
|
|
var deltaY = -1 * e.deltaY;
|
|
|
|
if (typeof deltaX === 'undefined' || typeof deltaY === 'undefined') {
|
|
// OS X Safari
|
|
deltaX = (-1 * e.wheelDeltaX) / 6;
|
|
deltaY = e.wheelDeltaY / 6;
|
|
}
|
|
|
|
if (e.deltaMode && e.deltaMode === 1) {
|
|
// Firefox in deltaMode 1: Line scrolling
|
|
deltaX *= 10;
|
|
deltaY *= 10;
|
|
}
|
|
|
|
if (deltaX !== deltaX && deltaY !== deltaY /* NaN checks */) {
|
|
// IE in some mouse drivers
|
|
deltaX = 0;
|
|
deltaY = e.wheelDelta;
|
|
}
|
|
|
|
if (e.shiftKey) {
|
|
// reverse axis with shift key
|
|
return [-deltaY, -deltaX];
|
|
}
|
|
return [deltaX, deltaY];
|
|
}
|
|
|
|
function shouldBeConsumedByChild(target, deltaX, deltaY) {
|
|
// FIXME: this is a workaround for <select> issue in FF and IE #571
|
|
if (!env.isWebKit && element.querySelector('select:focus')) {
|
|
return true;
|
|
}
|
|
|
|
if (!element.contains(target)) {
|
|
return false;
|
|
}
|
|
|
|
var cursor = target;
|
|
|
|
while (cursor && cursor !== element) {
|
|
if (cursor.classList.contains(cls.element.consuming)) {
|
|
return true;
|
|
}
|
|
|
|
var style = get(cursor);
|
|
|
|
// if deltaY && vertical scrollable
|
|
if (deltaY && style.overflowY.match(/(scroll|auto)/)) {
|
|
var maxScrollTop = cursor.scrollHeight - cursor.clientHeight;
|
|
if (maxScrollTop > 0) {
|
|
if (
|
|
(cursor.scrollTop > 0 && deltaY < 0) ||
|
|
(cursor.scrollTop < maxScrollTop && deltaY > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// if deltaX && horizontal scrollable
|
|
if (deltaX && style.overflowX.match(/(scroll|auto)/)) {
|
|
var maxScrollLeft = cursor.scrollWidth - cursor.clientWidth;
|
|
if (maxScrollLeft > 0) {
|
|
if (
|
|
(cursor.scrollLeft > 0 && deltaX < 0) ||
|
|
(cursor.scrollLeft < maxScrollLeft && deltaX > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor = cursor.parentNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function mousewheelHandler(e) {
|
|
var ref = getDeltaFromEvent(e);
|
|
var deltaX = ref[0];
|
|
var deltaY = ref[1];
|
|
|
|
if (shouldBeConsumedByChild(e.target, deltaX, deltaY)) {
|
|
return;
|
|
}
|
|
|
|
var shouldPrevent = false;
|
|
if (!i.settings.useBothWheelAxes) {
|
|
// deltaX will only be used for horizontal scrolling and deltaY will
|
|
// only be used for vertical scrolling - this is the default
|
|
element.scrollTop -= deltaY * i.settings.wheelSpeed;
|
|
element.scrollLeft += deltaX * i.settings.wheelSpeed;
|
|
} else if (i.scrollbarYActive && !i.scrollbarXActive) {
|
|
// only vertical scrollbar is active and useBothWheelAxes option is
|
|
// active, so let's scroll vertical bar using both mouse wheel axes
|
|
if (deltaY) {
|
|
element.scrollTop -= deltaY * i.settings.wheelSpeed;
|
|
} else {
|
|
element.scrollTop += deltaX * i.settings.wheelSpeed;
|
|
}
|
|
shouldPrevent = true;
|
|
} else if (i.scrollbarXActive && !i.scrollbarYActive) {
|
|
// useBothWheelAxes and only horizontal bar is active, so use both
|
|
// wheel axes for horizontal bar
|
|
if (deltaX) {
|
|
element.scrollLeft += deltaX * i.settings.wheelSpeed;
|
|
} else {
|
|
element.scrollLeft -= deltaY * i.settings.wheelSpeed;
|
|
}
|
|
shouldPrevent = true;
|
|
}
|
|
|
|
updateGeometry(i);
|
|
|
|
shouldPrevent = shouldPrevent || shouldPreventDefault(deltaX, deltaY);
|
|
if (shouldPrevent && !e.ctrlKey) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
if (typeof window.onwheel !== 'undefined') {
|
|
i.event.bind(element, 'wheel', mousewheelHandler);
|
|
} else if (typeof window.onmousewheel !== 'undefined') {
|
|
i.event.bind(element, 'mousewheel', mousewheelHandler);
|
|
}
|
|
}
|
|
|
|
function touch (i) {
|
|
if (!env.supportsTouch && !env.supportsIePointer) {
|
|
return;
|
|
}
|
|
|
|
var element = i.element;
|
|
|
|
var state = {
|
|
startOffset: {},
|
|
startTime: 0,
|
|
speed: {},
|
|
easingLoop: null,
|
|
};
|
|
|
|
function shouldPrevent(deltaX, deltaY) {
|
|
var scrollTop = Math.floor(element.scrollTop);
|
|
var scrollLeft = element.scrollLeft;
|
|
var magnitudeX = Math.abs(deltaX);
|
|
var magnitudeY = Math.abs(deltaY);
|
|
|
|
if (magnitudeY > magnitudeX) {
|
|
// user is perhaps trying to swipe up/down the page
|
|
|
|
if (
|
|
(deltaY < 0 && scrollTop === i.contentHeight - i.containerHeight) ||
|
|
(deltaY > 0 && scrollTop === 0)
|
|
) {
|
|
// set prevent for mobile Chrome refresh
|
|
return window.scrollY === 0 && deltaY > 0 && env.isChrome;
|
|
}
|
|
} else if (magnitudeX > magnitudeY) {
|
|
// user is perhaps trying to swipe left/right across the page
|
|
|
|
if (
|
|
(deltaX < 0 && scrollLeft === i.contentWidth - i.containerWidth) ||
|
|
(deltaX > 0 && scrollLeft === 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function applyTouchMove(differenceX, differenceY) {
|
|
element.scrollTop -= differenceY;
|
|
element.scrollLeft -= differenceX;
|
|
|
|
updateGeometry(i);
|
|
}
|
|
|
|
function getTouch(e) {
|
|
if (e.targetTouches) {
|
|
return e.targetTouches[0];
|
|
}
|
|
// Maybe IE pointer
|
|
return e;
|
|
}
|
|
|
|
function shouldHandle(e) {
|
|
if (e.target === i.scrollbarX || e.target === i.scrollbarY) {
|
|
return false;
|
|
}
|
|
if (e.pointerType && e.pointerType === 'pen' && e.buttons === 0) {
|
|
return false;
|
|
}
|
|
if (e.targetTouches && e.targetTouches.length === 1) {
|
|
return true;
|
|
}
|
|
if (e.pointerType && e.pointerType !== 'mouse' && e.pointerType !== e.MSPOINTER_TYPE_MOUSE) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function touchStart(e) {
|
|
if (!shouldHandle(e)) {
|
|
return;
|
|
}
|
|
|
|
var touch = getTouch(e);
|
|
|
|
state.startOffset.pageX = touch.pageX;
|
|
state.startOffset.pageY = touch.pageY;
|
|
|
|
state.startTime = new Date().getTime();
|
|
|
|
if (state.easingLoop !== null) {
|
|
clearInterval(state.easingLoop);
|
|
}
|
|
}
|
|
|
|
function shouldBeConsumedByChild(target, deltaX, deltaY) {
|
|
if (!element.contains(target)) {
|
|
return false;
|
|
}
|
|
|
|
var cursor = target;
|
|
|
|
while (cursor && cursor !== element) {
|
|
if (cursor.classList.contains(cls.element.consuming)) {
|
|
return true;
|
|
}
|
|
|
|
var style = get(cursor);
|
|
|
|
// if deltaY && vertical scrollable
|
|
if (deltaY && style.overflowY.match(/(scroll|auto)/)) {
|
|
var maxScrollTop = cursor.scrollHeight - cursor.clientHeight;
|
|
if (maxScrollTop > 0) {
|
|
if (
|
|
(cursor.scrollTop > 0 && deltaY < 0) ||
|
|
(cursor.scrollTop < maxScrollTop && deltaY > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// if deltaX && horizontal scrollable
|
|
if (deltaX && style.overflowX.match(/(scroll|auto)/)) {
|
|
var maxScrollLeft = cursor.scrollWidth - cursor.clientWidth;
|
|
if (maxScrollLeft > 0) {
|
|
if (
|
|
(cursor.scrollLeft > 0 && deltaX < 0) ||
|
|
(cursor.scrollLeft < maxScrollLeft && deltaX > 0)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
cursor = cursor.parentNode;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function touchMove(e) {
|
|
if (shouldHandle(e)) {
|
|
var touch = getTouch(e);
|
|
|
|
var currentOffset = { pageX: touch.pageX, pageY: touch.pageY };
|
|
|
|
var differenceX = currentOffset.pageX - state.startOffset.pageX;
|
|
var differenceY = currentOffset.pageY - state.startOffset.pageY;
|
|
|
|
if (shouldBeConsumedByChild(e.target, differenceX, differenceY)) {
|
|
return;
|
|
}
|
|
|
|
applyTouchMove(differenceX, differenceY);
|
|
state.startOffset = currentOffset;
|
|
|
|
var currentTime = new Date().getTime();
|
|
|
|
var timeGap = currentTime - state.startTime;
|
|
if (timeGap > 0) {
|
|
state.speed.x = differenceX / timeGap;
|
|
state.speed.y = differenceY / timeGap;
|
|
state.startTime = currentTime;
|
|
}
|
|
|
|
if (shouldPrevent(differenceX, differenceY)) {
|
|
// Prevent the default behavior if the event is cancelable
|
|
if (e.cancelable) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function touchEnd() {
|
|
if (i.settings.swipeEasing) {
|
|
clearInterval(state.easingLoop);
|
|
state.easingLoop = setInterval(function () {
|
|
if (i.isInitialized) {
|
|
clearInterval(state.easingLoop);
|
|
return;
|
|
}
|
|
|
|
if (!state.speed.x && !state.speed.y) {
|
|
clearInterval(state.easingLoop);
|
|
return;
|
|
}
|
|
|
|
if (Math.abs(state.speed.x) < 0.01 && Math.abs(state.speed.y) < 0.01) {
|
|
clearInterval(state.easingLoop);
|
|
return;
|
|
}
|
|
|
|
applyTouchMove(state.speed.x * 30, state.speed.y * 30);
|
|
|
|
state.speed.x *= 0.8;
|
|
state.speed.y *= 0.8;
|
|
}, 10);
|
|
}
|
|
}
|
|
|
|
if (env.supportsTouch) {
|
|
i.event.bind(element, 'touchstart', touchStart);
|
|
i.event.bind(element, 'touchmove', touchMove);
|
|
i.event.bind(element, 'touchend', touchEnd);
|
|
} else if (env.supportsIePointer) {
|
|
if (window.PointerEvent) {
|
|
i.event.bind(element, 'pointerdown', touchStart);
|
|
i.event.bind(element, 'pointermove', touchMove);
|
|
i.event.bind(element, 'pointerup', touchEnd);
|
|
} else if (window.MSPointerEvent) {
|
|
i.event.bind(element, 'MSPointerDown', touchStart);
|
|
i.event.bind(element, 'MSPointerMove', touchMove);
|
|
i.event.bind(element, 'MSPointerUp', touchEnd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* eslint-disable */
|
|
|
|
var defaultSettings = function () { return ({
|
|
handlers: ['click-rail', 'drag-thumb', 'keyboard', 'wheel', 'touch'],
|
|
maxScrollbarLength: null,
|
|
minScrollbarLength: null,
|
|
scrollingThreshold: 1000,
|
|
scrollXMarginOffset: 0,
|
|
scrollYMarginOffset: 0,
|
|
suppressScrollX: false,
|
|
suppressScrollY: false,
|
|
swipeEasing: true,
|
|
useBothWheelAxes: false,
|
|
wheelPropagation: true,
|
|
wheelSpeed: 1,
|
|
}); };
|
|
|
|
var handlers = {
|
|
'click-rail': clickRail,
|
|
'drag-thumb': setupScrollHandlers,
|
|
keyboard: keyboard,
|
|
wheel: wheel,
|
|
touch: touch,
|
|
};
|
|
|
|
var PerfectScrollbar = function PerfectScrollbar(element, userSettings) {
|
|
var this$1 = this;
|
|
if ( userSettings === void 0 ) userSettings = {};
|
|
|
|
if (typeof element === 'string') {
|
|
element = document.querySelector(element);
|
|
}
|
|
|
|
if (!element || !element.nodeName) {
|
|
throw new Error('no element is specified to initialize PerfectScrollbar');
|
|
}
|
|
|
|
this.element = element;
|
|
|
|
element.classList.add(cls.main);
|
|
|
|
this.settings = defaultSettings();
|
|
for (var key in userSettings) {
|
|
this.settings[key] = userSettings[key];
|
|
}
|
|
|
|
this.containerWidth = null;
|
|
this.containerHeight = null;
|
|
this.contentWidth = null;
|
|
this.contentHeight = null;
|
|
|
|
var focus = function () { return element.classList.add(cls.state.focus); };
|
|
var blur = function () { return element.classList.remove(cls.state.focus); };
|
|
|
|
this.isRtl = get(element).direction === 'rtl';
|
|
if (this.isRtl === true) {
|
|
element.classList.add(cls.rtl);
|
|
}
|
|
this.isNegativeScroll = (function () {
|
|
var originalScrollLeft = element.scrollLeft;
|
|
var result = null;
|
|
element.scrollLeft = -1;
|
|
result = element.scrollLeft < 0;
|
|
element.scrollLeft = originalScrollLeft;
|
|
return result;
|
|
})();
|
|
this.negativeScrollAdjustment = this.isNegativeScroll
|
|
? element.scrollWidth - element.clientWidth
|
|
: 0;
|
|
this.event = new EventManager();
|
|
this.ownerDocument = element.ownerDocument || document;
|
|
|
|
this.scrollbarXRail = div(cls.element.rail('x'));
|
|
element.appendChild(this.scrollbarXRail);
|
|
this.scrollbarX = div(cls.element.thumb('x'));
|
|
this.scrollbarXRail.appendChild(this.scrollbarX);
|
|
this.scrollbarX.setAttribute('tabindex', 0);
|
|
this.event.bind(this.scrollbarX, 'focus', focus);
|
|
this.event.bind(this.scrollbarX, 'blur', blur);
|
|
this.scrollbarXActive = null;
|
|
this.scrollbarXWidth = null;
|
|
this.scrollbarXLeft = null;
|
|
var railXStyle = get(this.scrollbarXRail);
|
|
this.scrollbarXBottom = parseInt(railXStyle.bottom, 10);
|
|
if (isNaN(this.scrollbarXBottom)) {
|
|
this.isScrollbarXUsingBottom = false;
|
|
this.scrollbarXTop = toInt(railXStyle.top);
|
|
} else {
|
|
this.isScrollbarXUsingBottom = true;
|
|
}
|
|
this.railBorderXWidth = toInt(railXStyle.borderLeftWidth) + toInt(railXStyle.borderRightWidth);
|
|
// Set rail to display:block to calculate margins
|
|
perfect_scrollbar_esm_set(this.scrollbarXRail, { display: 'block' });
|
|
this.railXMarginWidth = toInt(railXStyle.marginLeft) + toInt(railXStyle.marginRight);
|
|
perfect_scrollbar_esm_set(this.scrollbarXRail, { display: '' });
|
|
this.railXWidth = null;
|
|
this.railXRatio = null;
|
|
|
|
this.scrollbarYRail = div(cls.element.rail('y'));
|
|
element.appendChild(this.scrollbarYRail);
|
|
this.scrollbarY = div(cls.element.thumb('y'));
|
|
this.scrollbarYRail.appendChild(this.scrollbarY);
|
|
this.scrollbarY.setAttribute('tabindex', 0);
|
|
this.event.bind(this.scrollbarY, 'focus', focus);
|
|
this.event.bind(this.scrollbarY, 'blur', blur);
|
|
this.scrollbarYActive = null;
|
|
this.scrollbarYHeight = null;
|
|
this.scrollbarYTop = null;
|
|
var railYStyle = get(this.scrollbarYRail);
|
|
this.scrollbarYRight = parseInt(railYStyle.right, 10);
|
|
if (isNaN(this.scrollbarYRight)) {
|
|
this.isScrollbarYUsingRight = false;
|
|
this.scrollbarYLeft = toInt(railYStyle.left);
|
|
} else {
|
|
this.isScrollbarYUsingRight = true;
|
|
}
|
|
this.scrollbarYOuterWidth = this.isRtl ? perfect_scrollbar_esm_outerWidth(this.scrollbarY) : null;
|
|
this.railBorderYWidth = toInt(railYStyle.borderTopWidth) + toInt(railYStyle.borderBottomWidth);
|
|
perfect_scrollbar_esm_set(this.scrollbarYRail, { display: 'block' });
|
|
this.railYMarginHeight = toInt(railYStyle.marginTop) + toInt(railYStyle.marginBottom);
|
|
perfect_scrollbar_esm_set(this.scrollbarYRail, { display: '' });
|
|
this.railYHeight = null;
|
|
this.railYRatio = null;
|
|
|
|
this.reach = {
|
|
x:
|
|
element.scrollLeft <= 0
|
|
? 'start'
|
|
: element.scrollLeft >= this.contentWidth - this.containerWidth
|
|
? 'end'
|
|
: null,
|
|
y:
|
|
element.scrollTop <= 0
|
|
? 'start'
|
|
: element.scrollTop >= this.contentHeight - this.containerHeight
|
|
? 'end'
|
|
: null,
|
|
};
|
|
|
|
this.isAlive = true;
|
|
|
|
this.settings.handlers.forEach(function (handlerName) { return handlers[handlerName](this$1); });
|
|
|
|
this.lastScrollTop = Math.floor(element.scrollTop); // for onScroll only
|
|
this.lastScrollLeft = element.scrollLeft; // for onScroll only
|
|
this.event.bind(this.element, 'scroll', function (e) { return this$1.onScroll(e); });
|
|
updateGeometry(this);
|
|
};
|
|
|
|
PerfectScrollbar.prototype.update = function update () {
|
|
if (!this.isAlive) {
|
|
return;
|
|
}
|
|
|
|
// Recalcuate negative scrollLeft adjustment
|
|
this.negativeScrollAdjustment = this.isNegativeScroll
|
|
? this.element.scrollWidth - this.element.clientWidth
|
|
: 0;
|
|
|
|
// Recalculate rail margins
|
|
perfect_scrollbar_esm_set(this.scrollbarXRail, { display: 'block' });
|
|
perfect_scrollbar_esm_set(this.scrollbarYRail, { display: 'block' });
|
|
this.railXMarginWidth =
|
|
toInt(get(this.scrollbarXRail).marginLeft) +
|
|
toInt(get(this.scrollbarXRail).marginRight);
|
|
this.railYMarginHeight =
|
|
toInt(get(this.scrollbarYRail).marginTop) +
|
|
toInt(get(this.scrollbarYRail).marginBottom);
|
|
|
|
// Hide scrollbars not to affect scrollWidth and scrollHeight
|
|
perfect_scrollbar_esm_set(this.scrollbarXRail, { display: 'none' });
|
|
perfect_scrollbar_esm_set(this.scrollbarYRail, { display: 'none' });
|
|
|
|
updateGeometry(this);
|
|
|
|
processScrollDiff(this, 'top', 0, false, true);
|
|
processScrollDiff(this, 'left', 0, false, true);
|
|
|
|
perfect_scrollbar_esm_set(this.scrollbarXRail, { display: '' });
|
|
perfect_scrollbar_esm_set(this.scrollbarYRail, { display: '' });
|
|
};
|
|
|
|
PerfectScrollbar.prototype.onScroll = function onScroll (e) {
|
|
if (!this.isAlive) {
|
|
return;
|
|
}
|
|
|
|
updateGeometry(this);
|
|
processScrollDiff(this, 'top', this.element.scrollTop - this.lastScrollTop);
|
|
processScrollDiff(this, 'left', this.element.scrollLeft - this.lastScrollLeft);
|
|
|
|
this.lastScrollTop = Math.floor(this.element.scrollTop);
|
|
this.lastScrollLeft = this.element.scrollLeft;
|
|
};
|
|
|
|
PerfectScrollbar.prototype.destroy = function destroy () {
|
|
if (!this.isAlive) {
|
|
return;
|
|
}
|
|
|
|
this.event.unbindAll();
|
|
remove(this.scrollbarX);
|
|
remove(this.scrollbarY);
|
|
remove(this.scrollbarXRail);
|
|
remove(this.scrollbarYRail);
|
|
this.removePsClasses();
|
|
|
|
// unset elements
|
|
this.element = null;
|
|
this.scrollbarX = null;
|
|
this.scrollbarY = null;
|
|
this.scrollbarXRail = null;
|
|
this.scrollbarYRail = null;
|
|
|
|
this.isAlive = false;
|
|
};
|
|
|
|
PerfectScrollbar.prototype.removePsClasses = function removePsClasses () {
|
|
this.element.className = this.element.className
|
|
.split(' ')
|
|
.filter(function (name) { return !name.match(/^ps([-_].+|)$/); })
|
|
.join(' ');
|
|
};
|
|
|
|
/* harmony default export */ const perfect_scrollbar_esm = (PerfectScrollbar);
|
|
//# sourceMappingURL=perfect-scrollbar.esm.js.map
|
|
|
|
;// ../assets/scripts/scrollbar/index.js
|
|
|
|
/* harmony default export */ const scrollbar = ((function () {
|
|
const scrollables = document.querySelectorAll('.scrollable');
|
|
if (scrollables.length > 0) {
|
|
scrollables.forEach(el => {
|
|
new perfect_scrollbar_esm(el);
|
|
});
|
|
}
|
|
})());
|
|
;// ../assets/scripts/search/index.js
|
|
/* harmony default export */ const search = ((function () {
|
|
const searchToggle = document.querySelector('.search-toggle');
|
|
const searchBox = document.querySelector('.search-box');
|
|
const searchInput = document.querySelector('.search-input');
|
|
const searchInputField = document.querySelector('.search-input input');
|
|
if (searchToggle && searchBox && searchInput && searchInputField) {
|
|
searchToggle.addEventListener('click', e => {
|
|
searchBox.classList.toggle('active');
|
|
searchInput.classList.toggle('active');
|
|
searchInputField.focus();
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/skycons/skycons.js
|
|
var skycons = __webpack_require__(658);
|
|
var skycons_default = /*#__PURE__*/__webpack_require__.n(skycons);
|
|
;// ../assets/scripts/skycons/index.js
|
|
|
|
|
|
const Skycons = skycons_default()(window);
|
|
/* harmony default export */ const scripts_skycons = ((function () {
|
|
let icons;
|
|
const initSkycons = () => {
|
|
const skyconsColor = theme.getCSSVar('--skycons-color');
|
|
if (icons) {
|
|
icons.pause();
|
|
icons.remove('all');
|
|
}
|
|
icons = new Skycons({
|
|
'color': skyconsColor
|
|
});
|
|
const list = ['clear-day', 'clear-night', 'partly-cloudy-day', 'partly-cloudy-night', 'cloudy', 'rain', 'sleet', 'snow', 'wind', 'fog'];
|
|
let i = list.length;
|
|
while (i--) {
|
|
const weatherType = list[i],
|
|
elements = document.getElementsByClassName(weatherType);
|
|
let j = elements.length;
|
|
while (j--) {
|
|
icons.set(elements[j], weatherType);
|
|
}
|
|
}
|
|
icons.play();
|
|
};
|
|
|
|
// Initialize skycons
|
|
initSkycons();
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', initSkycons);
|
|
})());
|
|
;// ../../node_modules/jsvectormap/dist/jsvectormap.esm.js
|
|
/**
|
|
* By https://github.com/TehShrike/deepmerge
|
|
*/
|
|
|
|
var isMergeableObject = function isMergeableObject(value) {
|
|
return isNonNullObject(value) && !isSpecial(value);
|
|
};
|
|
function isNonNullObject(value) {
|
|
return !!value && typeof value === 'object';
|
|
}
|
|
function isSpecial(value) {
|
|
var stringValue = Object.prototype.toString.call(value);
|
|
return stringValue === '[object RegExp]' || stringValue === '[object Date]' || isNode(value) || isReactElement(value);
|
|
}
|
|
|
|
// see https://github.com/facebook/react/blob/b5ac963fb791d1298e7f396236383bc955f916c1/src/isomorphic/classic/element/ReactElement.js#L21-L25
|
|
var canUseSymbol = typeof Symbol === 'function' && Symbol.for;
|
|
var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7;
|
|
function isReactElement(value) {
|
|
return value.$$typeof === REACT_ELEMENT_TYPE;
|
|
}
|
|
function isNode(value) {
|
|
return value instanceof Node;
|
|
}
|
|
function emptyTarget(val) {
|
|
return Array.isArray(val) ? [] : {};
|
|
}
|
|
function cloneUnlessOtherwiseSpecified(value, options) {
|
|
return options.clone !== false && options.isMergeableObject(value) ? deepmerge(emptyTarget(value), value, options) : value;
|
|
}
|
|
function defaultArrayMerge(target, source, options) {
|
|
return target.concat(source).map(function (element) {
|
|
return cloneUnlessOtherwiseSpecified(element, options);
|
|
});
|
|
}
|
|
function getMergeFunction(key, options) {
|
|
if (!options.customMerge) {
|
|
return deepmerge;
|
|
}
|
|
var customMerge = options.customMerge(key);
|
|
return typeof customMerge === 'function' ? customMerge : deepmerge;
|
|
}
|
|
function getEnumerableOwnPropertySymbols(target) {
|
|
return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function (symbol) {
|
|
return target.propertyIsEnumerable(symbol);
|
|
}) : [];
|
|
}
|
|
function getKeys(target) {
|
|
return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target));
|
|
}
|
|
function propertyIsOnObject(object, property) {
|
|
try {
|
|
return property in object;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Protects from prototype poisoning and unexpected merging up the prototype chain.
|
|
function propertyIsUnsafe(target, key) {
|
|
return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
|
|
&& !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
|
|
&& Object.propertyIsEnumerable.call(target, key)); // and also unsafe if they're nonenumerable.
|
|
}
|
|
function mergeObject(target, source, options) {
|
|
var destination = {};
|
|
if (options.isMergeableObject(target)) {
|
|
getKeys(target).forEach(function (key) {
|
|
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options);
|
|
});
|
|
}
|
|
getKeys(source).forEach(function (key) {
|
|
if (propertyIsUnsafe(target, key)) {
|
|
return;
|
|
}
|
|
if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
|
|
destination[key] = getMergeFunction(key, options)(target[key], source[key], options);
|
|
} else {
|
|
destination[key] = cloneUnlessOtherwiseSpecified(source[key], options);
|
|
}
|
|
});
|
|
return destination;
|
|
}
|
|
var deepmerge = function deepmerge(target, source, options) {
|
|
options = options || {};
|
|
options.arrayMerge = options.arrayMerge || defaultArrayMerge;
|
|
options.isMergeableObject = options.isMergeableObject || isMergeableObject;
|
|
// cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge()
|
|
// implementations can use it. The caller may not replace it.
|
|
options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified;
|
|
var sourceIsArray = Array.isArray(source);
|
|
var targetIsArray = Array.isArray(target);
|
|
var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray;
|
|
if (!sourceAndTargetTypesMatch) {
|
|
return cloneUnlessOtherwiseSpecified(source, options);
|
|
} else if (sourceIsArray) {
|
|
return options.arrayMerge(target, source, options);
|
|
} else {
|
|
return mergeObject(target, source, options);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* --------------------------------------------------------------------------
|
|
* Public Util Api
|
|
* --------------------------------------------------------------------------
|
|
*/
|
|
var getElement = function getElement(selector) {
|
|
if (typeof selector === 'object' && typeof selector.nodeType !== 'undefined') {
|
|
return selector;
|
|
}
|
|
if (typeof selector === 'string') {
|
|
return document.querySelector(selector);
|
|
}
|
|
return null;
|
|
};
|
|
var createElement = function createElement(type, classes, content, html) {
|
|
if (html === void 0) {
|
|
html = false;
|
|
}
|
|
var el = document.createElement(type);
|
|
if (content) {
|
|
el[!html ? 'textContent' : 'innerHTML'] = content;
|
|
}
|
|
if (classes) {
|
|
el.className = classes;
|
|
}
|
|
return el;
|
|
};
|
|
var findElement = function findElement(parentElement, selector) {
|
|
return Element.prototype.querySelector.call(parentElement, selector);
|
|
};
|
|
var jsvectormap_esm_removeElement = function removeElement(target) {
|
|
target.parentNode.removeChild(target);
|
|
};
|
|
var isImageUrl = function isImageUrl(url) {
|
|
return /\.(jpg|gif|png)$/.test(url);
|
|
};
|
|
var hyphenate = function hyphenate(string) {
|
|
return string.replace(/[\w]([A-Z])/g, function (m) {
|
|
return m[0] + "-" + m[1];
|
|
}).toLowerCase();
|
|
};
|
|
var jsvectormap_esm_merge = function merge(target, source, deep) {
|
|
if (deep === void 0) {
|
|
deep = false;
|
|
}
|
|
if (deep) {
|
|
return deepmerge(target, source);
|
|
}
|
|
return Object.assign(target, source);
|
|
};
|
|
var getLineUid = function getLineUid(from, to) {
|
|
return from.toLowerCase() + ":to:" + to.toLowerCase();
|
|
};
|
|
var inherit = function inherit(target, source) {
|
|
Object.assign(target.prototype, source);
|
|
};
|
|
|
|
var eventRegistry = {};
|
|
var eventUid = 1;
|
|
var EventHandler = {
|
|
on: function on(element, event, handler, options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
var uid = "jvm:" + event + "::" + eventUid++;
|
|
eventRegistry[uid] = {
|
|
selector: element,
|
|
handler: handler
|
|
};
|
|
handler._uid = uid;
|
|
element.addEventListener(event, handler, options);
|
|
},
|
|
delegate: function delegate(element, event, selector, handler) {
|
|
event = event.split(' ');
|
|
event.forEach(function (eventName) {
|
|
EventHandler.on(element, eventName, function (e) {
|
|
var target = e.target;
|
|
if (target.matches(selector)) {
|
|
handler.call(target, e);
|
|
}
|
|
});
|
|
});
|
|
},
|
|
off: function off(element, event, handler) {
|
|
var eventType = event.split(':')[1];
|
|
element.removeEventListener(eventType, handler);
|
|
delete eventRegistry[handler._uid];
|
|
},
|
|
flush: function flush() {
|
|
Object.keys(eventRegistry).forEach(function (event) {
|
|
EventHandler.off(eventRegistry[event].selector, event, eventRegistry[event].handler);
|
|
});
|
|
},
|
|
getEventRegistry: function getEventRegistry() {
|
|
return eventRegistry;
|
|
}
|
|
};
|
|
|
|
function setupContainerEvents() {
|
|
var _this = this;
|
|
var map = this;
|
|
var mouseDown = false;
|
|
var oldPageX;
|
|
var oldPageY;
|
|
if (this.params.draggable) {
|
|
EventHandler.on(this.container, 'mousemove', function (e) {
|
|
if (!mouseDown) {
|
|
return false;
|
|
}
|
|
map.transX -= (oldPageX - e.pageX) / map.scale;
|
|
map.transY -= (oldPageY - e.pageY) / map.scale;
|
|
map._applyTransform();
|
|
oldPageX = e.pageX;
|
|
oldPageY = e.pageY;
|
|
});
|
|
EventHandler.on(this.container, 'mousedown', function (e) {
|
|
mouseDown = true;
|
|
oldPageX = e.pageX;
|
|
oldPageY = e.pageY;
|
|
return false;
|
|
});
|
|
EventHandler.on(document.body, 'mouseup', function () {
|
|
mouseDown = false;
|
|
});
|
|
}
|
|
if (this.params.zoomOnScroll) {
|
|
EventHandler.on(this.container, 'wheel', function (event) {
|
|
var deltaY = ((event.deltaY || -event.wheelDelta || event.detail) >> 10 || 1) * 75;
|
|
var rect = _this.container.getBoundingClientRect();
|
|
var offsetX = event.pageX - rect.left - window.scrollX;
|
|
var offsetY = event.pageY - rect.top - window.scrollY;
|
|
var zoomStep = Math.pow(1 + map.params.zoomOnScrollSpeed / 1000, -1.5 * deltaY);
|
|
if (map.tooltip) {
|
|
map._tooltip.hide();
|
|
}
|
|
map._setScale(map.scale * zoomStep, offsetX, offsetY);
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
}
|
|
|
|
var jsvectormap_esm_Events = {
|
|
onLoaded: 'map:loaded',
|
|
onViewportChange: 'viewport:changed',
|
|
onRegionClick: 'region:clicked',
|
|
onMarkerClick: 'marker:clicked',
|
|
onRegionSelected: 'region:selected',
|
|
onMarkerSelected: 'marker:selected',
|
|
onRegionTooltipShow: 'region.tooltip:show',
|
|
onMarkerTooltipShow: 'marker.tooltip:show',
|
|
onDestroyed: 'map:destroyed'
|
|
};
|
|
|
|
var jsvectormap_esm_parseEvent = function parseEvent(map, selector, isTooltip) {
|
|
var element = getElement(selector);
|
|
var type = element.getAttribute('class').indexOf('jvm-region') === -1 ? 'marker' : 'region';
|
|
var isRegion = type === 'region';
|
|
var code = isRegion ? element.getAttribute('data-code') : element.getAttribute('data-index');
|
|
var event = isRegion ? jsvectormap_esm_Events.onRegionSelected : jsvectormap_esm_Events.onMarkerSelected;
|
|
|
|
// Init tooltip event
|
|
if (isTooltip) {
|
|
event = isRegion ? jsvectormap_esm_Events.onRegionTooltipShow : jsvectormap_esm_Events.onMarkerTooltipShow;
|
|
}
|
|
return {
|
|
type: type,
|
|
code: code,
|
|
event: event,
|
|
element: isRegion ? map.regions[code].element : map._markers[code].element,
|
|
tooltipText: isRegion ? map._mapData.paths[code].name || '' : map._markers[code].config.name || ''
|
|
};
|
|
};
|
|
function setupElementEvents() {
|
|
var map = this;
|
|
var container = this.container;
|
|
var pageX, pageY, mouseMoved;
|
|
EventHandler.on(container, 'mousemove', function (event) {
|
|
if (Math.abs(pageX - event.pageX) + Math.abs(pageY - event.pageY) > 2) {
|
|
mouseMoved = true;
|
|
}
|
|
});
|
|
|
|
// When the mouse is pressed
|
|
EventHandler.delegate(container, 'mousedown', '.jvm-element', function (event) {
|
|
pageX = event.pageX;
|
|
pageY = event.pageY;
|
|
mouseMoved = false;
|
|
});
|
|
|
|
// When the mouse is over the region/marker | When the mouse is out the region/marker
|
|
EventHandler.delegate(container, 'mouseover mouseout', '.jvm-element', function (event) {
|
|
var data = jsvectormap_esm_parseEvent(map, this, true);
|
|
var showTooltip = map.params.showTooltip;
|
|
if (event.type === 'mouseover') {
|
|
data.element.hover(true);
|
|
if (showTooltip) {
|
|
map._tooltip.text(data.tooltipText);
|
|
map._emit(data.event, [event, map._tooltip, data.code]);
|
|
if (!event.defaultPrevented) {
|
|
map._tooltip.show();
|
|
}
|
|
}
|
|
} else {
|
|
data.element.hover(false);
|
|
if (showTooltip) {
|
|
map._tooltip.hide();
|
|
}
|
|
}
|
|
});
|
|
|
|
// When the click is released
|
|
EventHandler.delegate(container, 'mouseup', '.jvm-element', function (event) {
|
|
var data = jsvectormap_esm_parseEvent(map, this);
|
|
if (mouseMoved) {
|
|
return;
|
|
}
|
|
if (data.type === 'region' && map.params.regionsSelectable || data.type === 'marker' && map.params.markersSelectable) {
|
|
var element = data.element;
|
|
|
|
// We're checking if regions/markers|SelectableOne option is presented
|
|
if (map.params[data.type + "sSelectableOne"]) {
|
|
data.type === 'region' ? map.clearSelectedRegions() : map.clearSelectedMarkers();
|
|
}
|
|
if (data.element.isSelected) {
|
|
element.select(false);
|
|
} else {
|
|
element.select(true);
|
|
}
|
|
map._emit(data.event, [data.code, element.isSelected, data.type === 'region' ? map.getSelectedRegions() : map.getSelectedMarkers()]);
|
|
}
|
|
});
|
|
|
|
// When region/marker is clicked
|
|
EventHandler.delegate(container, 'click', '.jvm-element', function (event) {
|
|
var _parseEvent = jsvectormap_esm_parseEvent(map, this),
|
|
type = _parseEvent.type,
|
|
code = _parseEvent.code;
|
|
map._emit(type === 'region' ? jsvectormap_esm_Events.onRegionClick : jsvectormap_esm_Events.onMarkerClick, [event, code]);
|
|
});
|
|
}
|
|
|
|
function setupZoomButtons() {
|
|
var _this = this;
|
|
var zoomInOption = this.params.zoomInButton;
|
|
var zoomOutOption = this.params.zoomOutButton;
|
|
var getZoomButton = function getZoomButton(zoomOption) {
|
|
return typeof zoomOption === 'string' ? document.querySelector(zoomOption) : zoomOption;
|
|
};
|
|
var zoomIn = zoomInOption ? getZoomButton(zoomInOption) : createElement('div', 'jvm-zoom-btn jvm-zoomin', '+', true);
|
|
var zoomOut = zoomOutOption ? getZoomButton(zoomOutOption) : createElement('div', 'jvm-zoom-btn jvm-zoomout', '−', true);
|
|
if (!zoomInOption) {
|
|
this.container.appendChild(zoomIn);
|
|
}
|
|
if (!zoomOutOption) {
|
|
this.container.appendChild(zoomOut);
|
|
}
|
|
var handler = function handler(zoomin) {
|
|
if (zoomin === void 0) {
|
|
zoomin = true;
|
|
}
|
|
return function () {
|
|
return _this._setScale(zoomin ? _this.scale * _this.params.zoomStep : _this.scale / _this.params.zoomStep, _this._width / 2, _this._height / 2, false, _this.params.zoomAnimate);
|
|
};
|
|
};
|
|
EventHandler.on(zoomIn, 'click', handler());
|
|
EventHandler.on(zoomOut, 'click', handler(false));
|
|
}
|
|
|
|
function setupContainerTouchEvents() {
|
|
var map = this,
|
|
touchStartScale,
|
|
touchStartDistance,
|
|
touchX,
|
|
touchY,
|
|
centerTouchX,
|
|
centerTouchY,
|
|
lastTouchesLength;
|
|
var handleTouchEvent = function handleTouchEvent(e) {
|
|
var touches = e.touches;
|
|
var offset, scale, transXOld, transYOld;
|
|
if (e.type == 'touchstart') {
|
|
lastTouchesLength = 0;
|
|
}
|
|
if (touches.length == 1) {
|
|
if (lastTouchesLength == 1) {
|
|
var _map$_tooltip;
|
|
transXOld = map.transX;
|
|
transYOld = map.transY;
|
|
map.transX -= (touchX - touches[0].pageX) / map.scale;
|
|
map.transY -= (touchY - touches[0].pageY) / map.scale;
|
|
(_map$_tooltip = map._tooltip) == null || _map$_tooltip.hide();
|
|
map._applyTransform();
|
|
if (transXOld != map.transX || transYOld != map.transY) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
touchX = touches[0].pageX;
|
|
touchY = touches[0].pageY;
|
|
} else if (touches.length == 2) {
|
|
if (lastTouchesLength == 2) {
|
|
var _map$_tooltip2;
|
|
scale = Math.sqrt(Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2)) / touchStartDistance;
|
|
map._setScale(touchStartScale * scale, centerTouchX, centerTouchY);
|
|
(_map$_tooltip2 = map._tooltip) == null || _map$_tooltip2.hide();
|
|
e.preventDefault();
|
|
} else {
|
|
var rect = map.container.getBoundingClientRect();
|
|
offset = {
|
|
top: rect.top + window.scrollY,
|
|
left: rect.left + window.scrollX
|
|
};
|
|
if (touches[0].pageX > touches[1].pageX) {
|
|
centerTouchX = touches[1].pageX + (touches[0].pageX - touches[1].pageX) / 2;
|
|
} else {
|
|
centerTouchX = touches[0].pageX + (touches[1].pageX - touches[0].pageX) / 2;
|
|
}
|
|
if (touches[0].pageY > touches[1].pageY) {
|
|
centerTouchY = touches[1].pageY + (touches[0].pageY - touches[1].pageY) / 2;
|
|
} else {
|
|
centerTouchY = touches[0].pageY + (touches[1].pageY - touches[0].pageY) / 2;
|
|
}
|
|
centerTouchX -= offset.left;
|
|
centerTouchY -= offset.top;
|
|
touchStartScale = map.scale;
|
|
touchStartDistance = Math.sqrt(Math.pow(touches[0].pageX - touches[1].pageX, 2) + Math.pow(touches[0].pageY - touches[1].pageY, 2));
|
|
}
|
|
}
|
|
lastTouchesLength = touches.length;
|
|
};
|
|
EventHandler.on(map.container, 'touchstart', handleTouchEvent);
|
|
EventHandler.on(map.container, 'touchmove', handleTouchEvent);
|
|
}
|
|
|
|
function _arrayLikeToArray(r, a) {
|
|
(null == a || a > r.length) && (a = r.length);
|
|
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
|
|
return n;
|
|
}
|
|
function _assertThisInitialized(e) {
|
|
if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
|
|
return e;
|
|
}
|
|
function _defineProperties(e, r) {
|
|
for (var t = 0; t < r.length; t++) {
|
|
var o = r[t];
|
|
o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o);
|
|
}
|
|
}
|
|
function _createClass(e, r, t) {
|
|
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
|
|
writable: !1
|
|
}), e;
|
|
}
|
|
function _createForOfIteratorHelperLoose(r, e) {
|
|
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
|
|
if (t) return (t = t.call(r)).next.bind(t);
|
|
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) {
|
|
t && (r = t);
|
|
var o = 0;
|
|
return function () {
|
|
return o >= r.length ? {
|
|
done: !0
|
|
} : {
|
|
done: !1,
|
|
value: r[o++]
|
|
};
|
|
};
|
|
}
|
|
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
|
|
}
|
|
function _extends() {
|
|
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
for (var e = 1; e < arguments.length; e++) {
|
|
var t = arguments[e];
|
|
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
|
|
}
|
|
return n;
|
|
}, _extends.apply(null, arguments);
|
|
}
|
|
function _inheritsLoose(t, o) {
|
|
t.prototype = Object.create(o.prototype), t.prototype.constructor = t, _setPrototypeOf(t, o);
|
|
}
|
|
function _objectWithoutPropertiesLoose(r, e) {
|
|
if (null == r) return {};
|
|
var t = {};
|
|
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
|
|
if (-1 !== e.indexOf(n)) continue;
|
|
t[n] = r[n];
|
|
}
|
|
return t;
|
|
}
|
|
function _setPrototypeOf(t, e) {
|
|
return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) {
|
|
return t.__proto__ = e, t;
|
|
}, _setPrototypeOf(t, e);
|
|
}
|
|
function _toPrimitive(t, r) {
|
|
if ("object" != typeof t || !t) return t;
|
|
var e = t[Symbol.toPrimitive];
|
|
if (void 0 !== e) {
|
|
var i = e.call(t, r || "default");
|
|
if ("object" != typeof i) return i;
|
|
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
}
|
|
return ("string" === r ? String : Number)(t);
|
|
}
|
|
function _toPropertyKey(t) {
|
|
var i = _toPrimitive(t, "string");
|
|
return "symbol" == typeof i ? i : i + "";
|
|
}
|
|
function _unsupportedIterableToArray(r, a) {
|
|
if (r) {
|
|
if ("string" == typeof r) return _arrayLikeToArray(r, a);
|
|
var t = {}.toString.call(r).slice(8, -1);
|
|
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
|
|
}
|
|
}
|
|
|
|
var jsvectormap_esm_BaseComponent = /*#__PURE__*/function () {
|
|
function BaseComponent() {}
|
|
var _proto = BaseComponent.prototype;
|
|
_proto.dispose = function dispose() {
|
|
if (this._tooltip) {
|
|
jsvectormap_esm_removeElement(this._tooltip);
|
|
} else {
|
|
// @todo: move shape in base component in v2
|
|
this.shape.remove();
|
|
}
|
|
for (var _iterator = _createForOfIteratorHelperLoose(Object.getOwnPropertyNames(this)), _step; !(_step = _iterator()).done;) {
|
|
var propertyName = _step.value;
|
|
this[propertyName] = null;
|
|
}
|
|
};
|
|
return BaseComponent;
|
|
}();
|
|
|
|
var Interactable = {
|
|
getLabelText: function getLabelText(key, label) {
|
|
if (!label) {
|
|
return;
|
|
}
|
|
if (typeof label.render === 'function') {
|
|
var params = [];
|
|
|
|
// Pass additional paramater (Marker config object) in case it's a Marker.
|
|
if (this.constructor.Name === 'marker') {
|
|
params.push(this.getConfig());
|
|
}
|
|
|
|
// Becuase we need to add the key always at the end
|
|
params.push(key);
|
|
return label.render.apply(this, params);
|
|
}
|
|
return key;
|
|
},
|
|
getLabelOffsets: function getLabelOffsets(key, label) {
|
|
if (typeof label.offsets === 'function') {
|
|
return label.offsets(key);
|
|
}
|
|
|
|
// If offsets are an array of offsets e.g offsets: [ [0, 25], [10, 15] ]
|
|
if (Array.isArray(label.offsets)) {
|
|
return label.offsets[key];
|
|
}
|
|
return [0, 0];
|
|
},
|
|
setStyle: function setStyle(property, value) {
|
|
this.shape.setStyle(property, value);
|
|
},
|
|
remove: function remove() {
|
|
this.shape.remove();
|
|
if (this.label) this.label.remove();
|
|
},
|
|
hover: function hover(state) {
|
|
this._setStatus('isHovered', state);
|
|
},
|
|
select: function select(state) {
|
|
this._setStatus('isSelected', state);
|
|
},
|
|
// Private
|
|
_setStatus: function _setStatus(property, state) {
|
|
this.shape[property] = state;
|
|
this.shape.updateStyle();
|
|
this[property] = state;
|
|
if (this.label) {
|
|
this.label[property] = state;
|
|
this.label.updateStyle();
|
|
}
|
|
}
|
|
};
|
|
|
|
var Region = /*#__PURE__*/function (_BaseComponent) {
|
|
function Region(_ref) {
|
|
var _this;
|
|
var map = _ref.map,
|
|
code = _ref.code,
|
|
path = _ref.path,
|
|
style = _ref.style,
|
|
label = _ref.label,
|
|
labelStyle = _ref.labelStyle,
|
|
labelsGroup = _ref.labelsGroup;
|
|
_this = _BaseComponent.call(this) || this;
|
|
_this._map = map;
|
|
_this.shape = _this._createRegion(path, code, style);
|
|
var text = _this.getLabelText(code, label);
|
|
|
|
// If label is passed and render function returns something
|
|
if (label && text) {
|
|
var bbox = _this.shape.getBBox();
|
|
var offsets = _this.getLabelOffsets(code, label);
|
|
_this.labelX = bbox.x + bbox.width / 2 + offsets[0];
|
|
_this.labelY = bbox.y + bbox.height / 2 + offsets[1];
|
|
_this.label = _this._map.canvas.createText({
|
|
text: text,
|
|
textAnchor: 'middle',
|
|
alignmentBaseline: 'central',
|
|
dataCode: code,
|
|
x: _this.labelX,
|
|
y: _this.labelY
|
|
}, labelStyle, labelsGroup);
|
|
_this.label.addClass('jvm-region jvm-element');
|
|
}
|
|
return _this;
|
|
}
|
|
_inheritsLoose(Region, _BaseComponent);
|
|
var _proto = Region.prototype;
|
|
_proto._createRegion = function _createRegion(path, code, style) {
|
|
path = this._map.canvas.createPath({
|
|
d: path,
|
|
dataCode: code
|
|
}, style);
|
|
path.addClass('jvm-region jvm-element');
|
|
return path;
|
|
};
|
|
_proto.updateLabelPosition = function updateLabelPosition() {
|
|
if (this.label) {
|
|
this.label.set({
|
|
x: this.labelX * this._map.scale + this._map.transX * this._map.scale,
|
|
y: this.labelY * this._map.scale + this._map.transY * this._map.scale
|
|
});
|
|
}
|
|
};
|
|
return Region;
|
|
}(jsvectormap_esm_BaseComponent);
|
|
inherit(Region, Interactable);
|
|
|
|
function createRegions() {
|
|
this._regionLabelsGroup = this._regionLabelsGroup || this.canvas.createGroup('jvm-regions-labels-group');
|
|
for (var code in this._mapData.paths) {
|
|
var region = new Region({
|
|
map: this,
|
|
code: code,
|
|
path: this._mapData.paths[code].path,
|
|
style: jsvectormap_esm_merge({}, this.params.regionStyle),
|
|
labelStyle: this.params.regionLabelStyle,
|
|
labelsGroup: this._regionLabelsGroup,
|
|
label: this.params.labels && this.params.labels.regions
|
|
});
|
|
this.regions[code] = {
|
|
config: this._mapData.paths[code],
|
|
element: region
|
|
};
|
|
}
|
|
}
|
|
|
|
var LINE_CLASS = 'jvm-line';
|
|
var Line = /*#__PURE__*/function (_BaseComponent) {
|
|
function Line(options, style) {
|
|
var _this;
|
|
_this = _BaseComponent.call(this) || this;
|
|
_this._options = options;
|
|
_this._style = {
|
|
initial: style
|
|
};
|
|
_this._draw();
|
|
return _this;
|
|
}
|
|
_inheritsLoose(Line, _BaseComponent);
|
|
var _proto = Line.prototype;
|
|
_proto.setStyle = function setStyle(property, value) {
|
|
this.shape.setStyle(property, value);
|
|
};
|
|
_proto.getConfig = function getConfig() {
|
|
return this._options.config;
|
|
};
|
|
_proto._draw = function _draw() {
|
|
var _this$_options = this._options,
|
|
index = _this$_options.index,
|
|
group = _this$_options.group,
|
|
map = _this$_options.map;
|
|
var config = {
|
|
d: this._getDAttribute(),
|
|
fill: 'none',
|
|
dataIndex: index
|
|
};
|
|
this.shape = map.canvas.createPath(config, this._style, group);
|
|
this.shape.addClass(LINE_CLASS);
|
|
};
|
|
_proto._getDAttribute = function _getDAttribute() {
|
|
var _this$_options2 = this._options,
|
|
x1 = _this$_options2.x1,
|
|
y1 = _this$_options2.y1,
|
|
x2 = _this$_options2.x2,
|
|
y2 = _this$_options2.y2,
|
|
curvature = _this$_options2.curvature;
|
|
return "M" + x1 + "," + y1 + this._getQCommand(x1, y1, x2, y2, curvature) + x2 + "," + y2;
|
|
};
|
|
_proto._getQCommand = function _getQCommand(x1, y1, x2, y2, curvature) {
|
|
if (!curvature) {
|
|
return ' ';
|
|
}
|
|
var curveX = (x1 + x2) / 2 + curvature * (y2 - y1);
|
|
var curveY = (y1 + y2) / 2 - curvature * (x2 - x1);
|
|
return " Q" + curveX + "," + curveY + " ";
|
|
};
|
|
return Line;
|
|
}(jsvectormap_esm_BaseComponent);
|
|
|
|
var _excluded = ["curvature"],
|
|
_excluded2 = ["curvature"];
|
|
function createLines(lines) {
|
|
var point1 = false,
|
|
point2 = false;
|
|
var _this$params$lineStyl = this.params.lineStyle,
|
|
curvature = _this$params$lineStyl.curvature,
|
|
lineStyle = _objectWithoutPropertiesLoose(_this$params$lineStyl, _excluded);
|
|
for (var index in lines) {
|
|
var lineConfig = lines[index];
|
|
for (var _i = 0, _Object$values = Object.values(this._markers); _i < _Object$values.length; _i++) {
|
|
var markerConfig = _Object$values[_i].config;
|
|
if (markerConfig.name === lineConfig.from) {
|
|
point1 = this.getMarkerPosition(markerConfig);
|
|
}
|
|
if (markerConfig.name === lineConfig.to) {
|
|
point2 = this.getMarkerPosition(markerConfig);
|
|
}
|
|
}
|
|
if (point1 !== false && point2 !== false) {
|
|
var _ref = lineConfig.style || {},
|
|
curvatureOption = _ref.curvature,
|
|
style = _objectWithoutPropertiesLoose(_ref, _excluded2);
|
|
|
|
// Register lines with unique keys
|
|
this._lines[getLineUid(lineConfig.from, lineConfig.to)] = new Line({
|
|
index: index,
|
|
map: this,
|
|
group: this._linesGroup,
|
|
config: lineConfig,
|
|
x1: point1.x,
|
|
y1: point1.y,
|
|
x2: point2.x,
|
|
y2: point2.y,
|
|
curvature: curvatureOption == 0 ? 0 : curvatureOption || curvature
|
|
}, jsvectormap_esm_merge(lineStyle, style, true));
|
|
}
|
|
}
|
|
}
|
|
|
|
var NAME = 'marker';
|
|
var JVM_PREFIX$1 = 'jvm-';
|
|
var MARKER_CLASS = JVM_PREFIX$1 + "element " + JVM_PREFIX$1 + "marker";
|
|
var MARKER_LABEL_CLASS = JVM_PREFIX$1 + "element " + JVM_PREFIX$1 + "label";
|
|
var Marker = /*#__PURE__*/function (_BaseComponent) {
|
|
function Marker(options, style) {
|
|
var _this;
|
|
_this = _BaseComponent.call(this) || this;
|
|
_this._options = options;
|
|
_this._style = style;
|
|
_this._labelX = null;
|
|
_this._labelY = null;
|
|
_this._offsets = null;
|
|
_this._isImage = !!style.initial.image;
|
|
_this._draw();
|
|
if (_this._options.label) {
|
|
_this._drawLabel();
|
|
}
|
|
if (_this._isImage) {
|
|
_this.updateLabelPosition();
|
|
}
|
|
return _this;
|
|
}
|
|
_inheritsLoose(Marker, _BaseComponent);
|
|
var _proto = Marker.prototype;
|
|
_proto.getConfig = function getConfig() {
|
|
return this._options.config;
|
|
};
|
|
_proto.updateLabelPosition = function updateLabelPosition() {
|
|
var map = this._options.map;
|
|
if (this.label) {
|
|
this.label.set({
|
|
x: this._labelX * map.scale + this._offsets[0] + map.transX * map.scale + 5 + (this._isImage ? (this.shape.width || 0) / 2 : this.shape.node.r.baseVal.value),
|
|
y: this._labelY * map.scale + map.transY * this._options.map.scale + this._offsets[1]
|
|
});
|
|
}
|
|
};
|
|
_proto._draw = function _draw() {
|
|
var _this$_options = this._options,
|
|
index = _this$_options.index,
|
|
map = _this$_options.map,
|
|
group = _this$_options.group,
|
|
cx = _this$_options.cx,
|
|
cy = _this$_options.cy;
|
|
var shapeType = this._isImage ? 'createImage' : 'createCircle';
|
|
this.shape = map.canvas[shapeType]({
|
|
dataIndex: index,
|
|
cx: cx,
|
|
cy: cy
|
|
}, this._style, group);
|
|
this.shape.addClass(MARKER_CLASS);
|
|
};
|
|
_proto._drawLabel = function _drawLabel() {
|
|
var _this$_options2 = this._options,
|
|
index = _this$_options2.index,
|
|
map = _this$_options2.map,
|
|
label = _this$_options2.label,
|
|
labelsGroup = _this$_options2.labelsGroup,
|
|
cx = _this$_options2.cx,
|
|
cy = _this$_options2.cy,
|
|
config = _this$_options2.config,
|
|
isRecentlyCreated = _this$_options2.isRecentlyCreated;
|
|
var labelText = this.getLabelText(index, label);
|
|
this._labelX = cx / map.scale - map.transX;
|
|
this._labelY = cy / map.scale - map.transY;
|
|
this._offsets = isRecentlyCreated && config.offsets ? config.offsets : this.getLabelOffsets(index, label);
|
|
this.label = map.canvas.createText({
|
|
text: labelText,
|
|
dataIndex: index,
|
|
x: this._labelX,
|
|
y: this._labelY,
|
|
dy: '0.6ex'
|
|
}, map.params.markerLabelStyle, labelsGroup);
|
|
this.label.addClass(MARKER_LABEL_CLASS);
|
|
if (isRecentlyCreated) {
|
|
this.updateLabelPosition();
|
|
}
|
|
};
|
|
return _createClass(Marker, null, [{
|
|
key: "Name",
|
|
get: function get() {
|
|
return NAME;
|
|
}
|
|
}]);
|
|
}(jsvectormap_esm_BaseComponent);
|
|
inherit(Marker, Interactable);
|
|
|
|
function createMarkers(markers, isRecentlyCreated) {
|
|
var _this = this;
|
|
if (markers === void 0) {
|
|
markers = {};
|
|
}
|
|
if (isRecentlyCreated === void 0) {
|
|
isRecentlyCreated = false;
|
|
}
|
|
var _loop = function _loop() {
|
|
var config = markers[index];
|
|
var point = _this.getMarkerPosition(config);
|
|
var uid = config.coords.join(':');
|
|
if (!point) {
|
|
return 0; // continue
|
|
}
|
|
|
|
// We're checking if recently created marker does already exist
|
|
// If it does we don't need to create it again, so we'll continue
|
|
// Becuase we may have more than one marker submitted via `addMarkers` method.
|
|
if (isRecentlyCreated) {
|
|
if (Object.keys(_this._markers).filter(function (i) {
|
|
return _this._markers[i]._uid === uid;
|
|
}).length) {
|
|
return 0; // continue
|
|
}
|
|
index = Object.keys(_this._markers).length;
|
|
}
|
|
var marker = new Marker({
|
|
index: index,
|
|
map: _this,
|
|
label: _this.params.labels && _this.params.labels.markers,
|
|
labelsGroup: _this._markerLabelsGroup,
|
|
cx: point.x,
|
|
cy: point.y,
|
|
group: _this._markersGroup,
|
|
config: config,
|
|
isRecentlyCreated: isRecentlyCreated
|
|
}, jsvectormap_esm_merge(_this.params.markerStyle, _extends({}, config.style || {}), true));
|
|
|
|
// Check for marker duplication
|
|
// this is useful when for example: a user clicks a button for creating marker two times
|
|
// so it will remove the old one and the new one will take its place.
|
|
if (_this._markers[index]) {
|
|
_this.removeMarkers([index]);
|
|
}
|
|
_this._markers[index] = {
|
|
_uid: uid,
|
|
config: config,
|
|
element: marker
|
|
};
|
|
},
|
|
_ret;
|
|
for (var index in markers) {
|
|
_ret = _loop();
|
|
if (_ret === 0) continue;
|
|
}
|
|
}
|
|
|
|
var jsvectormap_esm_Legend = /*#__PURE__*/function () {
|
|
function Legend(options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
this._options = options;
|
|
this._map = this._options.map;
|
|
this._series = this._options.series;
|
|
this._body = createElement('div', 'jvm-legend');
|
|
if (this._options.cssClass) {
|
|
this._body.setAttribute('class', this._options.cssClass);
|
|
}
|
|
if (options.vertical) {
|
|
this._map.legendVertical.appendChild(this._body);
|
|
} else {
|
|
this._map.legendHorizontal.appendChild(this._body);
|
|
}
|
|
this.render();
|
|
}
|
|
var _proto = Legend.prototype;
|
|
_proto.render = function render() {
|
|
var ticks = this._series.scale.getTicks();
|
|
this._body.innderHTML = '';
|
|
if (this._options.title) {
|
|
var legendTitle = createElement('div', 'jvm-legend-title', this._options.title);
|
|
this._body.appendChild(legendTitle);
|
|
}
|
|
for (var i = 0; i < ticks.length; i++) {
|
|
var tick = createElement('div', 'jvm-legend-tick');
|
|
var sample = createElement('div', 'jvm-legend-tick-sample');
|
|
switch (this._series.config.attribute) {
|
|
case 'fill':
|
|
if (isImageUrl(ticks[i].value)) {
|
|
sample.style.background = "url(" + ticks[i].value + ")";
|
|
} else {
|
|
sample.style.background = ticks[i].value;
|
|
}
|
|
break;
|
|
case 'stroke':
|
|
sample.style.background = ticks[i].value;
|
|
break;
|
|
case 'image':
|
|
sample.style.background = "url(" + (typeof ticks[i].value === 'object' ? ticks[i].value.url : ticks[i].value) + ") no-repeat center center";
|
|
sample.style.backgroundSize = 'cover';
|
|
break;
|
|
}
|
|
tick.appendChild(sample);
|
|
var label = ticks[i].label;
|
|
if (this._options.labelRender) {
|
|
label = this._options.labelRender(label);
|
|
}
|
|
var tickText = createElement('div', 'jvm-legend-tick-text', label);
|
|
tick.appendChild(tickText);
|
|
this._body.appendChild(tick);
|
|
}
|
|
};
|
|
return Legend;
|
|
}();
|
|
|
|
var OrdinalScale = /*#__PURE__*/function () {
|
|
function OrdinalScale(scale) {
|
|
this._scale = scale;
|
|
}
|
|
var _proto = OrdinalScale.prototype;
|
|
_proto.getValue = function getValue(value) {
|
|
return this._scale[value];
|
|
};
|
|
_proto.getTicks = function getTicks() {
|
|
var ticks = [];
|
|
for (var key in this._scale) {
|
|
ticks.push({
|
|
label: key,
|
|
value: this._scale[key]
|
|
});
|
|
}
|
|
return ticks;
|
|
};
|
|
return OrdinalScale;
|
|
}();
|
|
|
|
var Series = /*#__PURE__*/function () {
|
|
function Series(config, elements, map) {
|
|
if (config === void 0) {
|
|
config = {};
|
|
}
|
|
// Private
|
|
this._map = map;
|
|
this._elements = elements; // Could be markers or regions
|
|
this._values = config.values || {};
|
|
|
|
// Protected
|
|
this.config = config;
|
|
this.config.attribute = config.attribute || 'fill';
|
|
|
|
// Set initial attributes
|
|
if (config.attributes) {
|
|
this.setAttributes(config.attributes);
|
|
}
|
|
if (typeof config.scale === 'object') {
|
|
this.scale = new OrdinalScale(config.scale);
|
|
}
|
|
if (this.config.legend) {
|
|
this.legend = new jsvectormap_esm_Legend(jsvectormap_esm_merge({
|
|
map: this._map,
|
|
series: this
|
|
}, this.config.legend));
|
|
}
|
|
this.setValues(this._values);
|
|
}
|
|
var _proto = Series.prototype;
|
|
_proto.setValues = function setValues(values) {
|
|
var attrs = {};
|
|
for (var key in values) {
|
|
if (values[key]) {
|
|
attrs[key] = this.scale.getValue(values[key]);
|
|
}
|
|
}
|
|
this.setAttributes(attrs);
|
|
};
|
|
_proto.setAttributes = function setAttributes(attrs) {
|
|
for (var code in attrs) {
|
|
if (this._elements[code]) {
|
|
this._elements[code].element.setStyle(this.config.attribute, attrs[code]);
|
|
}
|
|
}
|
|
};
|
|
_proto.clear = function clear() {
|
|
var key,
|
|
attrs = {};
|
|
for (key in this._values) {
|
|
if (this._elements[key]) {
|
|
attrs[key] = this._elements[key].element.shape.style.initial[this.config.attribute];
|
|
}
|
|
}
|
|
this.setAttributes(attrs);
|
|
this._values = {};
|
|
};
|
|
return Series;
|
|
}();
|
|
|
|
function createSeries() {
|
|
this.series = {
|
|
markers: [],
|
|
regions: []
|
|
};
|
|
for (var key in this.params.series) {
|
|
for (var i = 0; i < this.params.series[key].length; i++) {
|
|
this.series[key][i] = new Series(this.params.series[key][i], key === 'markers' ? this._markers : this.regions, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyTransform() {
|
|
var maxTransX, maxTransY, minTransX, minTransY;
|
|
if (this._defaultWidth * this.scale <= this._width) {
|
|
maxTransX = (this._width - this._defaultWidth * this.scale) / (2 * this.scale);
|
|
minTransX = (this._width - this._defaultWidth * this.scale) / (2 * this.scale);
|
|
} else {
|
|
maxTransX = 0;
|
|
minTransX = (this._width - this._defaultWidth * this.scale) / this.scale;
|
|
}
|
|
if (this._defaultHeight * this.scale <= this._height) {
|
|
maxTransY = (this._height - this._defaultHeight * this.scale) / (2 * this.scale);
|
|
minTransY = (this._height - this._defaultHeight * this.scale) / (2 * this.scale);
|
|
} else {
|
|
maxTransY = 0;
|
|
minTransY = (this._height - this._defaultHeight * this.scale) / this.scale;
|
|
}
|
|
if (this.transY > maxTransY) {
|
|
this.transY = maxTransY;
|
|
} else if (this.transY < minTransY) {
|
|
this.transY = minTransY;
|
|
}
|
|
if (this.transX > maxTransX) {
|
|
this.transX = maxTransX;
|
|
} else if (this.transX < minTransX) {
|
|
this.transX = minTransX;
|
|
}
|
|
this.canvas.applyTransformParams(this.scale, this.transX, this.transY);
|
|
if (this._markers) {
|
|
this._repositionMarkers();
|
|
}
|
|
if (this._lines) {
|
|
this._repositionLines();
|
|
}
|
|
this._repositionLabels();
|
|
}
|
|
|
|
function resize() {
|
|
var curBaseScale = this._baseScale;
|
|
if (this._width / this._height > this._defaultWidth / this._defaultHeight) {
|
|
this._baseScale = this._height / this._defaultHeight;
|
|
this._baseTransX = Math.abs(this._width - this._defaultWidth * this._baseScale) / (2 * this._baseScale);
|
|
} else {
|
|
this._baseScale = this._width / this._defaultWidth;
|
|
this._baseTransY = Math.abs(this._height - this._defaultHeight * this._baseScale) / (2 * this._baseScale);
|
|
}
|
|
this.scale *= this._baseScale / curBaseScale;
|
|
this.transX *= this._baseScale / curBaseScale;
|
|
this.transY *= this._baseScale / curBaseScale;
|
|
}
|
|
|
|
function setScale(scale, anchorX, anchorY, isCentered, animate) {
|
|
var _this = this;
|
|
var zoomStep,
|
|
interval,
|
|
i = 0,
|
|
count = Math.abs(Math.round((scale - this.scale) * 60 / Math.max(scale, this.scale))),
|
|
scaleStart,
|
|
scaleDiff,
|
|
transXStart,
|
|
transXDiff,
|
|
transYStart,
|
|
transYDiff,
|
|
transX,
|
|
transY;
|
|
if (scale > this.params.zoomMax * this._baseScale) {
|
|
scale = this.params.zoomMax * this._baseScale;
|
|
} else if (scale < this.params.zoomMin * this._baseScale) {
|
|
scale = this.params.zoomMin * this._baseScale;
|
|
}
|
|
if (typeof anchorX != 'undefined' && typeof anchorY != 'undefined') {
|
|
zoomStep = scale / this.scale;
|
|
if (isCentered) {
|
|
transX = anchorX + this._defaultWidth * (this._width / (this._defaultWidth * scale)) / 2;
|
|
transY = anchorY + this._defaultHeight * (this._height / (this._defaultHeight * scale)) / 2;
|
|
} else {
|
|
transX = this.transX - (zoomStep - 1) / scale * anchorX;
|
|
transY = this.transY - (zoomStep - 1) / scale * anchorY;
|
|
}
|
|
}
|
|
if (animate && count > 0) {
|
|
scaleStart = this.scale;
|
|
scaleDiff = (scale - scaleStart) / count;
|
|
transXStart = this.transX * this.scale;
|
|
transYStart = this.transY * this.scale;
|
|
transXDiff = (transX * scale - transXStart) / count;
|
|
transYDiff = (transY * scale - transYStart) / count;
|
|
interval = setInterval(function () {
|
|
i += 1;
|
|
_this.scale = scaleStart + scaleDiff * i;
|
|
_this.transX = (transXStart + transXDiff * i) / _this.scale;
|
|
_this.transY = (transYStart + transYDiff * i) / _this.scale;
|
|
_this._applyTransform();
|
|
if (i == count) {
|
|
clearInterval(interval);
|
|
_this._emit(jsvectormap_esm_Events.onViewportChange, [_this.scale, _this.transX, _this.transY]);
|
|
}
|
|
}, 10);
|
|
} else {
|
|
this.transX = transX;
|
|
this.transY = transY;
|
|
this.scale = scale;
|
|
this._applyTransform();
|
|
this._emit(jsvectormap_esm_Events.onViewportChange, [this.scale, this.transX, this.transY]);
|
|
}
|
|
}
|
|
|
|
function setFocus(config) {
|
|
var _this = this;
|
|
if (config === void 0) {
|
|
config = {};
|
|
}
|
|
var bbox,
|
|
codes = [];
|
|
if (config.region) {
|
|
codes.push(config.region);
|
|
} else if (config.regions) {
|
|
codes = config.regions;
|
|
}
|
|
if (codes.length) {
|
|
codes.forEach(function (code) {
|
|
if (_this.regions[code]) {
|
|
var itemBbox = _this.regions[code].element.shape.getBBox();
|
|
if (itemBbox) {
|
|
// Handle the first loop
|
|
if (typeof bbox == 'undefined') {
|
|
bbox = itemBbox;
|
|
} else {
|
|
// get the old bbox properties plus the current
|
|
// this kinda incrementing the old values and the new values
|
|
bbox = {
|
|
x: Math.min(bbox.x, itemBbox.x),
|
|
y: Math.min(bbox.y, itemBbox.y),
|
|
width: Math.max(bbox.x + bbox.width, itemBbox.x + itemBbox.width) - Math.min(bbox.x, itemBbox.x),
|
|
height: Math.max(bbox.y + bbox.height, itemBbox.y + itemBbox.height) - Math.min(bbox.y, itemBbox.y)
|
|
};
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return this._setScale(Math.min(this._width / bbox.width, this._height / bbox.height), -(bbox.x + bbox.width / 2), -(bbox.y + bbox.height / 2), true, config.animate);
|
|
} else if (config.coords) {
|
|
var point = this.coordsToPoint(config.coords[0], config.coords[1]);
|
|
var x = this.transX - point.x / this.scale;
|
|
var y = this.transY - point.y / this.scale;
|
|
return this._setScale(config.scale * this._baseScale, x, y, true, config.animate);
|
|
}
|
|
}
|
|
|
|
function updateSize() {
|
|
this._width = this.container.offsetWidth;
|
|
this._height = this.container.offsetHeight;
|
|
this._resize();
|
|
this.canvas.setSize(this._width, this._height);
|
|
this._applyTransform();
|
|
}
|
|
|
|
/**
|
|
* ------------------------------------------------------------------------
|
|
* Object
|
|
* ------------------------------------------------------------------------
|
|
*/
|
|
var Proj = {
|
|
/* sgn(n){
|
|
if (n > 0) {
|
|
return 1;
|
|
} else if (n < 0) {
|
|
return -1;
|
|
} else {
|
|
return n;
|
|
}
|
|
}, */
|
|
mill: function mill(lat, lng, c) {
|
|
return {
|
|
x: this.radius * (lng - c) * this.radDeg,
|
|
y: -this.radius * Math.log(Math.tan((45 + 0.4 * lat) * this.radDeg)) / 0.8
|
|
};
|
|
},
|
|
/* mill_inv(x, y, c) {
|
|
return {
|
|
lat: (2.5 * Math.atan(Math.exp(0.8 * y / this.radius)) - 5 * Math.PI / 8) * this.degRad,
|
|
lng: (c * this.radDeg + x / this.radius) * this.degRad
|
|
};
|
|
}, */
|
|
merc: function merc(lat, lng, c) {
|
|
return {
|
|
x: this.radius * (lng - c) * this.radDeg,
|
|
y: -this.radius * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360))
|
|
};
|
|
},
|
|
/* merc_inv(x, y, c) {
|
|
return {
|
|
lat: (2 * Math.atan(Math.exp(y / this.radius)) - Math.PI / 2) * this.degRad,
|
|
lng: (c * this.radDeg + x / this.radius) * this.degRad
|
|
};
|
|
}, */
|
|
aea: function aea(lat, lng, c) {
|
|
var fi0 = 0,
|
|
lambda0 = c * this.radDeg,
|
|
fi1 = 29.5 * this.radDeg,
|
|
fi2 = 45.5 * this.radDeg,
|
|
fi = lat * this.radDeg,
|
|
lambda = lng * this.radDeg,
|
|
n = (Math.sin(fi1) + Math.sin(fi2)) / 2,
|
|
C = Math.cos(fi1) * Math.cos(fi1) + 2 * n * Math.sin(fi1),
|
|
theta = n * (lambda - lambda0),
|
|
ro = Math.sqrt(C - 2 * n * Math.sin(fi)) / n,
|
|
ro0 = Math.sqrt(C - 2 * n * Math.sin(fi0)) / n;
|
|
return {
|
|
x: ro * Math.sin(theta) * this.radius,
|
|
y: -(ro0 - ro * Math.cos(theta)) * this.radius
|
|
};
|
|
},
|
|
/* aea_inv(xCoord, yCoord, c) {
|
|
var x = xCoord / this.radius,
|
|
y = yCoord / this.radius,
|
|
fi0 = 0,
|
|
lambda0 = c * this.radDeg,
|
|
fi1 = 29.5 * this.radDeg,
|
|
fi2 = 45.5 * this.radDeg,
|
|
n = (Math.sin(fi1)+Math.sin(fi2)) / 2,
|
|
C = Math.cos(fi1)*Math.cos(fi1)+2*n*Math.sin(fi1),
|
|
ro0 = Math.sqrt(C-2*n*Math.sin(fi0))/n,
|
|
ro = Math.sqrt(x*x+(ro0-y)*(ro0-y)),
|
|
theta = Math.atan( x / (ro0 - y) );
|
|
return {
|
|
lat: (Math.asin((C - ro * ro * n * n) / (2 * n))) * this.degRad,
|
|
lng: (lambda0 + theta / n) * this.degRad
|
|
};
|
|
}, */
|
|
lcc: function lcc(lat, lng, c) {
|
|
var fi0 = 0,
|
|
lambda0 = c * this.radDeg,
|
|
lambda = lng * this.radDeg,
|
|
fi1 = 33 * this.radDeg,
|
|
fi2 = 45 * this.radDeg,
|
|
fi = lat * this.radDeg,
|
|
n = Math.log(Math.cos(fi1) * (1 / Math.cos(fi2))) / Math.log(Math.tan(Math.PI / 4 + fi2 / 2) * (1 / Math.tan(Math.PI / 4 + fi1 / 2))),
|
|
F = Math.cos(fi1) * Math.pow(Math.tan(Math.PI / 4 + fi1 / 2), n) / n,
|
|
ro = F * Math.pow(1 / Math.tan(Math.PI / 4 + fi / 2), n),
|
|
ro0 = F * Math.pow(1 / Math.tan(Math.PI / 4 + fi0 / 2), n);
|
|
return {
|
|
x: ro * Math.sin(n * (lambda - lambda0)) * this.radius,
|
|
y: -(ro0 - ro * Math.cos(n * (lambda - lambda0))) * this.radius
|
|
};
|
|
}
|
|
/* lcc_inv(xCoord, yCoord, c) {
|
|
var x = xCoord / this.radius,
|
|
y = yCoord / this.radius,
|
|
fi0 = 0,
|
|
lambda0 = c * this.radDeg,
|
|
fi1 = 33 * this.radDeg,
|
|
fi2 = 45 * this.radDeg,
|
|
n = Math.log( Math.cos(fi1) * (1 / Math.cos(fi2)) ) / Math.log( Math.tan( Math.PI / 4 + fi2 / 2) * (1 / Math.tan( Math.PI / 4 + fi1 / 2) ) ),
|
|
F = ( Math.cos(fi1) * Math.pow( Math.tan( Math.PI / 4 + fi1 / 2 ), n ) ) / n,
|
|
ro0 = F * Math.pow( 1 / Math.tan( Math.PI / 4 + fi0 / 2 ), n ),
|
|
ro = this.sgn(n) * Math.sqrt(x*x+(ro0-y)*(ro0-y)),
|
|
theta = Math.atan( x / (ro0 - y) );
|
|
return {
|
|
lat: (2 * Math.atan(Math.pow(F/ro, 1/n)) - Math.PI / 2) * this.degRad,
|
|
lng: (lambda0 + theta / n) * this.degRad
|
|
};
|
|
} */
|
|
};
|
|
Proj.degRad = 180 / Math.PI;
|
|
Proj.radDeg = Math.PI / 180;
|
|
Proj.radius = 6381372;
|
|
|
|
function coordsToPoint(lat, lng) {
|
|
var projection = jsvectormap_esm_Map.maps[this.params.map].projection;
|
|
var _Proj$projection$type = Proj[projection.type](lat, lng, projection.centralMeridian),
|
|
x = _Proj$projection$type.x,
|
|
y = _Proj$projection$type.y;
|
|
var inset = this.getInsetForPoint(x, y);
|
|
if (!inset) {
|
|
return false;
|
|
}
|
|
var bbox = inset.bbox;
|
|
x = (x - bbox[0].x) / (bbox[1].x - bbox[0].x) * inset.width * this.scale;
|
|
y = (y - bbox[0].y) / (bbox[1].y - bbox[0].y) * inset.height * this.scale;
|
|
return {
|
|
x: x + this.transX * this.scale + inset.left * this.scale,
|
|
y: y + this.transY * this.scale + inset.top * this.scale
|
|
};
|
|
}
|
|
|
|
function getInsetForPoint(x, y) {
|
|
var insets = jsvectormap_esm_Map.maps[this.params.map].insets;
|
|
for (var index = 0; index < insets.length; index++) {
|
|
var _insets$index$bbox = insets[index].bbox,
|
|
start = _insets$index$bbox[0],
|
|
end = _insets$index$bbox[1];
|
|
if (x > start.x && x < end.x && y > start.y && y < end.y) {
|
|
return insets[index];
|
|
}
|
|
}
|
|
}
|
|
|
|
function getMarkerPosition(_ref) {
|
|
var coords = _ref.coords;
|
|
if (jsvectormap_esm_Map.maps[this.params.map].projection) {
|
|
return this.coordsToPoint.apply(this, coords);
|
|
}
|
|
return {
|
|
x: coords[0] * this.scale + this.transX * this.scale,
|
|
y: coords[1] * this.scale + this.transY * this.scale
|
|
};
|
|
}
|
|
|
|
function repositionLines() {
|
|
var _this = this;
|
|
var curvature = this.params.lineStyle.curvature;
|
|
Object.values(this._lines).forEach(function (line) {
|
|
var startMarker = Object.values(_this._markers).find(function (_ref) {
|
|
var config = _ref.config;
|
|
return config.name === line.getConfig().from;
|
|
});
|
|
var endMarker = Object.values(_this._markers).find(function (_ref2) {
|
|
var config = _ref2.config;
|
|
return config.name === line.getConfig().to;
|
|
});
|
|
if (startMarker && endMarker) {
|
|
var _this$getMarkerPositi = _this.getMarkerPosition(startMarker.config),
|
|
x1 = _this$getMarkerPositi.x,
|
|
y1 = _this$getMarkerPositi.y;
|
|
var _this$getMarkerPositi2 = _this.getMarkerPosition(endMarker.config),
|
|
x2 = _this$getMarkerPositi2.x,
|
|
y2 = _this$getMarkerPositi2.y;
|
|
var curvatureOption = line._options.curvature == 0 ? 0 : line._options.curvature || curvature;
|
|
var midX = (x1 + x2) / 2;
|
|
var midY = (y1 + y2) / 2;
|
|
var curveX = midX + curvatureOption * (y2 - y1);
|
|
var curveY = midY - curvatureOption * (x2 - x1);
|
|
line.setStyle({
|
|
d: "M" + x1 + "," + y1 + " Q" + curveX + "," + curveY + " " + x2 + "," + y2
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function repositionMarkers() {
|
|
for (var index in this._markers) {
|
|
var point = this.getMarkerPosition(this._markers[index].config);
|
|
if (point !== false) {
|
|
this._markers[index].element.setStyle({
|
|
cx: point.x,
|
|
cy: point.y
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function repositionLabels() {
|
|
var labels = this.params.labels;
|
|
if (!labels) {
|
|
return;
|
|
}
|
|
|
|
// Regions labels
|
|
if (labels.regions) {
|
|
for (var key in this.regions) {
|
|
this.regions[key].element.updateLabelPosition();
|
|
}
|
|
}
|
|
|
|
// Markers labels
|
|
if (labels.markers) {
|
|
for (var _key in this._markers) {
|
|
this._markers[_key].element.updateLabelPosition();
|
|
}
|
|
}
|
|
}
|
|
|
|
var core = {
|
|
_setupContainerEvents: setupContainerEvents,
|
|
_setupElementEvents: setupElementEvents,
|
|
_setupZoomButtons: setupZoomButtons,
|
|
_setupContainerTouchEvents: setupContainerTouchEvents,
|
|
_createRegions: createRegions,
|
|
_createLines: createLines,
|
|
_createMarkers: createMarkers,
|
|
_createSeries: createSeries,
|
|
_applyTransform: applyTransform,
|
|
_resize: resize,
|
|
_setScale: setScale,
|
|
setFocus: setFocus,
|
|
updateSize: updateSize,
|
|
coordsToPoint: coordsToPoint,
|
|
getInsetForPoint: getInsetForPoint,
|
|
getMarkerPosition: getMarkerPosition,
|
|
_repositionLines: repositionLines,
|
|
_repositionMarkers: repositionMarkers,
|
|
_repositionLabels: repositionLabels
|
|
};
|
|
|
|
var jsvectormap_esm_Defaults = {
|
|
map: 'world',
|
|
backgroundColor: 'transparent',
|
|
draggable: true,
|
|
zoomButtons: true,
|
|
zoomOnScroll: true,
|
|
zoomOnScrollSpeed: 3,
|
|
zoomMax: 12,
|
|
zoomMin: 1,
|
|
zoomAnimate: true,
|
|
showTooltip: true,
|
|
zoomStep: 1.5,
|
|
bindTouchEvents: true,
|
|
// Line options
|
|
lineStyle: {
|
|
curvature: 0,
|
|
stroke: '#808080',
|
|
strokeWidth: 1,
|
|
strokeLinecap: 'round'
|
|
},
|
|
// Marker options
|
|
markersSelectable: false,
|
|
markersSelectableOne: false,
|
|
markerStyle: {
|
|
initial: {
|
|
r: 7,
|
|
fill: '#374151',
|
|
fillOpacity: 1,
|
|
stroke: '#FFF',
|
|
strokeWidth: 5,
|
|
strokeOpacity: .5
|
|
},
|
|
hover: {
|
|
fill: '#3cc0ff',
|
|
cursor: 'pointer'
|
|
},
|
|
selected: {
|
|
fill: 'blue'
|
|
},
|
|
selectedHover: {}
|
|
},
|
|
markerLabelStyle: {
|
|
initial: {
|
|
fontFamily: 'Verdana',
|
|
fontSize: 12,
|
|
fontWeight: 500,
|
|
cursor: 'default',
|
|
fill: '#374151'
|
|
},
|
|
hover: {
|
|
cursor: 'pointer'
|
|
},
|
|
selected: {},
|
|
selectedHover: {}
|
|
},
|
|
// Region options
|
|
regionsSelectable: false,
|
|
regionsSelectableOne: false,
|
|
regionStyle: {
|
|
initial: {
|
|
fill: '#dee2e8',
|
|
fillOpacity: 1,
|
|
stroke: 'none',
|
|
strokeWidth: 0
|
|
},
|
|
hover: {
|
|
fillOpacity: .7,
|
|
cursor: 'pointer'
|
|
},
|
|
selected: {
|
|
fill: '#9ca3af'
|
|
},
|
|
selectedHover: {}
|
|
},
|
|
regionLabelStyle: {
|
|
initial: {
|
|
fontFamily: 'Verdana',
|
|
fontSize: '12',
|
|
fontWeight: 'bold',
|
|
cursor: 'default',
|
|
fill: '#35373e'
|
|
},
|
|
hover: {
|
|
cursor: 'pointer'
|
|
}
|
|
}
|
|
};
|
|
|
|
var SVGElement = /*#__PURE__*/function () {
|
|
function SVGElement(name, config) {
|
|
this.node = this._createElement(name);
|
|
if (config) {
|
|
this.set(config);
|
|
}
|
|
}
|
|
|
|
// Create new SVG element `svg`, `g`, `path`, `line`, `circle`, `image`, etc.
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS#important_namespace_uris
|
|
var _proto = SVGElement.prototype;
|
|
_proto._createElement = function _createElement(tagName) {
|
|
return document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
|
};
|
|
_proto.addClass = function addClass(className) {
|
|
this.node.setAttribute('class', className);
|
|
};
|
|
_proto.getBBox = function getBBox() {
|
|
return this.node.getBBox();
|
|
}
|
|
|
|
// Apply attributes on the current node element
|
|
;
|
|
_proto.set = function set(property, value) {
|
|
if (typeof property === 'object') {
|
|
for (var attr in property) {
|
|
this.applyAttr(attr, property[attr]);
|
|
}
|
|
} else {
|
|
this.applyAttr(property, value);
|
|
}
|
|
};
|
|
_proto.get = function get(property) {
|
|
return this.style.initial[property];
|
|
};
|
|
_proto.applyAttr = function applyAttr(property, value) {
|
|
this.node.setAttribute(hyphenate(property), value);
|
|
};
|
|
_proto.remove = function remove() {
|
|
jsvectormap_esm_removeElement(this.node);
|
|
};
|
|
return SVGElement;
|
|
}();
|
|
|
|
var SVGShapeElement = /*#__PURE__*/function (_SVGElement) {
|
|
function SVGShapeElement(name, config, style) {
|
|
var _this;
|
|
if (style === void 0) {
|
|
style = {};
|
|
}
|
|
_this = _SVGElement.call(this, name, config) || this;
|
|
_this.isHovered = false;
|
|
_this.isSelected = false;
|
|
_this.style = style;
|
|
_this.style.current = {};
|
|
_this.updateStyle();
|
|
return _this;
|
|
}
|
|
_inheritsLoose(SVGShapeElement, _SVGElement);
|
|
var _proto = SVGShapeElement.prototype;
|
|
_proto.setStyle = function setStyle(property, value) {
|
|
if (typeof property === 'object') {
|
|
jsvectormap_esm_merge(this.style.current, property);
|
|
} else {
|
|
var _merge;
|
|
jsvectormap_esm_merge(this.style.current, (_merge = {}, _merge[property] = value, _merge));
|
|
}
|
|
this.updateStyle();
|
|
};
|
|
_proto.updateStyle = function updateStyle() {
|
|
var attrs = {};
|
|
jsvectormap_esm_merge(attrs, this.style.initial);
|
|
jsvectormap_esm_merge(attrs, this.style.current);
|
|
if (this.isHovered) {
|
|
jsvectormap_esm_merge(attrs, this.style.hover);
|
|
}
|
|
if (this.isSelected) {
|
|
jsvectormap_esm_merge(attrs, this.style.selected);
|
|
if (this.isHovered) {
|
|
jsvectormap_esm_merge(attrs, this.style.selectedHover);
|
|
}
|
|
}
|
|
this.set(attrs);
|
|
};
|
|
return SVGShapeElement;
|
|
}(SVGElement);
|
|
|
|
var SVGTextElement = /*#__PURE__*/function (_SVGShapeElement) {
|
|
function SVGTextElement(config, style) {
|
|
return _SVGShapeElement.call(this, 'text', config, style) || this;
|
|
}
|
|
_inheritsLoose(SVGTextElement, _SVGShapeElement);
|
|
var _proto = SVGTextElement.prototype;
|
|
_proto.applyAttr = function applyAttr(attr, value) {
|
|
attr === 'text' ? this.node.textContent = value : _SVGShapeElement.prototype.applyAttr.call(this, attr, value);
|
|
};
|
|
return SVGTextElement;
|
|
}(SVGShapeElement);
|
|
|
|
var SVGImageElement = /*#__PURE__*/function (_SVGShapeElement) {
|
|
function SVGImageElement(config, style) {
|
|
return _SVGShapeElement.call(this, 'image', config, style) || this;
|
|
}
|
|
_inheritsLoose(SVGImageElement, _SVGShapeElement);
|
|
var _proto = SVGImageElement.prototype;
|
|
_proto.applyAttr = function applyAttr(attr, value) {
|
|
var imageUrl;
|
|
if (attr === 'image') {
|
|
// This get executed when we have url in series.markers[0].scale.someScale.url
|
|
if (typeof value === 'object') {
|
|
imageUrl = value.url;
|
|
this.offset = value.offset || [0, 0];
|
|
} else {
|
|
imageUrl = value;
|
|
this.offset = [0, 0];
|
|
}
|
|
this.node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', imageUrl);
|
|
|
|
// Set width and height then call this `applyAttr` again
|
|
this.width = 23;
|
|
this.height = 23;
|
|
this.applyAttr('width', this.width);
|
|
this.applyAttr('height', this.height);
|
|
this.applyAttr('x', this.cx - this.width / 2 + this.offset[0]);
|
|
this.applyAttr('y', this.cy - this.height / 2 + this.offset[1]);
|
|
} else if (attr == 'cx') {
|
|
this.cx = value;
|
|
if (this.width) {
|
|
this.applyAttr('x', value - this.width / 2 + this.offset[0]);
|
|
}
|
|
} else if (attr == 'cy') {
|
|
this.cy = value;
|
|
if (this.height) {
|
|
this.applyAttr('y', value - this.height / 2 + this.offset[1]);
|
|
}
|
|
} else {
|
|
// This time Call SVGElement
|
|
_SVGShapeElement.prototype.applyAttr.apply(this, arguments);
|
|
}
|
|
};
|
|
return SVGImageElement;
|
|
}(SVGShapeElement);
|
|
|
|
var SVGCanvasElement = /*#__PURE__*/function (_SVGElement) {
|
|
function SVGCanvasElement(container) {
|
|
var _this;
|
|
_this = _SVGElement.call(this, 'svg') || this; // Create svg element for holding the whole map
|
|
|
|
_this._container = container;
|
|
|
|
// Create the defs element
|
|
_this._defsElement = new SVGElement('defs');
|
|
|
|
// Create group element which will hold the paths (regions)
|
|
_this._rootElement = new SVGElement('g', {
|
|
id: 'jvm-regions-group'
|
|
});
|
|
|
|
// Append the defs element to the this.node (SVG tag)
|
|
_this.node.appendChild(_this._defsElement.node);
|
|
|
|
// Append the group to this.node (SVG tag)
|
|
_this.node.appendChild(_this._rootElement.node);
|
|
|
|
// Append this.node (SVG tag) to the container
|
|
_this._container.appendChild(_this.node);
|
|
return _this;
|
|
}
|
|
_inheritsLoose(SVGCanvasElement, _SVGElement);
|
|
var _proto = SVGCanvasElement.prototype;
|
|
_proto.setSize = function setSize(width, height) {
|
|
this.node.setAttribute('width', width);
|
|
this.node.setAttribute('height', height);
|
|
};
|
|
_proto.applyTransformParams = function applyTransformParams(scale, transX, transY) {
|
|
this._rootElement.node.setAttribute('transform', "scale(" + scale + ") translate(" + transX + ", " + transY + ")");
|
|
}
|
|
|
|
// Create `path` element
|
|
;
|
|
_proto.createPath = function createPath(config, style, group) {
|
|
var path = new SVGShapeElement('path', config, style);
|
|
path.node.setAttribute('fill-rule', 'evenodd');
|
|
return this._add(path, group);
|
|
}
|
|
|
|
// Create `circle` element
|
|
;
|
|
_proto.createCircle = function createCircle(config, style, group) {
|
|
var circle = new SVGShapeElement('circle', config, style);
|
|
return this._add(circle, group);
|
|
}
|
|
|
|
// Create `line` element
|
|
;
|
|
_proto.createLine = function createLine(config, style, group) {
|
|
var line = new SVGShapeElement('line', config, style);
|
|
return this._add(line, group);
|
|
}
|
|
|
|
// Create `text` element
|
|
;
|
|
_proto.createText = function createText(config, style, group) {
|
|
var text = new SVGTextElement(config, style);
|
|
return this._add(text, group);
|
|
}
|
|
|
|
// Create `image` element
|
|
;
|
|
_proto.createImage = function createImage(config, style, group) {
|
|
var image = new SVGImageElement(config, style);
|
|
return this._add(image, group);
|
|
}
|
|
|
|
// Create `g` element
|
|
;
|
|
_proto.createGroup = function createGroup(id) {
|
|
var group = new SVGElement('g');
|
|
this.node.appendChild(group.node);
|
|
if (id) {
|
|
group.node.id = id;
|
|
}
|
|
group.canvas = this;
|
|
return group;
|
|
}
|
|
|
|
// Add some element to a spcific group or the root element if the group isn't given
|
|
;
|
|
_proto._add = function _add(element, group) {
|
|
group = group || this._rootElement;
|
|
group.node.appendChild(element.node);
|
|
return element;
|
|
};
|
|
return SVGCanvasElement;
|
|
}(SVGElement);
|
|
|
|
var jsvectormap_esm_Tooltip = /*#__PURE__*/function (_BaseComponent) {
|
|
function Tooltip(map) {
|
|
var _this;
|
|
_this = _BaseComponent.call(this) || this;
|
|
var tooltip = createElement('div', 'jvm-tooltip');
|
|
_this._map = map;
|
|
_this._tooltip = document.body.appendChild(tooltip);
|
|
_this._bindEventListeners();
|
|
return _this || _assertThisInitialized(_this);
|
|
}
|
|
_inheritsLoose(Tooltip, _BaseComponent);
|
|
var _proto = Tooltip.prototype;
|
|
_proto._bindEventListeners = function _bindEventListeners() {
|
|
var _this2 = this;
|
|
EventHandler.on(this._map.container, 'mousemove', function (event) {
|
|
if (!_this2._tooltip.classList.contains('active')) {
|
|
return;
|
|
}
|
|
var container = findElement(_this2._map.container, '#jvm-regions-group').getBoundingClientRect();
|
|
var space = 5; // Space between the cursor and tooltip element
|
|
|
|
// Tooltip
|
|
var _this2$_tooltip$getBo = _this2._tooltip.getBoundingClientRect(),
|
|
height = _this2$_tooltip$getBo.height,
|
|
width = _this2$_tooltip$getBo.width;
|
|
var topIsPassed = event.clientY <= container.top + height + space;
|
|
var top = event.pageY - height - space;
|
|
var left = event.pageX - width - space;
|
|
|
|
// Ensure the tooltip will never cross outside the canvas area(map)
|
|
if (topIsPassed) {
|
|
// Top:
|
|
top += height + space;
|
|
|
|
// The cursor is a bit larger from left side
|
|
left -= space * 2;
|
|
}
|
|
if (event.clientX < container.left + width + space) {
|
|
// Left:
|
|
left = event.pageX + space + 2;
|
|
if (topIsPassed) {
|
|
left += space * 2;
|
|
}
|
|
}
|
|
_this2.css({
|
|
top: top + "px",
|
|
left: left + "px"
|
|
});
|
|
});
|
|
};
|
|
_proto.getElement = function getElement() {
|
|
return this._tooltip;
|
|
};
|
|
_proto.show = function show() {
|
|
this._tooltip.classList.add('active');
|
|
};
|
|
_proto.hide = function hide() {
|
|
this._tooltip.classList.remove('active');
|
|
};
|
|
_proto.text = function text(string, html) {
|
|
if (html === void 0) {
|
|
html = false;
|
|
}
|
|
var property = html ? 'innerHTML' : 'textContent';
|
|
if (!string) {
|
|
return this._tooltip[property];
|
|
}
|
|
this._tooltip[property] = string;
|
|
};
|
|
_proto.css = function css(_css) {
|
|
for (var style in _css) {
|
|
this._tooltip.style[style] = _css[style];
|
|
}
|
|
return this;
|
|
};
|
|
return Tooltip;
|
|
}(jsvectormap_esm_BaseComponent);
|
|
|
|
var DataVisualization = /*#__PURE__*/function () {
|
|
function DataVisualization(_ref, map) {
|
|
var scale = _ref.scale,
|
|
values = _ref.values;
|
|
this._scale = scale;
|
|
this._values = values;
|
|
this._fromColor = this.hexToRgb(scale[0]);
|
|
this._toColor = this.hexToRgb(scale[1]);
|
|
this._map = map;
|
|
this.setMinMaxValues(values);
|
|
this.visualize();
|
|
}
|
|
var _proto = DataVisualization.prototype;
|
|
_proto.setMinMaxValues = function setMinMaxValues(values) {
|
|
this.min = Number.MAX_VALUE;
|
|
this.max = 0;
|
|
for (var value in values) {
|
|
value = parseFloat(values[value]);
|
|
if (value > this.max) {
|
|
this.max = value;
|
|
}
|
|
if (value < this.min) {
|
|
this.min = value;
|
|
}
|
|
}
|
|
};
|
|
_proto.visualize = function visualize() {
|
|
var attrs = {},
|
|
value;
|
|
for (var regionCode in this._values) {
|
|
value = parseFloat(this._values[regionCode]);
|
|
if (!isNaN(value)) {
|
|
attrs[regionCode] = this.getValue(value);
|
|
}
|
|
}
|
|
this.setAttributes(attrs);
|
|
};
|
|
_proto.setAttributes = function setAttributes(attrs) {
|
|
for (var code in attrs) {
|
|
if (this._map.regions[code]) {
|
|
this._map.regions[code].element.setStyle('fill', attrs[code]);
|
|
}
|
|
}
|
|
};
|
|
_proto.getValue = function getValue(value) {
|
|
if (this.min === this.max) {
|
|
return "#" + this._toColor.join('');
|
|
}
|
|
var hex,
|
|
color = '#';
|
|
for (var i = 0; i < 3; i++) {
|
|
hex = Math.round(this._fromColor[i] + (this._toColor[i] - this._fromColor[i]) * ((value - this.min) / (this.max - this.min))).toString(16);
|
|
color += (hex.length === 1 ? '0' : '') + hex;
|
|
}
|
|
return color;
|
|
};
|
|
_proto.hexToRgb = function hexToRgb(h) {
|
|
var r = 0,
|
|
g = 0,
|
|
b = 0;
|
|
if (h.length == 4) {
|
|
r = '0x' + h[1] + h[1];
|
|
g = '0x' + h[2] + h[2];
|
|
b = '0x' + h[3] + h[3];
|
|
} else if (h.length == 7) {
|
|
r = '0x' + h[1] + h[2];
|
|
g = '0x' + h[3] + h[4];
|
|
b = '0x' + h[5] + h[6];
|
|
}
|
|
return [parseInt(r), parseInt(g), parseInt(b)];
|
|
};
|
|
return DataVisualization;
|
|
}();
|
|
|
|
var JVM_PREFIX = 'jvm-';
|
|
var CONTAINER_CLASS = JVM_PREFIX + "container";
|
|
var MARKERS_GROUP_ID = JVM_PREFIX + "markers-group";
|
|
var MARKERS_LABELS_GROUP_ID = JVM_PREFIX + "markers-labels-group";
|
|
var LINES_GROUP_ID = JVM_PREFIX + "lines-group";
|
|
var SERIES_CONTAINER_CLASS = JVM_PREFIX + "series-container";
|
|
var SERIES_CONTAINER_H_CLASS = SERIES_CONTAINER_CLASS + " " + JVM_PREFIX + "series-h";
|
|
var SERIES_CONTAINER_V_CLASS = SERIES_CONTAINER_CLASS + " " + JVM_PREFIX + "series-v";
|
|
var jsvectormap_esm_Map = /*#__PURE__*/function () {
|
|
function Map(options) {
|
|
var _this = this;
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
// Merge the given options with the default options
|
|
this.params = jsvectormap_esm_merge(Map.defaults, options, true);
|
|
|
|
// Throw an error if the given map name doesn't match
|
|
// the map that was set in map file
|
|
if (!Map.maps[this.params.map]) {
|
|
throw new Error("Attempt to use map which was not loaded: " + options.map);
|
|
}
|
|
this.regions = {};
|
|
this.scale = 1;
|
|
this.transX = 0;
|
|
this.transY = 0;
|
|
this._mapData = Map.maps[this.params.map];
|
|
this._markers = {};
|
|
this._lines = {};
|
|
this._defaultWidth = this._mapData.width;
|
|
this._defaultHeight = this._mapData.height;
|
|
this._height = 0;
|
|
this._width = 0;
|
|
this._baseScale = 1;
|
|
this._baseTransX = 0;
|
|
this._baseTransY = 0;
|
|
|
|
// `document` is already ready, just initialise now
|
|
if (document.readyState !== 'loading') {
|
|
this._init();
|
|
} else {
|
|
// Wait until `document` is ready
|
|
window.addEventListener('DOMContentLoaded', function () {
|
|
return _this._init();
|
|
});
|
|
}
|
|
}
|
|
var _proto = Map.prototype;
|
|
_proto._init = function _init() {
|
|
var options = this.params;
|
|
this.container = getElement(options.selector);
|
|
this.container.classList.add(CONTAINER_CLASS);
|
|
|
|
// The map canvas element
|
|
this.canvas = new SVGCanvasElement(this.container);
|
|
|
|
// Set the map's background color
|
|
this.setBackgroundColor(options.backgroundColor);
|
|
|
|
// Create regions
|
|
this._createRegions();
|
|
|
|
// Update size
|
|
this.updateSize();
|
|
|
|
// Lines group must be created before markers
|
|
// Otherwise the lines will be drawn on top of the markers.
|
|
if (options.lines) {
|
|
this._linesGroup = this.canvas.createGroup(LINES_GROUP_ID);
|
|
}
|
|
if (options.markers) {
|
|
this._markersGroup = this.canvas.createGroup(MARKERS_GROUP_ID);
|
|
this._markerLabelsGroup = this.canvas.createGroup(MARKERS_LABELS_GROUP_ID);
|
|
}
|
|
|
|
// Create markers
|
|
this._createMarkers(options.markers);
|
|
|
|
// Create lines
|
|
this._createLines(options.lines || {});
|
|
|
|
// Position labels
|
|
this._repositionLabels();
|
|
|
|
// Setup the container events
|
|
this._setupContainerEvents();
|
|
|
|
// Setup regions/markers events
|
|
this._setupElementEvents();
|
|
|
|
// Create zoom buttons if `zoomButtons` is presented
|
|
if (options.zoomButtons) {
|
|
this._setupZoomButtons();
|
|
}
|
|
|
|
// Create toolip
|
|
if (options.showTooltip) {
|
|
this._tooltip = new jsvectormap_esm_Tooltip(this);
|
|
}
|
|
|
|
// Set selected regions if any
|
|
if (options.selectedRegions) {
|
|
this._setSelected('regions', options.selectedRegions);
|
|
}
|
|
|
|
// Set selected regions if any
|
|
if (options.selectedMarkers) {
|
|
this._setSelected('_markers', options.selectedMarkers);
|
|
}
|
|
|
|
// Set focus on a spcific region
|
|
if (options.focusOn) {
|
|
this.setFocus(options.focusOn);
|
|
}
|
|
|
|
// Data visualization
|
|
if (options.visualizeData) {
|
|
this.dataVisualization = new DataVisualization(options.visualizeData, this);
|
|
}
|
|
|
|
// Bind touch events if true
|
|
if (options.bindTouchEvents) {
|
|
if ('ontouchstart' in window || window.DocumentTouch && document instanceof DocumentTouch) {
|
|
this._setupContainerTouchEvents();
|
|
}
|
|
}
|
|
|
|
// Create series if any
|
|
if (options.series) {
|
|
this.container.appendChild(this.legendHorizontal = createElement('div', SERIES_CONTAINER_H_CLASS));
|
|
this.container.appendChild(this.legendVertical = createElement('div', SERIES_CONTAINER_V_CLASS));
|
|
this._createSeries();
|
|
}
|
|
|
|
// Fire loaded event
|
|
this._emit(jsvectormap_esm_Events.onLoaded, [this]);
|
|
}
|
|
|
|
// Public
|
|
;
|
|
_proto.setBackgroundColor = function setBackgroundColor(color) {
|
|
this.container.style.backgroundColor = color;
|
|
}
|
|
|
|
// Regions
|
|
;
|
|
_proto.getSelectedRegions = function getSelectedRegions() {
|
|
return this._getSelected('regions');
|
|
};
|
|
_proto.clearSelectedRegions = function clearSelectedRegions(regions) {
|
|
var _this2 = this;
|
|
if (regions === void 0) {
|
|
regions = undefined;
|
|
}
|
|
regions = this._normalizeRegions(regions) || this._getSelected('regions');
|
|
regions.forEach(function (key) {
|
|
_this2.regions[key].element.select(false);
|
|
});
|
|
};
|
|
_proto.setSelectedRegions = function setSelectedRegions(regions) {
|
|
this.clearSelectedRegions();
|
|
this._setSelected('regions', this._normalizeRegions(regions));
|
|
}
|
|
|
|
// Markers
|
|
;
|
|
_proto.getSelectedMarkers = function getSelectedMarkers() {
|
|
return this._getSelected('_markers');
|
|
};
|
|
_proto.clearSelectedMarkers = function clearSelectedMarkers() {
|
|
this._clearSelected('_markers');
|
|
};
|
|
_proto.setSelectedMarkers = function setSelectedMarkers(markers) {
|
|
this._setSelected('_markers', markers);
|
|
};
|
|
_proto.addMarkers = function addMarkers(config) {
|
|
config = Array.isArray(config) ? config : [config];
|
|
this._createMarkers(config, true);
|
|
};
|
|
_proto.removeMarkers = function removeMarkers(markers) {
|
|
var _this3 = this;
|
|
if (!markers) {
|
|
markers = Object.keys(this._markers);
|
|
}
|
|
markers.forEach(function (index) {
|
|
// Remove the element from the DOM
|
|
_this3._markers[index].element.remove();
|
|
// Remove the element from markers object
|
|
delete _this3._markers[index];
|
|
});
|
|
}
|
|
|
|
// Lines
|
|
;
|
|
_proto.addLine = function addLine(from, to, style) {
|
|
if (style === void 0) {
|
|
style = {};
|
|
}
|
|
console.warn('`addLine` method is deprecated, please use `addLines` instead.');
|
|
this._createLines([{
|
|
from: from,
|
|
to: to,
|
|
style: style
|
|
}], this._markers, true);
|
|
};
|
|
_proto.addLines = function addLines(config) {
|
|
var uids = this._getLinesAsUids();
|
|
if (!Array.isArray(config)) {
|
|
config = [config];
|
|
}
|
|
this._createLines(config.filter(function (line) {
|
|
return !(uids.indexOf(getLineUid(line.from, line.to)) > -1);
|
|
}), true);
|
|
};
|
|
_proto.removeLines = function removeLines(lines) {
|
|
var _this4 = this;
|
|
if (Array.isArray(lines)) {
|
|
lines = lines.map(function (line) {
|
|
return getLineUid(line.from, line.to);
|
|
});
|
|
} else {
|
|
lines = this._getLinesAsUids();
|
|
}
|
|
lines.forEach(function (uid) {
|
|
_this4._lines[uid].dispose();
|
|
delete _this4._lines[uid];
|
|
});
|
|
};
|
|
_proto.removeLine = function removeLine(from, to) {
|
|
console.warn('`removeLine` method is deprecated, please use `removeLines` instead.');
|
|
var uid = getLineUid(from, to);
|
|
if (this._lines.hasOwnProperty(uid)) {
|
|
this._lines[uid].element.remove();
|
|
delete this._lines[uid];
|
|
}
|
|
}
|
|
|
|
// Reset map
|
|
;
|
|
_proto.reset = function reset() {
|
|
for (var key in this.series) {
|
|
for (var i = 0; i < this.series[key].length; i++) {
|
|
this.series[key][i].clear();
|
|
}
|
|
}
|
|
if (this.legendHorizontal) {
|
|
jsvectormap_esm_removeElement(this.legendHorizontal);
|
|
this.legendHorizontal = null;
|
|
}
|
|
if (this.legendVertical) {
|
|
jsvectormap_esm_removeElement(this.legendVertical);
|
|
this.legendVertical = null;
|
|
}
|
|
this.scale = this._baseScale;
|
|
this.transX = this._baseTransX;
|
|
this.transY = this._baseTransY;
|
|
this._applyTransform();
|
|
this.clearSelectedMarkers();
|
|
this.clearSelectedRegions();
|
|
this.removeMarkers();
|
|
}
|
|
|
|
// Destroy the map
|
|
;
|
|
_proto.destroy = function destroy(destroyInstance) {
|
|
var _this5 = this;
|
|
if (destroyInstance === void 0) {
|
|
destroyInstance = true;
|
|
}
|
|
// Remove event registry
|
|
EventHandler.flush();
|
|
|
|
// Remove tooltip from DOM and memory
|
|
this._tooltip.dispose();
|
|
|
|
// Fire destroyed event
|
|
this._emit(jsvectormap_esm_Events.onDestroyed);
|
|
|
|
// Remove references
|
|
if (destroyInstance) {
|
|
Object.keys(this).forEach(function (key) {
|
|
try {
|
|
delete _this5[key];
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
};
|
|
_proto.extend = function extend(name, callback) {
|
|
if (typeof this[name] === 'function') {
|
|
throw new Error("The method [" + name + "] does already exist, please use another name.");
|
|
}
|
|
Map.prototype[name] = callback;
|
|
}
|
|
|
|
// Private
|
|
;
|
|
_proto._emit = function _emit(eventName, args) {
|
|
for (var event in jsvectormap_esm_Events) {
|
|
if (jsvectormap_esm_Events[event] === eventName && typeof this.params[event] === 'function') {
|
|
this.params[event].apply(this, args);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get selected markers/regions
|
|
;
|
|
_proto._getSelected = function _getSelected(type) {
|
|
var selected = [];
|
|
for (var key in this[type]) {
|
|
if (this[type][key].element.isSelected) {
|
|
selected.push(key);
|
|
}
|
|
}
|
|
return selected;
|
|
};
|
|
_proto._setSelected = function _setSelected(type, keys) {
|
|
var _this6 = this;
|
|
keys.forEach(function (key) {
|
|
if (_this6[type][key]) {
|
|
_this6[type][key].element.select(true);
|
|
}
|
|
});
|
|
};
|
|
_proto._clearSelected = function _clearSelected(type) {
|
|
var _this7 = this;
|
|
this._getSelected(type).forEach(function (key) {
|
|
_this7[type][key].element.select(false);
|
|
});
|
|
};
|
|
_proto._getLinesAsUids = function _getLinesAsUids() {
|
|
return Object.keys(this._lines);
|
|
};
|
|
_proto._normalizeRegions = function _normalizeRegions(regions) {
|
|
return typeof regions === 'string' ? [regions] : regions;
|
|
};
|
|
return Map;
|
|
}();
|
|
jsvectormap_esm_Map.maps = {};
|
|
jsvectormap_esm_Map.defaults = jsvectormap_esm_Defaults;
|
|
Object.assign(jsvectormap_esm_Map.prototype, core);
|
|
|
|
/**
|
|
* jsVectorMap
|
|
* Copyrights (c) Mustafa Omar https://github.com/themustafaomar
|
|
* Released under the MIT License.
|
|
*/
|
|
var scripts_jsVectorMap = /*#__PURE__*/function () {
|
|
function jsVectorMap(options) {
|
|
if (options === void 0) {
|
|
options = {};
|
|
}
|
|
if (!options.selector) {
|
|
throw new Error('Selector is not given.');
|
|
}
|
|
return new jsvectormap_esm_Map(options);
|
|
}
|
|
|
|
// Public
|
|
jsVectorMap.addMap = function addMap(name, map) {
|
|
jsvectormap_esm_Map.maps[name] = map;
|
|
};
|
|
return jsVectorMap;
|
|
}();
|
|
var jsvectormap_esm_index = window.jsVectorMap = scripts_jsVectorMap;
|
|
|
|
|
|
|
|
// EXTERNAL MODULE: ../../node_modules/jsvectormap/dist/maps/world.js
|
|
var world = __webpack_require__(177);
|
|
;// ../assets/scripts/utils/storage.js
|
|
/**
|
|
* Adminator Storage Utilities
|
|
* Safe localStorage wrapper with error handling
|
|
*
|
|
* @module utils/storage
|
|
*/
|
|
|
|
/**
|
|
* Check if localStorage is available
|
|
* @returns {boolean}
|
|
*/
|
|
const isAvailable = () => {
|
|
try {
|
|
const test = '__storage_test__';
|
|
localStorage.setItem(test, test);
|
|
localStorage.removeItem(test);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* In-memory fallback when localStorage is unavailable
|
|
* @type {Map<string, string>}
|
|
*/
|
|
const memoryStorage = new Map();
|
|
|
|
/**
|
|
* Storage utility with safe localStorage access
|
|
* Falls back to in-memory storage when localStorage is unavailable
|
|
* (e.g., private browsing, storage quota exceeded)
|
|
*
|
|
* @namespace
|
|
*/
|
|
const storage_Storage = {
|
|
/**
|
|
* Check if storage is available
|
|
* @returns {boolean}
|
|
*/
|
|
isAvailable,
|
|
/**
|
|
* Get an item from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {string|null} Stored value or null
|
|
*
|
|
* @example
|
|
* const theme = Storage.get('theme');
|
|
*/
|
|
get(key) {
|
|
try {
|
|
if (isAvailable()) {
|
|
return localStorage.getItem(key);
|
|
}
|
|
return memoryStorage.get(key) ?? null;
|
|
} catch {
|
|
return memoryStorage.get(key) ?? null;
|
|
}
|
|
},
|
|
/**
|
|
* Set an item in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {string} value - Value to store
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.set('theme', 'dark');
|
|
*/
|
|
set(key, value) {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.setItem(key, value);
|
|
return true;
|
|
}
|
|
memoryStorage.set(key, value);
|
|
return true;
|
|
} catch {
|
|
// Fallback to memory storage
|
|
memoryStorage.set(key, value);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Remove an item from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.remove('theme');
|
|
*/
|
|
remove(key) {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.removeItem(key);
|
|
}
|
|
memoryStorage.delete(key);
|
|
return true;
|
|
} catch {
|
|
memoryStorage.delete(key);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Clear all storage
|
|
*
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.clear();
|
|
*/
|
|
clear() {
|
|
try {
|
|
if (isAvailable()) {
|
|
localStorage.clear();
|
|
}
|
|
memoryStorage.clear();
|
|
return true;
|
|
} catch {
|
|
memoryStorage.clear();
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Get a JSON object from storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {*} [defaultValue=null] - Default value if key doesn't exist or parse fails
|
|
* @returns {*} Parsed object or default value
|
|
*
|
|
* @example
|
|
* const settings = Storage.getJSON('settings', { theme: 'light' });
|
|
*/
|
|
getJSON(key) {
|
|
let defaultValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
try {
|
|
const value = this.get(key);
|
|
if (value === null) {
|
|
return defaultValue;
|
|
}
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return defaultValue;
|
|
}
|
|
},
|
|
/**
|
|
* Set a JSON object in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @param {*} value - Value to store (will be JSON stringified)
|
|
* @returns {boolean} Success status
|
|
*
|
|
* @example
|
|
* Storage.setJSON('settings', { theme: 'dark', sidebar: 'expanded' });
|
|
*/
|
|
setJSON(key, value) {
|
|
try {
|
|
return this.set(key, JSON.stringify(value));
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
/**
|
|
* Check if a key exists in storage
|
|
*
|
|
* @param {string} key - Storage key
|
|
* @returns {boolean}
|
|
*
|
|
* @example
|
|
* if (Storage.has('theme')) {
|
|
* // Use stored theme
|
|
* }
|
|
*/
|
|
has(key) {
|
|
return this.get(key) !== null;
|
|
},
|
|
/**
|
|
* Get all keys in storage
|
|
*
|
|
* @returns {string[]} Array of keys
|
|
*
|
|
* @example
|
|
* const keys = Storage.keys();
|
|
*/
|
|
keys() {
|
|
try {
|
|
if (isAvailable()) {
|
|
return Object.keys(localStorage);
|
|
}
|
|
return Array.from(memoryStorage.keys());
|
|
} catch {
|
|
return Array.from(memoryStorage.keys());
|
|
}
|
|
},
|
|
/**
|
|
* Get storage size in bytes (approximate)
|
|
*
|
|
* @returns {number} Size in bytes
|
|
*
|
|
* @example
|
|
* console.log(`Storage used: ${Storage.size()} bytes`);
|
|
*/
|
|
size() {
|
|
try {
|
|
let total = 0;
|
|
const keys = this.keys();
|
|
for (const key of keys) {
|
|
const value = this.get(key);
|
|
if (value) {
|
|
total += key.length + value.length;
|
|
}
|
|
}
|
|
return total * 2; // UTF-16 uses 2 bytes per character
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
};
|
|
/* harmony default export */ const storage = ((/* unused pure expression or super */ null && (storage_Storage)));
|
|
;// ../assets/scripts/utils/sanitize.js
|
|
/**
|
|
* Adminator Sanitization Utilities
|
|
* HTML and input sanitization for security
|
|
*
|
|
* @module utils/sanitize
|
|
*/
|
|
|
|
/**
|
|
* HTML entities map for encoding
|
|
* @type {Object<string, string>}
|
|
*/
|
|
const HTML_ENTITIES = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/',
|
|
'`': '`',
|
|
'=': '='
|
|
};
|
|
|
|
/**
|
|
* Sanitization utilities namespace
|
|
* @namespace
|
|
*/
|
|
const Sanitize = {
|
|
/**
|
|
* Escape HTML entities to prevent XSS
|
|
*
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string safe for HTML insertion
|
|
*
|
|
* @example
|
|
* const safe = Sanitize.html('<script>alert("xss")</script>');
|
|
* // Returns: <script>alert("xss")</script>
|
|
*/
|
|
html(str) {
|
|
if (typeof str !== 'string') {
|
|
return String(str ?? '');
|
|
}
|
|
return str.replace(/[&<>"'`=/]/g, char => HTML_ENTITIES[char]);
|
|
},
|
|
/**
|
|
* Escape string for use in HTML attributes
|
|
*
|
|
* @param {string} str - String to escape
|
|
* @returns {string} Escaped string safe for attribute values
|
|
*
|
|
* @example
|
|
* element.setAttribute('data-name', Sanitize.attr(userInput));
|
|
*/
|
|
attr(str) {
|
|
return this.html(str);
|
|
},
|
|
/**
|
|
* Sanitize URL to prevent javascript: and data: URLs
|
|
*
|
|
* @param {string} url - URL to sanitize
|
|
* @param {string[]} [allowedProtocols=['http:', 'https:', 'mailto:', 'tel:']] - Allowed protocols
|
|
* @returns {string} Sanitized URL or empty string if invalid
|
|
*
|
|
* @example
|
|
* const safeUrl = Sanitize.url(userProvidedUrl);
|
|
* if (safeUrl) {
|
|
* window.location.href = safeUrl;
|
|
* }
|
|
*/
|
|
url(url) {
|
|
let allowedProtocols = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ['http:', 'https:', 'mailto:', 'tel:'];
|
|
if (typeof url !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
// Trim and normalize
|
|
const trimmed = url.trim().toLowerCase();
|
|
|
|
// Block dangerous protocols
|
|
const dangerousProtocols = ['javascript:', 'data:', 'vbscript:'];
|
|
for (const protocol of dangerousProtocols) {
|
|
if (trimmed.startsWith(protocol)) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Check for allowed protocols
|
|
try {
|
|
const parsed = new URL(url, window.location.origin);
|
|
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
// Allow relative URLs
|
|
if (!url.startsWith('/') && !url.startsWith('./') && !url.startsWith('../')) {
|
|
return '';
|
|
}
|
|
}
|
|
return url;
|
|
} catch {
|
|
// If URL parsing fails, check if it's a relative URL
|
|
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../') || url.startsWith('#')) {
|
|
return url;
|
|
}
|
|
return '';
|
|
}
|
|
},
|
|
/**
|
|
* Strip HTML tags from a string
|
|
*
|
|
* @param {string} str - String with HTML
|
|
* @returns {string} String with HTML tags removed
|
|
*
|
|
* @example
|
|
* const text = Sanitize.stripTags('<p>Hello <b>World</b></p>');
|
|
* // Returns: 'Hello World'
|
|
*/
|
|
stripTags(str) {
|
|
if (typeof str !== 'string') {
|
|
return String(str ?? '');
|
|
}
|
|
|
|
// Create a temporary element to leverage browser's HTML parser
|
|
const div = document.createElement('div');
|
|
div.innerHTML = str;
|
|
return div.textContent || div.innerText || '';
|
|
},
|
|
/**
|
|
* Sanitize a string for use in CSS
|
|
*
|
|
* @param {string} str - String to sanitize
|
|
* @returns {string} CSS-safe string
|
|
*
|
|
* @example
|
|
* element.style.setProperty('--custom', Sanitize.css(userInput));
|
|
*/
|
|
css(str) {
|
|
if (typeof str !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
// Remove potentially dangerous CSS values
|
|
return str.replace(/expression\s*\(/gi, '').replace(/url\s*\(/gi, '').replace(/javascript:/gi, '').replace(/[<>"']/g, '');
|
|
},
|
|
/**
|
|
* Sanitize a string for use in a filename
|
|
*
|
|
* @param {string} str - String to sanitize
|
|
* @returns {string} Filename-safe string
|
|
*
|
|
* @example
|
|
* const filename = Sanitize.filename(userInput) + '.txt';
|
|
*/
|
|
filename(str) {
|
|
if (typeof str !== 'string') {
|
|
return '';
|
|
}
|
|
return str.replace(/[/\\?%*:|"<>]/g, '-') // Replace dangerous characters
|
|
.replace(/\.\./g, '-') // Prevent directory traversal
|
|
.replace(/^\./, '_') // Don't start with dot
|
|
.slice(0, 255); // Limit length
|
|
},
|
|
/**
|
|
* Create safe innerHTML by escaping interpolated values
|
|
*
|
|
* @param {TemplateStringsArray} strings - Template literal strings
|
|
* @param {...*} values - Values to interpolate
|
|
* @returns {string} HTML string with escaped values
|
|
*
|
|
* @example
|
|
* element.innerHTML = Sanitize.template`<div>${userInput}</div>`;
|
|
*/
|
|
template(strings) {
|
|
for (var _len = arguments.length, values = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
values[_key - 1] = arguments[_key];
|
|
}
|
|
return strings.reduce((result, str, i) => {
|
|
const value = values[i - 1];
|
|
const escaped = value !== undefined ? this.html(String(value)) : '';
|
|
return result + escaped + str;
|
|
});
|
|
},
|
|
/**
|
|
* Validate and sanitize an email address
|
|
*
|
|
* @param {string} email - Email to validate
|
|
* @returns {string} Sanitized email or empty string if invalid
|
|
*
|
|
* @example
|
|
* const email = Sanitize.email(userInput);
|
|
* if (email) {
|
|
* sendEmail(email);
|
|
* }
|
|
*/
|
|
email(email) {
|
|
if (typeof email !== 'string') {
|
|
return '';
|
|
}
|
|
const trimmed = email.trim().toLowerCase();
|
|
|
|
// Basic email validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(trimmed)) {
|
|
return '';
|
|
}
|
|
|
|
// Additional checks
|
|
if (trimmed.length > 254) {
|
|
return '';
|
|
}
|
|
return trimmed;
|
|
},
|
|
/**
|
|
* Sanitize a number input
|
|
*
|
|
* @param {*} value - Value to sanitize
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.min=-Infinity] - Minimum value
|
|
* @param {number} [options.max=Infinity] - Maximum value
|
|
* @param {number} [options.default=0] - Default if invalid
|
|
* @returns {number} Sanitized number
|
|
*
|
|
* @example
|
|
* const age = Sanitize.number(userInput, { min: 0, max: 120, default: 18 });
|
|
*/
|
|
number(value) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
const {
|
|
min = -Infinity,
|
|
max = Infinity,
|
|
default: defaultValue = 0
|
|
} = options;
|
|
const num = parseFloat(value);
|
|
if (isNaN(num) || !isFinite(num)) {
|
|
return defaultValue;
|
|
}
|
|
return Math.max(min, Math.min(max, num));
|
|
},
|
|
/**
|
|
* Sanitize an integer input
|
|
*
|
|
* @param {*} value - Value to sanitize
|
|
* @param {Object} [options={}] - Options
|
|
* @param {number} [options.min=-Infinity] - Minimum value
|
|
* @param {number} [options.max=Infinity] - Maximum value
|
|
* @param {number} [options.default=0] - Default if invalid
|
|
* @returns {number} Sanitized integer
|
|
*
|
|
* @example
|
|
* const count = Sanitize.integer(userInput, { min: 1, max: 100 });
|
|
*/
|
|
integer(value) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
return Math.floor(this.number(value, options));
|
|
}
|
|
};
|
|
/* harmony default export */ const sanitize = ((/* unused pure expression or super */ null && (Sanitize)));
|
|
;// ../assets/scripts/utils/index.js
|
|
/**
|
|
* Adminator Utilities Index
|
|
* Re-exports all utility modules for convenient importing
|
|
*
|
|
* @module utils
|
|
*/
|
|
|
|
// Re-export all utilities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Legacy initialization code
|
|
/* harmony default export */ const utils = ((function () {
|
|
// ------------------------------------------------------
|
|
// @Window Resize
|
|
// ------------------------------------------------------
|
|
|
|
/**
|
|
* NOTE: Register resize event for Masonry layout
|
|
*/
|
|
const EVENT = document.createEvent('UIEvents');
|
|
window.EVENT = EVENT;
|
|
EVENT.initUIEvent('resize', true, false, window, 0);
|
|
window.addEventListener('load', () => {
|
|
/**
|
|
* Trigger window resize event after page load
|
|
* for recalculation of masonry layout.
|
|
*/
|
|
window.dispatchEvent(EVENT);
|
|
});
|
|
|
|
// ------------------------------------------------------
|
|
// @External Links
|
|
// ------------------------------------------------------
|
|
|
|
// Open external links in new window
|
|
const externalLinks = document.querySelectorAll('a[href^="http"], a[href^="//"]');
|
|
externalLinks.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href && !href.includes(window.location.host)) {
|
|
link.setAttribute('rel', 'noopener noreferrer');
|
|
link.setAttribute('target', '_blank');
|
|
}
|
|
});
|
|
|
|
// ------------------------------------------------------
|
|
// @Resize Trigger
|
|
// ------------------------------------------------------
|
|
|
|
// Trigger resize on any element click
|
|
document.addEventListener('click', () => {
|
|
window.dispatchEvent(window.EVENT);
|
|
});
|
|
})());
|
|
;// ../assets/scripts/vectorMaps/index.js
|
|
|
|
|
|
|
|
|
|
|
|
/* harmony default export */ const vectorMaps = ((function () {
|
|
// Store map instance for cleanup
|
|
let mapInstance = null;
|
|
|
|
// Main initialization function
|
|
const vectorMapInit = () => {
|
|
const worldMapContainer = document.getElementById('world-map-marker');
|
|
if (!worldMapContainer) return;
|
|
|
|
// Remove existing map
|
|
const existingMap = document.getElementById('vmap');
|
|
if (existingMap) {
|
|
existingMap.remove();
|
|
}
|
|
|
|
// Destroy existing map instance
|
|
if (mapInstance) {
|
|
try {
|
|
mapInstance.destroy();
|
|
} catch {
|
|
// Map instance cleanup
|
|
}
|
|
mapInstance = null;
|
|
}
|
|
|
|
// Get current theme colors - using template colors directly
|
|
const isDark = theme.current() === 'dark';
|
|
const colors = {
|
|
backgroundColor: isDark ? '#313644' : '#f9fafb',
|
|
regionColor: isDark ? '#565a5c' : '#e6eaf0',
|
|
borderColor: isDark ? '#72777a' : '#d3d9e3',
|
|
hoverColor: isDark ? '#7774e7' : '#0f9aee',
|
|
selectedColor: isDark ? '#37c936' : '#7774e7',
|
|
markerFill: isDark ? '#0f9aee' : '#7774e7',
|
|
markerStroke: isDark ? '#37c936' : '#0f9aee',
|
|
scaleStart: isDark ? '#b9c2d0' : '#e6eaf0',
|
|
scaleEnd: isDark ? '#0f9aee' : '#007bff',
|
|
textColor: isDark ? '#99abb4' : '#72777a'
|
|
};
|
|
|
|
// Create new map container
|
|
const mapContainer = document.createElement('div');
|
|
mapContainer.id = 'vmap';
|
|
mapContainer.style.height = '490px';
|
|
mapContainer.style.position = 'relative';
|
|
mapContainer.style.overflow = 'hidden';
|
|
mapContainer.style.backgroundColor = colors.backgroundColor;
|
|
mapContainer.style.borderRadius = '8px';
|
|
mapContainer.style.border = `1px solid ${colors.borderColor}`;
|
|
worldMapContainer.appendChild(mapContainer);
|
|
|
|
// Initialize JSVectorMap
|
|
try {
|
|
mapInstance = jsvectormap_esm_index({
|
|
selector: '#vmap',
|
|
map: 'world',
|
|
// Styling options
|
|
backgroundColor: 'transparent',
|
|
// Region styling
|
|
regionStyle: {
|
|
initial: {
|
|
fill: colors.regionColor,
|
|
stroke: colors.borderColor,
|
|
'stroke-width': 1,
|
|
'stroke-opacity': 0.4
|
|
},
|
|
hover: {
|
|
fill: colors.hoverColor,
|
|
cursor: 'pointer'
|
|
},
|
|
selected: {
|
|
fill: colors.selectedColor
|
|
}
|
|
},
|
|
// Marker styling
|
|
markerStyle: {
|
|
initial: {
|
|
r: 7,
|
|
fill: colors.markerFill,
|
|
stroke: colors.markerStroke,
|
|
'stroke-width': 2,
|
|
'stroke-opacity': 0.4
|
|
},
|
|
hover: {
|
|
r: 10,
|
|
fill: colors.hoverColor,
|
|
'stroke-opacity': 0.8,
|
|
cursor: 'pointer'
|
|
}
|
|
},
|
|
// Markers data
|
|
markers: [{
|
|
name: 'INDIA : 350',
|
|
coords: [21.00, 78.00]
|
|
}, {
|
|
name: 'Australia : 250',
|
|
coords: [-33.00, 151.00]
|
|
}, {
|
|
name: 'USA : 250',
|
|
coords: [36.77, -119.41]
|
|
}, {
|
|
name: 'UK : 250',
|
|
coords: [55.37, -3.41]
|
|
}, {
|
|
name: 'UAE : 250',
|
|
coords: [25.20, 55.27]
|
|
}],
|
|
// Simplified approach - remove series for now to test base colors
|
|
// series: {
|
|
// regions: [
|
|
// {
|
|
// attribute: 'fill',
|
|
// scale: [colors.scaleStart, colors.scaleEnd],
|
|
// normalizeFunction: 'polynomial',
|
|
// values: {
|
|
// 'US': 50,
|
|
// 'SA': 30,
|
|
// 'AU': 70,
|
|
// 'IN': 40,
|
|
// 'GB': 60,
|
|
// 'LV': 80,
|
|
// },
|
|
// },
|
|
// ],
|
|
// },
|
|
|
|
// Interaction options
|
|
zoomOnScroll: false,
|
|
zoomButtons: false,
|
|
// Event handlers
|
|
onMarkerTooltipShow(event, tooltip, index) {
|
|
// Safe access to marker data
|
|
const marker = this.markers && this.markers[index];
|
|
const markerName = marker ? marker.name : `Marker ${index + 1}`;
|
|
tooltip.text(markerName);
|
|
},
|
|
onRegionTooltipShow(event, tooltip, code) {
|
|
// Safe access to region data
|
|
const regionName = this.mapData && this.mapData.paths && this.mapData.paths[code] ? this.mapData.paths[code].name || code : code;
|
|
const value = this.series && this.series.regions && this.series.regions[0] && this.series.regions[0].values ? this.series.regions[0].values[code] : null;
|
|
tooltip.text(`${regionName}${value ? `: ${value}` : ''}`);
|
|
},
|
|
onLoaded() {
|
|
// Map loaded successfully
|
|
}
|
|
});
|
|
|
|
// Store instance for theme updates
|
|
worldMapContainer.mapInstance = mapInstance;
|
|
} catch {
|
|
// Error initializing JSVectorMap
|
|
|
|
// Fallback: show a simple message
|
|
mapContainer.innerHTML = `
|
|
<div style="
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
background: ${colors.backgroundColor};
|
|
border: 1px solid ${colors.borderColor};
|
|
border-radius: 8px;
|
|
color: ${colors.textColor};
|
|
font-size: 14px;
|
|
">
|
|
<div style="text-align: center;">
|
|
<div style="font-size: 24px; margin-bottom: 8px;">🗺️</div>
|
|
<div>World Map</div>
|
|
<div style="font-size: 12px; margin-top: 4px;">Interactive map will load here</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
// Theme update function
|
|
const updateMapTheme = () => {
|
|
if (mapInstance) {
|
|
const isDark = theme.current() === 'dark';
|
|
const colors = {
|
|
backgroundColor: isDark ? '#313644' : '#f9fafb',
|
|
regionColor: isDark ? '#565a5c' : '#e6eaf0',
|
|
borderColor: isDark ? '#72777a' : '#d3d9e3',
|
|
hoverColor: isDark ? '#7774e7' : '#0f9aee',
|
|
selectedColor: isDark ? '#37c936' : '#7774e7',
|
|
markerFill: isDark ? '#0f9aee' : '#7774e7',
|
|
markerStroke: isDark ? '#37c936' : '#0f9aee',
|
|
scaleStart: isDark ? '#b9c2d0' : '#e6eaf0',
|
|
scaleEnd: isDark ? '#0f9aee' : '#007bff',
|
|
textColor: isDark ? '#99abb4' : '#72777a'
|
|
};
|
|
try {
|
|
// Update region styles - commented out series for now
|
|
// mapInstance.updateSeries('regions', {
|
|
// attribute: 'fill',
|
|
// scale: [colors.scaleStart, colors.scaleEnd],
|
|
// values: {
|
|
// 'US': 50,
|
|
// 'SA': 30,
|
|
// 'AU': 70,
|
|
// 'IN': 40,
|
|
// 'GB': 60,
|
|
// 'LV': 80,
|
|
// },
|
|
// });
|
|
|
|
// Update container background
|
|
const container = document.getElementById('vmap');
|
|
if (container) {
|
|
container.style.backgroundColor = colors.backgroundColor;
|
|
}
|
|
} catch {
|
|
// Theme update failed, reinitializing map
|
|
vectorMapInit();
|
|
}
|
|
} else {
|
|
vectorMapInit();
|
|
}
|
|
};
|
|
|
|
// Initialize map
|
|
vectorMapInit();
|
|
|
|
// Reinitialize on window resize
|
|
window.addEventListener('resize', events.debounce(vectorMapInit, 300));
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', events.debounce(updateMapTheme, 150));
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
if (mapInstance) {
|
|
try {
|
|
mapInstance.destroy();
|
|
} catch {
|
|
// Map cleanup on unload
|
|
}
|
|
mapInstance = null;
|
|
}
|
|
});
|
|
|
|
// Return public API
|
|
return {
|
|
init: vectorMapInit,
|
|
updateTheme: updateMapTheme,
|
|
getInstance: () => mapInstance
|
|
};
|
|
})());
|
|
;// ../assets/scripts/chat/index.js
|
|
/* harmony default export */ const chat = ((function () {
|
|
const chatSidebarToggle = document.getElementById('chat-sidebar-toggle');
|
|
const chatSidebar = document.getElementById('chat-sidebar');
|
|
if (chatSidebarToggle && chatSidebar) {
|
|
chatSidebarToggle.addEventListener('click', e => {
|
|
chatSidebar.classList.toggle('open');
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
})());
|
|
;// ../assets/scripts/email/index.js
|
|
/* harmony default export */ const email = ((function () {
|
|
// Email side toggle functionality
|
|
const emailSideToggle = document.querySelector('.email-side-toggle');
|
|
const emailApp = document.querySelector('.email-app');
|
|
if (emailSideToggle && emailApp) {
|
|
emailSideToggle.addEventListener('click', e => {
|
|
emailApp.classList.toggle('side-active');
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
|
|
// Email list item and back to mailbox functionality
|
|
const emailListItems = document.querySelectorAll('.email-list-item, .back-to-mailbox');
|
|
const emailContent = document.querySelector('.email-content');
|
|
if (emailListItems.length > 0 && emailContent) {
|
|
emailListItems.forEach(item => {
|
|
item.addEventListener('click', e => {
|
|
emailContent.classList.toggle('open');
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
}
|
|
})());
|
|
// EXTERNAL MODULE: ../../node_modules/load-google-maps-api/index.js
|
|
var load_google_maps_api = __webpack_require__(95);
|
|
var load_google_maps_api_default = /*#__PURE__*/__webpack_require__.n(load_google_maps_api);
|
|
;// ../assets/scripts/googleMaps/index.js
|
|
|
|
|
|
/* harmony default export */ const googleMaps = ((function () {
|
|
let map, marker;
|
|
const initGoogleMap = () => {
|
|
const googleMapElement = document.getElementById('google-map');
|
|
if (googleMapElement) {
|
|
load_google_maps_api_default()({
|
|
key: 'AIzaSyDW8td30_gj6sGXjiMU0ALeMu1SDEwUnEA'
|
|
}).then(() => {
|
|
const latitude = 26.8206;
|
|
const longitude = 30.8025;
|
|
const mapZoom = 5;
|
|
const {
|
|
google
|
|
} = window;
|
|
const mapOptions = {
|
|
center: new google.maps.LatLng(latitude, longitude),
|
|
zoom: mapZoom,
|
|
mapTypeId: google.maps.MapTypeId.ROADMAP,
|
|
styles: [{
|
|
'featureType': 'landscape',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-landscape-hue')
|
|
}, {
|
|
'saturation': 43.400000000000006
|
|
}, {
|
|
'lightness': 37.599999999999994
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.highway',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-highway-hue')
|
|
}, {
|
|
'saturation': -61.8
|
|
}, {
|
|
'lightness': 45.599999999999994
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.arterial',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-road-hue')
|
|
}, {
|
|
'saturation': -100
|
|
}, {
|
|
'lightness': 51.19999999999999
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'road.local',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-road-hue')
|
|
}, {
|
|
'saturation': -100
|
|
}, {
|
|
'lightness': 52
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'water',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-water-hue')
|
|
}, {
|
|
'saturation': -13.200000000000003
|
|
}, {
|
|
'lightness': 2.4000000000000057
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}, {
|
|
'featureType': 'poi',
|
|
'stylers': [{
|
|
'hue': theme.getCSSVar('--gmap-poi-hue')
|
|
}, {
|
|
'saturation': -1.0989010989011234
|
|
}, {
|
|
'lightness': 11.200000000000017
|
|
}, {
|
|
'gamma': 1
|
|
}]
|
|
}]
|
|
};
|
|
map = new google.maps.Map(document.getElementById('google-map'), mapOptions);
|
|
if (marker) {
|
|
marker.setMap(null);
|
|
}
|
|
marker = new google.maps.Marker({
|
|
map,
|
|
position: new google.maps.LatLng(latitude, longitude),
|
|
visible: true
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initialize Google Maps
|
|
initGoogleMap();
|
|
|
|
// Listen for theme changes
|
|
window.addEventListener('adminator:themeChanged', initGoogleMap);
|
|
})());
|
|
;// ../assets/scripts/ui/index.js
|
|
/**
|
|
* UI Page Bootstrap Components
|
|
* Vanilla JavaScript implementations for Bootstrap components
|
|
*/
|
|
|
|
/* harmony default export */ const ui = ((function () {
|
|
// Modal functionality
|
|
class VanillaModal {
|
|
constructor(element) {
|
|
this.element = element;
|
|
this.modal = null;
|
|
this.backdrop = null;
|
|
this.isOpen = false;
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.modal = document.querySelector(this.element.getAttribute('data-bs-target'));
|
|
if (this.modal) {
|
|
this.element.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.show();
|
|
});
|
|
|
|
// Close button functionality
|
|
const closeButtons = this.modal.querySelectorAll('[data-bs-dismiss="modal"]');
|
|
closeButtons.forEach(btn => {
|
|
btn.addEventListener('click', () => this.hide());
|
|
});
|
|
|
|
// Close on backdrop click
|
|
this.modal.addEventListener('click', e => {
|
|
if (e.target === this.modal) {
|
|
this.hide();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
show() {
|
|
if (this.isOpen) return;
|
|
|
|
// Create backdrop
|
|
this.backdrop = document.createElement('div');
|
|
this.backdrop.className = 'modal-backdrop fade show';
|
|
document.body.appendChild(this.backdrop);
|
|
|
|
// Show modal
|
|
this.modal.style.display = 'block';
|
|
this.modal.classList.add('show');
|
|
document.body.classList.add('modal-open');
|
|
this.isOpen = true;
|
|
|
|
// Focus the modal
|
|
this.modal.setAttribute('tabindex', '-1');
|
|
this.modal.focus();
|
|
|
|
// Escape key handler
|
|
this.escapeHandler = e => {
|
|
if (e.key === 'Escape') {
|
|
this.hide();
|
|
}
|
|
};
|
|
document.addEventListener('keydown', this.escapeHandler);
|
|
}
|
|
hide() {
|
|
if (!this.isOpen) return;
|
|
|
|
// Hide modal
|
|
this.modal.classList.remove('show');
|
|
this.modal.style.display = 'none';
|
|
document.body.classList.remove('modal-open');
|
|
|
|
// Remove backdrop
|
|
if (this.backdrop) {
|
|
this.backdrop.remove();
|
|
this.backdrop = null;
|
|
}
|
|
this.isOpen = false;
|
|
|
|
// Remove escape handler
|
|
if (this.escapeHandler) {
|
|
document.removeEventListener('keydown', this.escapeHandler);
|
|
this.escapeHandler = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dropdown functionality
|
|
class VanillaDropdown {
|
|
constructor(element) {
|
|
this.element = element;
|
|
this.menu = null;
|
|
this.isOpen = false;
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.menu = this.element.parentNode.querySelector('.dropdown-menu');
|
|
if (this.menu) {
|
|
this.element.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.toggle();
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', e => {
|
|
if (!this.element.parentNode.contains(e.target)) {
|
|
this.hide();
|
|
}
|
|
});
|
|
|
|
// Close on escape
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape' && this.isOpen) {
|
|
this.hide();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
toggle() {
|
|
if (this.isOpen) {
|
|
this.hide();
|
|
} else {
|
|
this.show();
|
|
}
|
|
}
|
|
show() {
|
|
if (this.isOpen) return;
|
|
|
|
// Close other dropdowns
|
|
document.querySelectorAll('.dropdown-menu.show').forEach(menu => {
|
|
menu.classList.remove('show');
|
|
});
|
|
this.menu.classList.add('show');
|
|
this.element.setAttribute('aria-expanded', 'true');
|
|
this.isOpen = true;
|
|
}
|
|
hide() {
|
|
if (!this.isOpen) return;
|
|
this.menu.classList.remove('show');
|
|
this.element.setAttribute('aria-expanded', 'false');
|
|
this.isOpen = false;
|
|
}
|
|
}
|
|
|
|
// Popover functionality
|
|
class VanillaPopover {
|
|
constructor(element) {
|
|
this.element = element;
|
|
this.popover = null;
|
|
this.isOpen = false;
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.element.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggle();
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', e => {
|
|
if (!this.element.contains(e.target) && (!this.popover || !this.popover.contains(e.target))) {
|
|
this.hide();
|
|
}
|
|
});
|
|
}
|
|
toggle() {
|
|
if (this.isOpen) {
|
|
this.hide();
|
|
} else {
|
|
this.show();
|
|
}
|
|
}
|
|
show() {
|
|
if (this.isOpen) return;
|
|
|
|
// Close other popovers
|
|
document.querySelectorAll('.popover').forEach(popover => {
|
|
popover.remove();
|
|
});
|
|
const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title');
|
|
const content = this.element.getAttribute('data-bs-content');
|
|
this.popover = document.createElement('div');
|
|
this.popover.className = 'popover bs-popover-top show';
|
|
this.popover.style.position = 'absolute';
|
|
this.popover.style.zIndex = '1070';
|
|
this.popover.style.maxWidth = '276px';
|
|
this.popover.style.backgroundColor = '#fff';
|
|
this.popover.style.border = '1px solid rgba(0,0,0,.2)';
|
|
this.popover.style.borderRadius = '6px';
|
|
this.popover.style.boxShadow = '0 5px 10px rgba(0,0,0,.2)';
|
|
let popoverContent = '';
|
|
if (title) {
|
|
popoverContent += `<div class="popover-header" style="padding: 8px 14px; margin-bottom: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; font-weight: 600;">${title}</div>`;
|
|
}
|
|
popoverContent += `<div class="popover-body" style="padding: 9px 14px; word-wrap: break-word;">${content}</div>`;
|
|
this.popover.innerHTML = popoverContent;
|
|
document.body.appendChild(this.popover);
|
|
|
|
// Position popover
|
|
const rect = this.element.getBoundingClientRect();
|
|
this.popover.style.left = `${rect.left + rect.width / 2 - this.popover.offsetWidth / 2}px`;
|
|
this.popover.style.top = `${rect.top - this.popover.offsetHeight - 10}px`;
|
|
this.isOpen = true;
|
|
}
|
|
hide() {
|
|
if (!this.isOpen) return;
|
|
if (this.popover) {
|
|
this.popover.remove();
|
|
this.popover = null;
|
|
}
|
|
this.isOpen = false;
|
|
}
|
|
}
|
|
|
|
// Tooltip functionality
|
|
class VanillaTooltip {
|
|
constructor(element) {
|
|
this.element = element;
|
|
this.tooltip = null;
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.element.addEventListener('mouseenter', () => this.show());
|
|
this.element.addEventListener('mouseleave', () => this.hide());
|
|
this.element.addEventListener('focus', () => this.show());
|
|
this.element.addEventListener('blur', () => this.hide());
|
|
}
|
|
show() {
|
|
if (this.tooltip) return;
|
|
const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title');
|
|
const placement = this.element.getAttribute('data-bs-placement') || 'top';
|
|
if (!title) return;
|
|
this.tooltip = document.createElement('div');
|
|
this.tooltip.className = `tooltip bs-tooltip-${placement} show`;
|
|
this.tooltip.style.position = 'absolute';
|
|
this.tooltip.style.zIndex = '1070';
|
|
this.tooltip.style.maxWidth = '200px';
|
|
this.tooltip.style.padding = '4px 8px';
|
|
this.tooltip.style.fontSize = '12px';
|
|
this.tooltip.style.backgroundColor = '#000';
|
|
this.tooltip.style.color = '#fff';
|
|
this.tooltip.style.borderRadius = '4px';
|
|
this.tooltip.style.pointerEvents = 'none';
|
|
this.tooltip.style.whiteSpace = 'nowrap';
|
|
this.tooltip.innerHTML = `<div class="tooltip-inner">${title}</div>`;
|
|
document.body.appendChild(this.tooltip);
|
|
|
|
// Position tooltip
|
|
const rect = this.element.getBoundingClientRect();
|
|
switch (placement) {
|
|
case 'top':
|
|
this.tooltip.style.left = `${rect.left + rect.width / 2 - this.tooltip.offsetWidth / 2}px`;
|
|
this.tooltip.style.top = `${rect.top - this.tooltip.offsetHeight - 5}px`;
|
|
break;
|
|
case 'bottom':
|
|
this.tooltip.style.left = `${rect.left + rect.width / 2 - this.tooltip.offsetWidth / 2}px`;
|
|
this.tooltip.style.top = `${rect.bottom + 5}px`;
|
|
break;
|
|
case 'left':
|
|
this.tooltip.style.left = `${rect.left - this.tooltip.offsetWidth - 5}px`;
|
|
this.tooltip.style.top = `${rect.top + rect.height / 2 - this.tooltip.offsetHeight / 2}px`;
|
|
break;
|
|
case 'right':
|
|
this.tooltip.style.left = `${rect.right + 5}px`;
|
|
this.tooltip.style.top = `${rect.top + rect.height / 2 - this.tooltip.offsetHeight / 2}px`;
|
|
break;
|
|
}
|
|
}
|
|
hide() {
|
|
if (this.tooltip) {
|
|
this.tooltip.remove();
|
|
this.tooltip = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Accordion functionality
|
|
class VanillaAccordion {
|
|
constructor(element) {
|
|
this.element = element;
|
|
this.accordion = element.closest('.accordion');
|
|
this.target = document.querySelector(element.getAttribute('data-bs-target'));
|
|
this.isOpen = !element.classList.contains('collapsed');
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.element.addEventListener('click', e => {
|
|
e.preventDefault();
|
|
this.toggle();
|
|
});
|
|
}
|
|
toggle() {
|
|
if (this.isOpen) {
|
|
this.hide();
|
|
} else {
|
|
this.show();
|
|
}
|
|
}
|
|
show() {
|
|
if (this.isOpen) return;
|
|
|
|
// Close other accordion items in the same parent
|
|
const parentAccordion = this.accordion;
|
|
if (parentAccordion) {
|
|
const otherItems = parentAccordion.querySelectorAll('.accordion-collapse.show');
|
|
otherItems.forEach(item => {
|
|
if (item !== this.target) {
|
|
item.classList.remove('show');
|
|
const button = parentAccordion.querySelector(`[data-bs-target="#${item.id}"]`);
|
|
if (button) {
|
|
button.classList.add('collapsed');
|
|
button.setAttribute('aria-expanded', 'false');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show this item
|
|
this.target.classList.add('show');
|
|
this.element.classList.remove('collapsed');
|
|
this.element.setAttribute('aria-expanded', 'true');
|
|
this.isOpen = true;
|
|
}
|
|
hide() {
|
|
if (!this.isOpen) return;
|
|
this.target.classList.remove('show');
|
|
this.element.classList.add('collapsed');
|
|
this.element.setAttribute('aria-expanded', 'false');
|
|
this.isOpen = false;
|
|
}
|
|
}
|
|
|
|
// Initialize all components
|
|
const initComponents = () => {
|
|
// Initialize modals
|
|
document.querySelectorAll('[data-bs-toggle="modal"]').forEach(element => {
|
|
new VanillaModal(element);
|
|
});
|
|
|
|
// Initialize dropdowns
|
|
document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(element => {
|
|
new VanillaDropdown(element);
|
|
});
|
|
|
|
// Initialize popovers
|
|
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(element => {
|
|
new VanillaPopover(element);
|
|
});
|
|
|
|
// Initialize tooltips
|
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(element => {
|
|
new VanillaTooltip(element);
|
|
});
|
|
|
|
// Initialize accordions
|
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(element => {
|
|
new VanillaAccordion(element);
|
|
});
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initComponents);
|
|
} else {
|
|
initComponents();
|
|
}
|
|
|
|
// Public API
|
|
return {
|
|
init: initComponents,
|
|
Modal: VanillaModal,
|
|
Dropdown: VanillaDropdown,
|
|
Popover: VanillaPopover,
|
|
Tooltip: VanillaTooltip,
|
|
Accordion: VanillaAccordion
|
|
};
|
|
})());
|
|
;// ../assets/scripts/app.js
|
|
/**
|
|
* Modern Adminator Application
|
|
* Main application entry point with enhanced mobile support
|
|
*
|
|
* @module app
|
|
* @version 2.9.0
|
|
*/
|
|
|
|
// Note: Bootstrap 5 CSS is still available via SCSS imports
|
|
// Bootstrap JS components removed to eliminate jQuery dependency
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Import styles
|
|
|
|
|
|
// Import other modules that don't need immediate modernization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdminatorApp {
|
|
constructor() {
|
|
this.components = new Map();
|
|
this.isInitialized = false;
|
|
this.themeManager = theme;
|
|
this.cleanupFunctions = [];
|
|
|
|
// Initialize when DOM is ready
|
|
DOM.ready(() => {
|
|
this.init();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the application
|
|
*/
|
|
init() {
|
|
if (this.isInitialized) return;
|
|
logger.time('Adminator Init');
|
|
try {
|
|
// Initialize core components
|
|
this.initSidebar();
|
|
this.initCharts();
|
|
this.initDataTables();
|
|
this.initDatePickers();
|
|
this.initTheme();
|
|
this.initMobileEnhancements();
|
|
|
|
// Setup global event listeners using event delegation
|
|
this.setupGlobalEvents();
|
|
this.isInitialized = true;
|
|
logger.timeEnd('Adminator Init');
|
|
logger.info('Application initialized successfully');
|
|
|
|
// Dispatch custom event for other scripts
|
|
events.emit(window, 'adminator:ready', {
|
|
app: this
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error initializing Adminator App', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Sidebar component
|
|
*/
|
|
initSidebar() {
|
|
if (DOM.exists('.sidebar')) {
|
|
const sidebar = new components_Sidebar();
|
|
this.components.set('sidebar', sidebar);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize Chart components
|
|
*/
|
|
initCharts() {
|
|
// Check if we have any chart elements
|
|
const hasCharts = DOM.exists('#sparklinedash') || DOM.exists('.sparkline') || DOM.exists('.sparkbar') || DOM.exists('.sparktri') || DOM.exists('.sparkdisc') || DOM.exists('.sparkbull') || DOM.exists('.sparkbox') || DOM.exists('.easy-pie-chart') || DOM.exists('#line-chart') || DOM.exists('#area-chart') || DOM.exists('#scatter-chart') || DOM.exists('#bar-chart');
|
|
if (hasCharts) {
|
|
const charts = new components_Chart();
|
|
this.components.set('charts', charts);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize DataTables (modern approach)
|
|
*/
|
|
initDataTables() {
|
|
const dataTableElement = DOM.select('#dataTable');
|
|
if (dataTableElement) {
|
|
// For now, use a lightweight approach
|
|
// In future iterations, we can replace with a modern table library
|
|
this.initBasicDataTable(dataTableElement);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic DataTable implementation (placeholder for full modernization)
|
|
*/
|
|
initBasicDataTable(table) {
|
|
// Add basic sorting functionality
|
|
const headers = DOM.selectAll('th', table);
|
|
headers.forEach(header => {
|
|
if (header.textContent.trim()) {
|
|
header.style.cursor = 'pointer';
|
|
header.style.userSelect = 'none';
|
|
DOM.on(header, 'click', () => {
|
|
// Basic sort functionality can be added here
|
|
// For now, we'll keep the existing DataTables library
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize Date Pickers (modern approach with Day.js)
|
|
*/
|
|
initDatePickers() {
|
|
const startDatePickers = DOM.selectAll('.start-date');
|
|
const endDatePickers = DOM.selectAll('.end-date');
|
|
[...startDatePickers, ...endDatePickers].forEach(picker => {
|
|
// Convert to HTML5 date input for better UX
|
|
if (picker.type !== 'date') {
|
|
picker.type = 'date';
|
|
picker.classList.add('form-control');
|
|
|
|
// Clear the placeholder since HTML5 date inputs don't need it
|
|
picker.removeAttribute('placeholder');
|
|
|
|
// Set default value to today if no value is set
|
|
if (!picker.value) {
|
|
picker.value = date.form.toInputValue(date.now());
|
|
}
|
|
|
|
// Make sure the input is clickable and focusable
|
|
picker.style.pointerEvents = 'auto';
|
|
picker.style.cursor = 'pointer';
|
|
|
|
// Ensure proper styling for HTML5 date input
|
|
picker.style.minHeight = '38px';
|
|
picker.style.lineHeight = '1.5';
|
|
|
|
// Date picker converted to HTML5 with Day.js support
|
|
}
|
|
});
|
|
|
|
// Add enhanced interaction - handle both field and icon clicks
|
|
[...startDatePickers, ...endDatePickers].forEach(picker => {
|
|
// Handle direct field clicks
|
|
DOM.on(picker, 'click', event => {
|
|
event.target.focus();
|
|
// For mobile browsers, trigger the date picker
|
|
if (event.target.showPicker && typeof event.target.showPicker === 'function') {
|
|
try {
|
|
event.target.showPicker();
|
|
} catch {
|
|
// Fallback if showPicker is not supported
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle calendar icon clicks (find the icon in the input group)
|
|
const inputGroup = picker.closest('.input-group');
|
|
if (inputGroup) {
|
|
const calendarIcon = inputGroup.querySelector('.input-group-text i.ti-calendar');
|
|
if (calendarIcon) {
|
|
DOM.on(calendarIcon, 'click', event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
picker.focus();
|
|
if (picker.showPicker && typeof picker.showPicker === 'function') {
|
|
try {
|
|
picker.showPicker();
|
|
} catch {
|
|
// Date picker opened via icon click
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize theme system with toggle
|
|
*/
|
|
initTheme() {
|
|
// Initializing theme system
|
|
|
|
// Initialize theme system first
|
|
this.themeManager.init();
|
|
|
|
// Inject theme toggle if missing - with retry mechanism
|
|
setTimeout(() => {
|
|
const navRight = DOM.select('.nav-right');
|
|
// Check for nav-right and theme-toggle existence
|
|
|
|
if (navRight && !DOM.exists('#theme-toggle')) {
|
|
const li = document.createElement('li');
|
|
li.className = 'theme-toggle d-flex ai-c';
|
|
const currentTheme = this.themeManager.current();
|
|
const isDark = currentTheme === 'dark';
|
|
li.innerHTML = `
|
|
<div class="form-check form-switch d-flex ai-c" style="margin: 0; padding: 0;" role="group" aria-label="Theme switcher">
|
|
<label class="form-check-label me-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-right: 8px;">
|
|
<i class="ti-sun" aria-hidden="true" style="margin-right: 4px;"></i><span class="theme-label">Light</span>
|
|
</label>
|
|
<input class="form-check-input" type="checkbox" id="theme-toggle"
|
|
role="switch"
|
|
aria-checked="${isDark}"
|
|
aria-label="Toggle dark mode"
|
|
${isDark ? 'checked' : ''}
|
|
style="margin: 0;">
|
|
<label class="form-check-label ms-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-left: 8px;">
|
|
<span class="theme-label">Dark</span><i class="ti-moon" aria-hidden="true" style="margin-left: 4px;"></i>
|
|
</label>
|
|
</div>
|
|
`;
|
|
|
|
// Insert before user dropdown (last item) - safer approach
|
|
const lastItem = navRight.querySelector('li:last-child');
|
|
if (lastItem && lastItem.parentNode === navRight) {
|
|
navRight.insertBefore(li, lastItem);
|
|
// Theme toggle inserted before last item
|
|
} else {
|
|
navRight.appendChild(li);
|
|
// Theme toggle appended to nav-right (safer approach)
|
|
}
|
|
|
|
// Add toggle functionality
|
|
const toggle = DOM.select('#theme-toggle');
|
|
if (toggle) {
|
|
DOM.on(toggle, 'change', () => {
|
|
const newTheme = toggle.checked ? 'dark' : 'light';
|
|
toggle.setAttribute('aria-checked', toggle.checked ? 'true' : 'false');
|
|
this.themeManager.apply(newTheme);
|
|
});
|
|
|
|
// Listen for theme changes from other sources
|
|
window.addEventListener('adminator:themeChanged', event => {
|
|
const isDark = event.detail.theme === 'dark';
|
|
toggle.checked = isDark;
|
|
toggle.setAttribute('aria-checked', isDark ? 'true' : 'false');
|
|
|
|
// Update charts when theme changes
|
|
const charts = this.components.get('charts');
|
|
if (charts) charts.redrawCharts();
|
|
});
|
|
}
|
|
} else {
|
|
// No nav-right found or theme-toggle already exists
|
|
}
|
|
}, 100); // Wait 100ms for DOM to be fully ready
|
|
}
|
|
|
|
/**
|
|
* Initialize mobile-specific enhancements
|
|
*/
|
|
initMobileEnhancements() {
|
|
// Initializing mobile enhancements
|
|
this.enhanceMobileDropdowns();
|
|
this.enhanceMobileSearch();
|
|
|
|
// Prevent horizontal scroll on mobile
|
|
if (this.isMobile()) {
|
|
document.body.style.overflowX = 'hidden';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup global event listeners using event delegation for performance
|
|
*/
|
|
setupGlobalEvents() {
|
|
// Use event delegation for dropdown clicks (single listener instead of many)
|
|
const dropdownCleanup = events.delegate(document, 'click', '.nav-right .dropdown-toggle', (e, toggle) => this.handleDropdownClick(e, toggle));
|
|
this.cleanupFunctions.push(dropdownCleanup);
|
|
|
|
// Global click handler for closing dropdowns/search
|
|
const globalClickCleanup = events.on(document, 'click', event => {
|
|
this.handleGlobalClick(event);
|
|
});
|
|
this.cleanupFunctions.push(globalClickCleanup);
|
|
|
|
// Window resize handler with debouncing using Events utility
|
|
const debouncedResize = events.debounce(() => this.handleResize(), 250);
|
|
const resizeCleanup = events.on(window, 'resize', debouncedResize);
|
|
this.cleanupFunctions.push(resizeCleanup);
|
|
|
|
// Escape key handler using delegation
|
|
const escapeCleanup = events.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
this.closeAllOverlays();
|
|
}
|
|
});
|
|
this.cleanupFunctions.push(escapeCleanup);
|
|
logger.debug('Global event listeners set up with delegation');
|
|
}
|
|
|
|
/**
|
|
* Handle dropdown toggle clicks
|
|
* @param {Event} e - Click event
|
|
* @param {Element} toggle - The clicked toggle element
|
|
*/
|
|
handleDropdownClick(e, toggle) {
|
|
if (!this.isMobile()) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const dropdown = toggle.closest('.dropdown');
|
|
const menu = dropdown === null || dropdown === void 0 ? void 0 : dropdown.querySelector('.dropdown-menu');
|
|
if (!dropdown || !menu) return;
|
|
|
|
// Close search if open
|
|
this.closeSearch();
|
|
|
|
// Close other dropdowns
|
|
DOM.selectAll('.nav-right .dropdown').forEach(d => {
|
|
if (d !== dropdown) {
|
|
var _d$querySelector;
|
|
d.classList.remove('show');
|
|
(_d$querySelector = d.querySelector('.dropdown-menu')) === null || _d$querySelector === void 0 || _d$querySelector.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Toggle current dropdown
|
|
const isOpen = dropdown.classList.contains('show');
|
|
dropdown.classList.toggle('show', !isOpen);
|
|
menu.classList.toggle('show', !isOpen);
|
|
document.body.style.overflow = isOpen ? '' : 'hidden';
|
|
document.body.classList.toggle('mobile-menu-open', !isOpen);
|
|
}
|
|
|
|
/**
|
|
* Close all overlays (dropdowns, search)
|
|
*/
|
|
closeAllOverlays() {
|
|
// Close dropdowns
|
|
DOM.selectAll('.nav-right .dropdown').forEach(dropdown => {
|
|
var _dropdown$querySelect;
|
|
dropdown.classList.remove('show');
|
|
(_dropdown$querySelect = dropdown.querySelector('.dropdown-menu')) === null || _dropdown$querySelect === void 0 || _dropdown$querySelect.classList.remove('show');
|
|
});
|
|
|
|
// Close search
|
|
this.closeSearch();
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
|
|
/**
|
|
* Close the search overlay
|
|
*/
|
|
closeSearch() {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Reset icon
|
|
const searchIcon = searchBox.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
const field = searchInput.querySelector('input');
|
|
if (field) {
|
|
field.value = '';
|
|
field.blur();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle window resize events
|
|
*/
|
|
handleResize() {
|
|
// Window resized, updating mobile features
|
|
|
|
// Close all mobile-specific overlays when switching to desktop
|
|
if (!this.isMobile()) {
|
|
document.body.style.overflow = '';
|
|
document.body.style.overflowX = '';
|
|
|
|
// Close dropdowns
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
|
|
// Close search
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
}
|
|
} else {
|
|
// Re-enable mobile overflow protection
|
|
document.body.style.overflowX = 'hidden';
|
|
}
|
|
|
|
// Re-apply mobile enhancements
|
|
this.enhanceMobileDropdowns();
|
|
this.enhanceMobileSearch();
|
|
}
|
|
|
|
/**
|
|
* Handle global click events
|
|
*/
|
|
handleGlobalClick(event) {
|
|
// Close mobile dropdowns when clicking outside
|
|
if (!event.target.closest('.dropdown')) {
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Close search when clicking outside
|
|
if (!event.target.closest('.search-box') && !event.target.closest('.search-input')) {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if we're on a mobile device
|
|
*/
|
|
isMobile() {
|
|
return window.innerWidth <= 768;
|
|
}
|
|
|
|
/**
|
|
* Enhanced mobile dropdown handling with improved email layout
|
|
*/
|
|
enhanceMobileDropdowns() {
|
|
if (!this.isMobile()) return;
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
const toggle = dropdown.querySelector('.dropdown-toggle');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (toggle && menu) {
|
|
// Remove existing listeners to prevent duplicates
|
|
const newToggle = toggle.cloneNode(true);
|
|
toggle.replaceWith(newToggle);
|
|
|
|
// Add click functionality for mobile dropdowns
|
|
DOM.on(newToggle, 'click', e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Close search if open
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
searchBox.classList.remove('active');
|
|
searchInput.classList.remove('active');
|
|
}
|
|
|
|
// Close other dropdowns first
|
|
dropdowns.forEach(otherDropdown => {
|
|
if (otherDropdown !== dropdown) {
|
|
otherDropdown.classList.remove('show');
|
|
const otherMenu = otherDropdown.querySelector('.dropdown-menu');
|
|
if (otherMenu) otherMenu.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
// Toggle current dropdown
|
|
const isOpen = dropdown.classList.contains('show');
|
|
if (isOpen) {
|
|
dropdown.classList.remove('show');
|
|
menu.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
} else {
|
|
dropdown.classList.add('show');
|
|
menu.classList.add('show');
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.classList.add('mobile-menu-open');
|
|
}
|
|
});
|
|
|
|
// Enhanced mobile close button functionality
|
|
DOM.on(menu, 'click', e => {
|
|
// Check if clicked on the close area (::before pseudo-element area)
|
|
const rect = menu.getBoundingClientRect();
|
|
const clickY = e.clientY - rect.top;
|
|
|
|
// If clicked in top 50px (close button area)
|
|
if (clickY <= 50) {
|
|
dropdown.classList.remove('show');
|
|
menu.classList.remove('show');
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Close dropdowns on escape key
|
|
DOM.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
document.body.style.overflow = '';
|
|
document.body.classList.remove('mobile-menu-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enhanced mobile search handling - Full-width search bar
|
|
*/
|
|
enhanceMobileSearch() {
|
|
const searchBox = DOM.select('.search-box');
|
|
const searchInput = DOM.select('.search-input');
|
|
if (searchBox && searchInput) {
|
|
const searchToggle = searchBox.querySelector('a');
|
|
const searchField = searchInput.querySelector('input');
|
|
if (searchToggle && searchField) {
|
|
// Setting up full-width search functionality
|
|
|
|
// Remove existing listeners to prevent duplication
|
|
const newSearchToggle = searchToggle.cloneNode(true);
|
|
searchToggle.replaceWith(newSearchToggle);
|
|
DOM.on(newSearchToggle, 'click', e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Full-width search toggle clicked
|
|
|
|
// Close any open dropdowns first
|
|
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.remove('show');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
if (menu) menu.classList.remove('show');
|
|
});
|
|
|
|
// Toggle search state
|
|
const isActive = searchInput.classList.contains('active');
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (isActive) {
|
|
// Close search
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Change icon back to search
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
if (searchField) {
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
|
|
// Full-width search closed
|
|
} else {
|
|
// Open search
|
|
searchInput.classList.add('active');
|
|
document.body.classList.add('search-open');
|
|
|
|
// Change icon to close
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-close';
|
|
}
|
|
|
|
// Focus the input after a short delay
|
|
setTimeout(() => {
|
|
if (searchField) {
|
|
searchField.focus();
|
|
// Search field focused
|
|
}
|
|
}, 100);
|
|
|
|
// Full-width search opened
|
|
}
|
|
});
|
|
|
|
// Close search on escape
|
|
DOM.on(document, 'keydown', e => {
|
|
if (e.key === 'Escape' && searchInput.classList.contains('active')) {
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
|
|
// Reset icon
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
|
|
// Clear input
|
|
if (searchField) {
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
|
|
// Full-width search closed via escape
|
|
}
|
|
});
|
|
|
|
// Handle search input
|
|
DOM.on(searchField, 'keypress', e => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
const query = searchField.value.trim();
|
|
if (query) {
|
|
// Search query submitted
|
|
// Implement your search logic here
|
|
|
|
// For demo, close search after "searching"
|
|
searchInput.classList.remove('active');
|
|
document.body.classList.remove('search-open');
|
|
const searchIcon = newSearchToggle.querySelector('i');
|
|
if (searchIcon) {
|
|
searchIcon.className = 'ti-search';
|
|
}
|
|
searchField.value = '';
|
|
searchField.blur();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Full-width search functionality initialized
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a component by name
|
|
*/
|
|
getComponent(name) {
|
|
return this.components.get(name);
|
|
}
|
|
|
|
/**
|
|
* Check if app is ready
|
|
*/
|
|
isReady() {
|
|
return this.isInitialized;
|
|
}
|
|
|
|
/**
|
|
* Destroy the application and clean up all resources
|
|
*/
|
|
destroy() {
|
|
logger.info('Destroying Adminator App');
|
|
|
|
// Clean up all event listeners
|
|
this.cleanupFunctions.forEach(cleanup => {
|
|
if (typeof cleanup === 'function') {
|
|
cleanup();
|
|
}
|
|
});
|
|
this.cleanupFunctions = [];
|
|
|
|
// Destroy all components
|
|
this.components.forEach((component, name) => {
|
|
if (typeof component.destroy === 'function') {
|
|
component.destroy();
|
|
logger.debug(`Component destroyed: ${name}`);
|
|
}
|
|
});
|
|
this.components.clear();
|
|
|
|
// Cleanup performance observers
|
|
utils_performance.cleanup();
|
|
this.isInitialized = false;
|
|
logger.info('Adminator App destroyed');
|
|
}
|
|
|
|
/**
|
|
* Refresh/reinitialize the application
|
|
*/
|
|
refresh() {
|
|
logger.info('Refreshing Adminator App');
|
|
if (this.isInitialized) {
|
|
this.destroy();
|
|
}
|
|
setTimeout(() => {
|
|
this.init();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// Initialize the application
|
|
const app = new AdminatorApp();
|
|
|
|
// Make app globally available for debugging
|
|
window.AdminatorApp = app;
|
|
|
|
// Export for module usage
|
|
/* harmony default export */ const scripts_app = ((/* unused pure expression or super */ null && (app)));
|
|
;// ../assets/scripts/datatable/index.js
|
|
// DataTable implementation
|
|
|
|
/* harmony default export */ const datatable = ((function () {
|
|
// Vanilla JS DataTable implementation
|
|
class VanillaDataTable {
|
|
constructor(element) {
|
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
this.element = element;
|
|
this.options = {
|
|
sortable: true,
|
|
searchable: true,
|
|
pagination: true,
|
|
pageSize: 10,
|
|
...options
|
|
};
|
|
this.originalData = [];
|
|
this.filteredData = [];
|
|
this.currentPage = 1;
|
|
this.sortColumn = null;
|
|
this.sortDirection = 'asc';
|
|
this.init();
|
|
}
|
|
init() {
|
|
this.extractData();
|
|
this.createControls();
|
|
this.applyStyles();
|
|
this.bindEvents();
|
|
this.render();
|
|
}
|
|
extractData() {
|
|
const rows = this.element.querySelectorAll('tbody tr');
|
|
this.originalData = Array.from(rows).map(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
return Array.from(cells).map(cell => cell.textContent.trim());
|
|
});
|
|
this.filteredData = [...this.originalData];
|
|
}
|
|
createControls() {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'datatable-wrapper';
|
|
|
|
// Create search input
|
|
if (this.options.searchable) {
|
|
const searchWrapper = document.createElement('div');
|
|
searchWrapper.className = 'datatable-search';
|
|
searchWrapper.innerHTML = `
|
|
<label>
|
|
Search:
|
|
<input type="text" class="form-control" placeholder="Search...">
|
|
</label>
|
|
`;
|
|
wrapper.appendChild(searchWrapper);
|
|
}
|
|
|
|
// Create pagination info
|
|
if (this.options.pagination) {
|
|
const infoWrapper = document.createElement('div');
|
|
infoWrapper.className = 'datatable-info';
|
|
wrapper.appendChild(infoWrapper);
|
|
}
|
|
|
|
// Wrap the table
|
|
this.element.parentNode.insertBefore(wrapper, this.element);
|
|
wrapper.appendChild(this.element);
|
|
|
|
// Create pagination controls
|
|
if (this.options.pagination) {
|
|
const paginationWrapper = document.createElement('div');
|
|
paginationWrapper.className = 'datatable-pagination';
|
|
wrapper.appendChild(paginationWrapper);
|
|
}
|
|
this.wrapper = wrapper;
|
|
}
|
|
applyStyles() {
|
|
// Apply Bootstrap-like styles
|
|
this.element.classList.add('table', 'table-striped', 'table-bordered');
|
|
|
|
// Add custom styles
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.datatable-wrapper {
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.datatable-search {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.datatable-search input {
|
|
width: 250px;
|
|
display: inline-block;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.datatable-info {
|
|
margin-top: 15px;
|
|
color: var(--c-text-muted, #6c757d);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.datatable-pagination {
|
|
margin-top: 15px;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.datatable-pagination button {
|
|
background: var(--c-bkg-card, #fff);
|
|
border: 1px solid var(--c-border, #dee2e6);
|
|
color: var(--c-text-base, #333);
|
|
padding: 6px 12px;
|
|
margin: 0 2px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.datatable-pagination button:hover {
|
|
background: var(--c-primary, #007bff);
|
|
color: white;
|
|
}
|
|
|
|
.datatable-pagination button.active {
|
|
background: var(--c-primary, #007bff);
|
|
color: white;
|
|
}
|
|
|
|
.datatable-pagination button:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.datatable-sort {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
position: relative;
|
|
}
|
|
|
|
.datatable-sort:hover {
|
|
background: var(--c-bkg-card, #f8f9fa);
|
|
}
|
|
|
|
.datatable-sort::after {
|
|
content: '↕';
|
|
position: absolute;
|
|
right: 8px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.datatable-sort.asc::after {
|
|
content: '↑';
|
|
opacity: 1;
|
|
}
|
|
|
|
.datatable-sort.desc::after {
|
|
content: '↓';
|
|
opacity: 1;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
bindEvents() {
|
|
// Search functionality
|
|
if (this.options.searchable) {
|
|
const searchInput = this.wrapper.querySelector('.datatable-search input');
|
|
searchInput.addEventListener('input', e => {
|
|
this.search(e.target.value);
|
|
});
|
|
}
|
|
|
|
// Sorting functionality
|
|
if (this.options.sortable) {
|
|
const headers = this.element.querySelectorAll('thead th');
|
|
headers.forEach((header, index) => {
|
|
header.classList.add('datatable-sort');
|
|
header.addEventListener('click', () => {
|
|
this.sort(index);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
search(query) {
|
|
if (!query) {
|
|
this.filteredData = [...this.originalData];
|
|
} else {
|
|
this.filteredData = this.originalData.filter(row => row.some(cell => cell.toLowerCase().includes(query.toLowerCase())));
|
|
}
|
|
this.currentPage = 1;
|
|
this.render();
|
|
}
|
|
sort(columnIndex) {
|
|
if (this.sortColumn === columnIndex) {
|
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.sortColumn = columnIndex;
|
|
this.sortDirection = 'asc';
|
|
}
|
|
this.filteredData.sort((a, b) => {
|
|
const aVal = a[columnIndex];
|
|
const bVal = b[columnIndex];
|
|
|
|
// Try to parse as numbers
|
|
const aNum = parseFloat(aVal);
|
|
const bNum = parseFloat(bVal);
|
|
let comparison = 0;
|
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
comparison = aNum - bNum;
|
|
} else {
|
|
comparison = aVal.localeCompare(bVal);
|
|
}
|
|
return this.sortDirection === 'asc' ? comparison : -comparison;
|
|
});
|
|
this.updateSortHeaders();
|
|
this.render();
|
|
}
|
|
updateSortHeaders() {
|
|
const headers = this.element.querySelectorAll('thead th');
|
|
headers.forEach((header, index) => {
|
|
header.classList.remove('asc', 'desc');
|
|
if (index === this.sortColumn) {
|
|
header.classList.add(this.sortDirection);
|
|
}
|
|
});
|
|
}
|
|
render() {
|
|
const tbody = this.element.querySelector('tbody');
|
|
const startIndex = (this.currentPage - 1) * this.options.pageSize;
|
|
const endIndex = startIndex + this.options.pageSize;
|
|
const pageData = this.filteredData.slice(startIndex, endIndex);
|
|
|
|
// Clear tbody
|
|
tbody.innerHTML = '';
|
|
|
|
// Add rows
|
|
pageData.forEach(rowData => {
|
|
const row = document.createElement('tr');
|
|
rowData.forEach(cellData => {
|
|
const cell = document.createElement('td');
|
|
cell.textContent = cellData;
|
|
row.appendChild(cell);
|
|
});
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Update pagination
|
|
if (this.options.pagination) {
|
|
this.updatePagination();
|
|
}
|
|
|
|
// Update info
|
|
this.updateInfo();
|
|
}
|
|
updatePagination() {
|
|
const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize);
|
|
const paginationWrapper = this.wrapper.querySelector('.datatable-pagination');
|
|
paginationWrapper.innerHTML = '';
|
|
if (totalPages <= 1) return;
|
|
|
|
// Previous button
|
|
const prevBtn = document.createElement('button');
|
|
prevBtn.textContent = 'Previous';
|
|
prevBtn.disabled = this.currentPage === 1;
|
|
prevBtn.addEventListener('click', () => {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
this.render();
|
|
}
|
|
});
|
|
paginationWrapper.appendChild(prevBtn);
|
|
|
|
// Page numbers
|
|
for (let i = 1; i <= totalPages; i++) {
|
|
const pageBtn = document.createElement('button');
|
|
pageBtn.textContent = i;
|
|
pageBtn.classList.toggle('active', i === this.currentPage);
|
|
pageBtn.addEventListener('click', () => {
|
|
this.currentPage = i;
|
|
this.render();
|
|
});
|
|
paginationWrapper.appendChild(pageBtn);
|
|
}
|
|
|
|
// Next button
|
|
const nextBtn = document.createElement('button');
|
|
nextBtn.textContent = 'Next';
|
|
nextBtn.disabled = this.currentPage === totalPages;
|
|
nextBtn.addEventListener('click', () => {
|
|
if (this.currentPage < totalPages) {
|
|
this.currentPage++;
|
|
this.render();
|
|
}
|
|
});
|
|
paginationWrapper.appendChild(nextBtn);
|
|
}
|
|
updateInfo() {
|
|
const infoWrapper = this.wrapper.querySelector('.datatable-info');
|
|
if (!infoWrapper) return;
|
|
const startIndex = (this.currentPage - 1) * this.options.pageSize + 1;
|
|
const endIndex = Math.min(startIndex + this.options.pageSize - 1, this.filteredData.length);
|
|
const total = this.filteredData.length;
|
|
infoWrapper.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries`;
|
|
}
|
|
destroy() {
|
|
if (this.wrapper && this.wrapper.parentNode) {
|
|
this.wrapper.parentNode.replaceChild(this.element, this.wrapper);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize DataTable
|
|
const initializeDataTable = () => {
|
|
const tableElement = document.getElementById('dataTable');
|
|
if (tableElement) {
|
|
// Clean up existing instance
|
|
if (tableElement.dataTableInstance) {
|
|
tableElement.dataTableInstance.destroy();
|
|
}
|
|
|
|
// Create new instance
|
|
const dataTable = new VanillaDataTable(tableElement, {
|
|
sortable: true,
|
|
searchable: true,
|
|
pagination: true,
|
|
pageSize: 10
|
|
});
|
|
|
|
// Store instance for cleanup
|
|
tableElement.dataTableInstance = dataTable;
|
|
}
|
|
};
|
|
|
|
// Initialize on load
|
|
initializeDataTable();
|
|
|
|
// Reinitialize on theme change
|
|
window.addEventListener('adminator:themeChanged', () => {
|
|
setTimeout(initializeDataTable, 100);
|
|
});
|
|
|
|
// Cleanup on page unload
|
|
window.addEventListener('beforeunload', () => {
|
|
const tableElement = document.getElementById('dataTable');
|
|
if (tableElement && tableElement.dataTableInstance) {
|
|
tableElement.dataTableInstance.destroy();
|
|
}
|
|
});
|
|
|
|
// Return public API
|
|
return {
|
|
init: initializeDataTable,
|
|
VanillaDataTable
|
|
};
|
|
})());
|
|
;// ../assets/scripts/index.js
|
|
/**
|
|
* Adminator Admin Template
|
|
* Modern Entry Point - Phase 2 Modernization
|
|
*/
|
|
|
|
// Import the modern application
|
|
|
|
|
|
// Legacy imports that haven't been modernized yet
|
|
// These will be gradually replaced in future iterations
|
|
|
|
// import './datepicker'; // REMOVED: Replaced with modern day.js implementation in app.js
|
|
|
|
// Note: The following have been modernized and are now handled by app.js:
|
|
// - sidebar (now Sidebar component)
|
|
// - charts (now ChartComponent using Chart.js instead of jQuery Sparkline)
|
|
// - Basic DOM utilities (now DOM utils)
|
|
})();
|
|
|
|
/******/ })()
|
|
; |