datepicker.js 80 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550
  1. var Datepicker = (function () {
  2. 'use strict';
  3. function hasProperty(obj, prop) {
  4. return Object.prototype.hasOwnProperty.call(obj, prop);
  5. }
  6. function lastItemOf(arr) {
  7. return arr[arr.length - 1];
  8. }
  9. // push only the items not included in the array
  10. function pushUnique(arr, ...items) {
  11. items.forEach((item) => {
  12. if (arr.includes(item)) {
  13. return;
  14. }
  15. arr.push(item);
  16. });
  17. return arr;
  18. }
  19. function stringToArray(str, separator) {
  20. // convert empty string to an empty array
  21. return str ? str.split(separator) : [];
  22. }
  23. function isInRange(testVal, min, max) {
  24. const minOK = min === undefined || testVal >= min;
  25. const maxOK = max === undefined || testVal <= max;
  26. return minOK && maxOK;
  27. }
  28. function limitToRange(val, min, max) {
  29. if (val < min) {
  30. return min;
  31. }
  32. if (val > max) {
  33. return max;
  34. }
  35. return val;
  36. }
  37. function createTagRepeat(tagName, repeat, attributes = {}, index = 0, html = '') {
  38. const openTagSrc = Object.keys(attributes).reduce((src, attr) => {
  39. let val = attributes[attr];
  40. if (typeof val === 'function') {
  41. val = val(index);
  42. }
  43. return `${src} ${attr}="${val}"`;
  44. }, tagName);
  45. html += `<${openTagSrc}></${tagName}>`;
  46. const next = index + 1;
  47. return next < repeat
  48. ? createTagRepeat(tagName, repeat, attributes, next, html)
  49. : html;
  50. }
  51. // Remove the spacing surrounding tags for HTML parser not to create text nodes
  52. // before/after elements
  53. function optimizeTemplateHTML(html) {
  54. return html.replace(/>\s+/g, '>').replace(/\s+</, '<');
  55. }
  56. function stripTime(timeValue) {
  57. return new Date(timeValue).setHours(0, 0, 0, 0);
  58. }
  59. function today() {
  60. return new Date().setHours(0, 0, 0, 0);
  61. }
  62. // Get the time value of the start of given date or year, month and day
  63. function dateValue(...args) {
  64. switch (args.length) {
  65. case 0:
  66. return today();
  67. case 1:
  68. return stripTime(args[0]);
  69. }
  70. // use setFullYear() to keep 2-digit year from being mapped to 1900-1999
  71. const newDate = new Date(0);
  72. newDate.setFullYear(...args);
  73. return newDate.setHours(0, 0, 0, 0);
  74. }
  75. function addDays(date, amount) {
  76. const newDate = new Date(date);
  77. return newDate.setDate(newDate.getDate() + amount);
  78. }
  79. function addWeeks(date, amount) {
  80. return addDays(date, amount * 7);
  81. }
  82. function addMonths(date, amount) {
  83. // If the day of the date is not in the new month, the last day of the new
  84. // month will be returned. e.g. Jan 31 + 1 month → Feb 28 (not Mar 03)
  85. const newDate = new Date(date);
  86. const monthsToSet = newDate.getMonth() + amount;
  87. let expectedMonth = monthsToSet % 12;
  88. if (expectedMonth < 0) {
  89. expectedMonth += 12;
  90. }
  91. const time = newDate.setMonth(monthsToSet);
  92. return newDate.getMonth() !== expectedMonth ? newDate.setDate(0) : time;
  93. }
  94. function addYears(date, amount) {
  95. // If the date is Feb 29 and the new year is not a leap year, Feb 28 of the
  96. // new year will be returned.
  97. const newDate = new Date(date);
  98. const expectedMonth = newDate.getMonth();
  99. const time = newDate.setFullYear(newDate.getFullYear() + amount);
  100. return expectedMonth === 1 && newDate.getMonth() === 2 ? newDate.setDate(0) : time;
  101. }
  102. // Calculate the distance bettwen 2 days of the week
  103. function dayDiff(day, from) {
  104. return (day - from + 7) % 7;
  105. }
  106. // Get the date of the specified day of the week of given base date
  107. function dayOfTheWeekOf(baseDate, dayOfWeek, weekStart = 0) {
  108. const baseDay = new Date(baseDate).getDay();
  109. return addDays(baseDate, dayDiff(dayOfWeek, weekStart) - dayDiff(baseDay, weekStart));
  110. }
  111. // Get the ISO week of a date
  112. function getWeek(date) {
  113. // start of ISO week is Monday
  114. const thuOfTheWeek = dayOfTheWeekOf(date, 4, 1);
  115. // 1st week == the week where the 4th of January is in
  116. const firstThu = dayOfTheWeekOf(new Date(thuOfTheWeek).setMonth(0, 4), 4, 1);
  117. return Math.round((thuOfTheWeek - firstThu) / 604800000) + 1;
  118. }
  119. // Get the start year of the period of years that includes given date
  120. // years: length of the year period
  121. function startOfYearPeriod(date, years) {
  122. /* @see https://en.wikipedia.org/wiki/Year_zero#ISO_8601 */
  123. const year = new Date(date).getFullYear();
  124. return Math.floor(year / years) * years;
  125. }
  126. // pattern for format parts
  127. const reFormatTokens = /dd?|DD?|mm?|MM?|yy?(?:yy)?/;
  128. // pattern for non date parts
  129. const reNonDateParts = /[\s!-/:-@[-`{-~年月日]+/;
  130. // cache for persed formats
  131. let knownFormats = {};
  132. // parse funtions for date parts
  133. const parseFns = {
  134. y(date, year) {
  135. return new Date(date).setFullYear(parseInt(year, 10));
  136. },
  137. m(date, month, locale) {
  138. const newDate = new Date(date);
  139. let monthIndex = parseInt(month, 10) - 1;
  140. if (isNaN(monthIndex)) {
  141. if (!month) {
  142. return NaN;
  143. }
  144. const monthName = month.toLowerCase();
  145. const compareNames = name => name.toLowerCase().startsWith(monthName);
  146. // compare with both short and full names because some locales have periods
  147. // in the short names (not equal to the first X letters of the full names)
  148. monthIndex = locale.monthsShort.findIndex(compareNames);
  149. if (monthIndex < 0) {
  150. monthIndex = locale.months.findIndex(compareNames);
  151. }
  152. if (monthIndex < 0) {
  153. return NaN;
  154. }
  155. }
  156. newDate.setMonth(monthIndex);
  157. return newDate.getMonth() !== normalizeMonth(monthIndex)
  158. ? newDate.setDate(0)
  159. : newDate.getTime();
  160. },
  161. d(date, day) {
  162. return new Date(date).setDate(parseInt(day, 10));
  163. },
  164. };
  165. // format functions for date parts
  166. const formatFns = {
  167. d(date) {
  168. return date.getDate();
  169. },
  170. dd(date) {
  171. return padZero(date.getDate(), 2);
  172. },
  173. D(date, locale) {
  174. return locale.daysShort[date.getDay()];
  175. },
  176. DD(date, locale) {
  177. return locale.days[date.getDay()];
  178. },
  179. m(date) {
  180. return date.getMonth() + 1;
  181. },
  182. mm(date) {
  183. return padZero(date.getMonth() + 1, 2);
  184. },
  185. M(date, locale) {
  186. return locale.monthsShort[date.getMonth()];
  187. },
  188. MM(date, locale) {
  189. return locale.months[date.getMonth()];
  190. },
  191. y(date) {
  192. return date.getFullYear();
  193. },
  194. yy(date) {
  195. return padZero(date.getFullYear(), 2).slice(-2);
  196. },
  197. yyyy(date) {
  198. return padZero(date.getFullYear(), 4);
  199. },
  200. };
  201. // get month index in normal range (0 - 11) from any number
  202. function normalizeMonth(monthIndex) {
  203. return monthIndex > -1 ? monthIndex % 12 : normalizeMonth(monthIndex + 12);
  204. }
  205. function padZero(num, length) {
  206. return num.toString().padStart(length, '0');
  207. }
  208. function parseFormatString(format) {
  209. if (typeof format !== 'string') {
  210. throw new Error("Invalid date format.");
  211. }
  212. if (format in knownFormats) {
  213. return knownFormats[format];
  214. }
  215. // sprit the format string into parts and seprators
  216. const separators = format.split(reFormatTokens);
  217. const parts = format.match(new RegExp(reFormatTokens, 'g'));
  218. if (separators.length === 0 || !parts) {
  219. throw new Error("Invalid date format.");
  220. }
  221. // collect format functions used in the format
  222. const partFormatters = parts.map(token => formatFns[token]);
  223. // collect parse function keys used in the format
  224. // iterate over parseFns' keys in order to keep the order of the keys.
  225. const partParserKeys = Object.keys(parseFns).reduce((keys, key) => {
  226. const token = parts.find(part => part[0] !== 'D' && part[0].toLowerCase() === key);
  227. if (token) {
  228. keys.push(key);
  229. }
  230. return keys;
  231. }, []);
  232. return knownFormats[format] = {
  233. parser(dateStr, locale) {
  234. const dateParts = dateStr.split(reNonDateParts).reduce((dtParts, part, index) => {
  235. if (part.length > 0 && parts[index]) {
  236. const token = parts[index][0];
  237. if (token === 'M') {
  238. dtParts.m = part;
  239. } else if (token !== 'D') {
  240. dtParts[token] = part;
  241. }
  242. }
  243. return dtParts;
  244. }, {});
  245. // iterate over partParserkeys so that the parsing is made in the oder
  246. // of year, month and day to prevent the day parser from correcting last
  247. // day of month wrongly
  248. return partParserKeys.reduce((origDate, key) => {
  249. const newDate = parseFns[key](origDate, dateParts[key], locale);
  250. // ingnore the part failed to parse
  251. return isNaN(newDate) ? origDate : newDate;
  252. }, today());
  253. },
  254. formatter(date, locale) {
  255. let dateStr = partFormatters.reduce((str, fn, index) => {
  256. return str += `${separators[index]}${fn(date, locale)}`;
  257. }, '');
  258. // separators' length is always parts' length + 1,
  259. return dateStr += lastItemOf(separators);
  260. },
  261. };
  262. }
  263. function parseDate(dateStr, format, locale) {
  264. if (dateStr instanceof Date || typeof dateStr === 'number') {
  265. const date = stripTime(dateStr);
  266. return isNaN(date) ? undefined : date;
  267. }
  268. if (!dateStr) {
  269. return undefined;
  270. }
  271. if (dateStr === 'today') {
  272. return today();
  273. }
  274. if (format && format.toValue) {
  275. const date = format.toValue(dateStr, format, locale);
  276. return isNaN(date) ? undefined : stripTime(date);
  277. }
  278. return parseFormatString(format).parser(dateStr, locale);
  279. }
  280. function formatDate(date, format, locale) {
  281. if (isNaN(date) || (!date && date !== 0)) {
  282. return '';
  283. }
  284. const dateObj = typeof date === 'number' ? new Date(date) : date;
  285. if (format.toDisplay) {
  286. return format.toDisplay(dateObj, format, locale);
  287. }
  288. return parseFormatString(format).formatter(dateObj, locale);
  289. }
  290. const listenerRegistry = new WeakMap();
  291. const {addEventListener, removeEventListener} = EventTarget.prototype;
  292. // Register event listeners to a key object
  293. // listeners: array of listener definitions;
  294. // - each definition must be a flat array of event target and the arguments
  295. // used to call addEventListener() on the target
  296. function registerListeners(keyObj, listeners) {
  297. let registered = listenerRegistry.get(keyObj);
  298. if (!registered) {
  299. registered = [];
  300. listenerRegistry.set(keyObj, registered);
  301. }
  302. listeners.forEach((listener) => {
  303. addEventListener.call(...listener);
  304. registered.push(listener);
  305. });
  306. }
  307. function unregisterListeners(keyObj) {
  308. let listeners = listenerRegistry.get(keyObj);
  309. if (!listeners) {
  310. return;
  311. }
  312. listeners.forEach((listener) => {
  313. removeEventListener.call(...listener);
  314. });
  315. listenerRegistry.delete(keyObj);
  316. }
  317. // Event.composedPath() polyfill for Edge
  318. // based on https://gist.github.com/kleinfreund/e9787d73776c0e3750dcfcdc89f100ec
  319. if (!Event.prototype.composedPath) {
  320. const getComposedPath = (node, path = []) => {
  321. path.push(node);
  322. let parent;
  323. if (node.parentNode) {
  324. parent = node.parentNode;
  325. } else if (node.host) { // ShadowRoot
  326. parent = node.host;
  327. } else if (node.defaultView) { // Document
  328. parent = node.defaultView;
  329. }
  330. return parent ? getComposedPath(parent, path) : path;
  331. };
  332. Event.prototype.composedPath = function () {
  333. return getComposedPath(this.target);
  334. };
  335. }
  336. function findFromPath(path, criteria, currentTarget, index = 0) {
  337. const el = path[index];
  338. if (criteria(el)) {
  339. return el;
  340. } else if (el === currentTarget || !el.parentElement) {
  341. // stop when reaching currentTarget or <html>
  342. return;
  343. }
  344. return findFromPath(path, criteria, currentTarget, index + 1);
  345. }
  346. // Search for the actual target of a delegated event
  347. function findElementInEventPath(ev, selector) {
  348. const criteria = typeof selector === 'function' ? selector : el => el.matches(selector);
  349. return findFromPath(ev.composedPath(), criteria, ev.currentTarget);
  350. }
  351. // default locales
  352. const locales = {
  353. en: {
  354. days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
  355. daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
  356. daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
  357. months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
  358. monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
  359. today: "Today",
  360. clear: "Clear",
  361. titleFormat: "MM y"
  362. }
  363. };
  364. // config options updatable by setOptions() and their default values
  365. const defaultOptions = {
  366. autohide: false,
  367. beforeShowDay: null,
  368. beforeShowDecade: null,
  369. beforeShowMonth: null,
  370. beforeShowYear: null,
  371. calendarWeeks: false,
  372. clearBtn: false,
  373. dateDelimiter: ',',
  374. datesDisabled: [],
  375. daysOfWeekDisabled: [],
  376. daysOfWeekHighlighted: [],
  377. defaultViewDate: undefined, // placeholder, defaults to today() by the program
  378. disableTouchKeyboard: false,
  379. format: 'mm/dd/yyyy',
  380. language: 'en',
  381. maxDate: null,
  382. maxNumberOfDates: 1,
  383. maxView: 3,
  384. minDate: null,
  385. nextArrow: '»',
  386. orientation: 'auto',
  387. pickLevel: 0,
  388. prevArrow: '«',
  389. showDaysOfWeek: true,
  390. showOnClick: true,
  391. showOnFocus: true,
  392. startView: 0,
  393. title: '',
  394. todayBtn: false,
  395. todayBtnMode: 0,
  396. todayHighlight: false,
  397. updateOnBlur: true,
  398. weekStart: 0,
  399. };
  400. const range = document.createRange();
  401. function parseHTML(html) {
  402. return range.createContextualFragment(html);
  403. }
  404. function hideElement(el) {
  405. if (el.style.display === 'none') {
  406. return;
  407. }
  408. // back up the existing display setting in data-style-display
  409. if (el.style.display) {
  410. el.dataset.styleDisplay = el.style.display;
  411. }
  412. el.style.display = 'none';
  413. }
  414. function showElement(el) {
  415. if (el.style.display !== 'none') {
  416. return;
  417. }
  418. if (el.dataset.styleDisplay) {
  419. // restore backed-up dispay property
  420. el.style.display = el.dataset.styleDisplay;
  421. delete el.dataset.styleDisplay;
  422. } else {
  423. el.style.display = '';
  424. }
  425. }
  426. function emptyChildNodes(el) {
  427. if (el.firstChild) {
  428. el.removeChild(el.firstChild);
  429. emptyChildNodes(el);
  430. }
  431. }
  432. function replaceChildNodes(el, newChildNodes) {
  433. emptyChildNodes(el);
  434. if (newChildNodes instanceof DocumentFragment) {
  435. el.appendChild(newChildNodes);
  436. } else if (typeof newChildNodes === 'string') {
  437. el.appendChild(parseHTML(newChildNodes));
  438. } else if (typeof newChildNodes.forEach === 'function') {
  439. newChildNodes.forEach((node) => {
  440. el.appendChild(node);
  441. });
  442. }
  443. }
  444. const {
  445. language: defaultLang,
  446. format: defaultFormat,
  447. weekStart: defaultWeekStart,
  448. } = defaultOptions;
  449. // Reducer function to filter out invalid day-of-week from the input
  450. function sanitizeDOW(dow, day) {
  451. return dow.length < 6 && day >= 0 && day < 7
  452. ? pushUnique(dow, day)
  453. : dow;
  454. }
  455. function calcEndOfWeek(startOfWeek) {
  456. return (startOfWeek + 6) % 7;
  457. }
  458. // validate input date. if invalid, fallback to the original value
  459. function validateDate(value, format, locale, origValue) {
  460. const date = parseDate(value, format, locale);
  461. return date !== undefined ? date : origValue;
  462. }
  463. // Validate viewId. if invalid, fallback to the original value
  464. function validateViewId(value, origValue, max = 3) {
  465. const viewId = parseInt(value, 10);
  466. return viewId >= 0 && viewId <= max ? viewId : origValue;
  467. }
  468. // Create Datepicker configuration to set
  469. function processOptions(options, datepicker) {
  470. const inOpts = Object.assign({}, options);
  471. const config = {};
  472. const locales = datepicker.constructor.locales;
  473. let {
  474. format,
  475. language,
  476. locale,
  477. maxDate,
  478. maxView,
  479. minDate,
  480. pickLevel,
  481. startView,
  482. weekStart,
  483. } = datepicker.config || {};
  484. if (inOpts.language) {
  485. let lang;
  486. if (inOpts.language !== language) {
  487. if (locales[inOpts.language]) {
  488. lang = inOpts.language;
  489. } else {
  490. // Check if langauge + region tag can fallback to the one without
  491. // region (e.g. fr-CA → fr)
  492. lang = inOpts.language.split('-')[0];
  493. if (locales[lang] === undefined) {
  494. lang = false;
  495. }
  496. }
  497. }
  498. delete inOpts.language;
  499. if (lang) {
  500. language = config.language = lang;
  501. // update locale as well when updating language
  502. const origLocale = locale || locales[defaultLang];
  503. // use default language's properties for the fallback
  504. locale = Object.assign({
  505. format: defaultFormat,
  506. weekStart: defaultWeekStart
  507. }, locales[defaultLang]);
  508. if (language !== defaultLang) {
  509. Object.assign(locale, locales[language]);
  510. }
  511. config.locale = locale;
  512. // if format and/or weekStart are the same as old locale's defaults,
  513. // update them to new locale's defaults
  514. if (format === origLocale.format) {
  515. format = config.format = locale.format;
  516. }
  517. if (weekStart === origLocale.weekStart) {
  518. weekStart = config.weekStart = locale.weekStart;
  519. config.weekEnd = calcEndOfWeek(locale.weekStart);
  520. }
  521. }
  522. }
  523. if (inOpts.format) {
  524. const hasToDisplay = typeof inOpts.format.toDisplay === 'function';
  525. const hasToValue = typeof inOpts.format.toValue === 'function';
  526. const validFormatString = reFormatTokens.test(inOpts.format);
  527. if ((hasToDisplay && hasToValue) || validFormatString) {
  528. format = config.format = inOpts.format;
  529. }
  530. delete inOpts.format;
  531. }
  532. //*** dates ***//
  533. // while min and maxDate for "no limit" in the options are better to be null
  534. // (especially when updating), the ones in the config have to be undefined
  535. // because null is treated as 0 (= unix epoch) when comparing with time value
  536. let minDt = minDate;
  537. let maxDt = maxDate;
  538. if (inOpts.minDate !== undefined) {
  539. minDt = inOpts.minDate === null
  540. ? dateValue(0, 0, 1) // set 0000-01-01 to prevent negative values for year
  541. : validateDate(inOpts.minDate, format, locale, minDt);
  542. delete inOpts.minDate;
  543. }
  544. if (inOpts.maxDate !== undefined) {
  545. maxDt = inOpts.maxDate === null
  546. ? undefined
  547. : validateDate(inOpts.maxDate, format, locale, maxDt);
  548. delete inOpts.maxDate;
  549. }
  550. if (maxDt < minDt) {
  551. minDate = config.minDate = maxDt;
  552. maxDate = config.maxDate = minDt;
  553. } else {
  554. if (minDate !== minDt) {
  555. minDate = config.minDate = minDt;
  556. }
  557. if (maxDate !== maxDt) {
  558. maxDate = config.maxDate = maxDt;
  559. }
  560. }
  561. if (inOpts.datesDisabled) {
  562. config.datesDisabled = inOpts.datesDisabled.reduce((dates, dt) => {
  563. const date = parseDate(dt, format, locale);
  564. return date !== undefined ? pushUnique(dates, date) : dates;
  565. }, []);
  566. delete inOpts.datesDisabled;
  567. }
  568. if (inOpts.defaultViewDate !== undefined) {
  569. const viewDate = parseDate(inOpts.defaultViewDate, format, locale);
  570. if (viewDate !== undefined) {
  571. config.defaultViewDate = viewDate;
  572. }
  573. delete inOpts.defaultViewDate;
  574. }
  575. //*** days of week ***//
  576. if (inOpts.weekStart !== undefined) {
  577. const wkStart = Number(inOpts.weekStart) % 7;
  578. if (!isNaN(wkStart)) {
  579. weekStart = config.weekStart = wkStart;
  580. config.weekEnd = calcEndOfWeek(wkStart);
  581. }
  582. delete inOpts.weekStart;
  583. }
  584. if (inOpts.daysOfWeekDisabled) {
  585. config.daysOfWeekDisabled = inOpts.daysOfWeekDisabled.reduce(sanitizeDOW, []);
  586. delete inOpts.daysOfWeekDisabled;
  587. }
  588. if (inOpts.daysOfWeekHighlighted) {
  589. config.daysOfWeekHighlighted = inOpts.daysOfWeekHighlighted.reduce(sanitizeDOW, []);
  590. delete inOpts.daysOfWeekHighlighted;
  591. }
  592. //*** multi date ***//
  593. if (inOpts.maxNumberOfDates !== undefined) {
  594. const maxNumberOfDates = parseInt(inOpts.maxNumberOfDates, 10);
  595. if (maxNumberOfDates >= 0) {
  596. config.maxNumberOfDates = maxNumberOfDates;
  597. config.multidate = maxNumberOfDates !== 1;
  598. }
  599. delete inOpts.maxNumberOfDates;
  600. }
  601. if (inOpts.dateDelimiter) {
  602. config.dateDelimiter = String(inOpts.dateDelimiter);
  603. delete inOpts.dateDelimiter;
  604. }
  605. //*** pick level & view ***//
  606. let newPickLevel = pickLevel;
  607. if (inOpts.pickLevel !== undefined) {
  608. newPickLevel = validateViewId(inOpts.pickLevel, 2);
  609. delete inOpts.pickLevel;
  610. }
  611. if (newPickLevel !== pickLevel) {
  612. pickLevel = config.pickLevel = newPickLevel;
  613. }
  614. let newMaxView = maxView;
  615. if (inOpts.maxView !== undefined) {
  616. newMaxView = validateViewId(inOpts.maxView, maxView);
  617. delete inOpts.maxView;
  618. }
  619. // ensure max view >= pick level
  620. newMaxView = pickLevel > newMaxView ? pickLevel : newMaxView;
  621. if (newMaxView !== maxView) {
  622. maxView = config.maxView = newMaxView;
  623. }
  624. let newStartView = startView;
  625. if (inOpts.startView !== undefined) {
  626. newStartView = validateViewId(inOpts.startView, newStartView);
  627. delete inOpts.startView;
  628. }
  629. // ensure pick level <= start view <= max view
  630. if (newStartView < pickLevel) {
  631. newStartView = pickLevel;
  632. } else if (newStartView > maxView) {
  633. newStartView = maxView;
  634. }
  635. if (newStartView !== startView) {
  636. config.startView = newStartView;
  637. }
  638. //*** template ***//
  639. if (inOpts.prevArrow) {
  640. const prevArrow = parseHTML(inOpts.prevArrow);
  641. if (prevArrow.childNodes.length > 0) {
  642. config.prevArrow = prevArrow.childNodes;
  643. }
  644. delete inOpts.prevArrow;
  645. }
  646. if (inOpts.nextArrow) {
  647. const nextArrow = parseHTML(inOpts.nextArrow);
  648. if (nextArrow.childNodes.length > 0) {
  649. config.nextArrow = nextArrow.childNodes;
  650. }
  651. delete inOpts.nextArrow;
  652. }
  653. //*** misc ***//
  654. if (inOpts.disableTouchKeyboard !== undefined) {
  655. config.disableTouchKeyboard = 'ontouchstart' in document && !!inOpts.disableTouchKeyboard;
  656. delete inOpts.disableTouchKeyboard;
  657. }
  658. if (inOpts.orientation) {
  659. const orientation = inOpts.orientation.toLowerCase().split(/\s+/g);
  660. config.orientation = {
  661. x: orientation.find(x => (x === 'left' || x === 'right')) || 'auto',
  662. y: orientation.find(y => (y === 'top' || y === 'bottom')) || 'auto',
  663. };
  664. delete inOpts.orientation;
  665. }
  666. if (inOpts.todayBtnMode !== undefined) {
  667. switch(inOpts.todayBtnMode) {
  668. case 0:
  669. case 1:
  670. config.todayBtnMode = inOpts.todayBtnMode;
  671. }
  672. delete inOpts.todayBtnMode;
  673. }
  674. //*** copy the rest ***//
  675. Object.keys(inOpts).forEach((key) => {
  676. if (inOpts[key] !== undefined && hasProperty(defaultOptions, key)) {
  677. config[key] = inOpts[key];
  678. }
  679. });
  680. return config;
  681. }
  682. const pickerTemplate = optimizeTemplateHTML(`<div class="datepicker">
  683. <div class="datepicker-picker">
  684. <div class="datepicker-header">
  685. <div class="datepicker-title"></div>
  686. <div class="datepicker-controls">
  687. <button type="button" class="%buttonClass% prev-btn"></button>
  688. <button type="button" class="%buttonClass% view-switch"></button>
  689. <button type="button" class="%buttonClass% next-btn"></button>
  690. </div>
  691. </div>
  692. <div class="datepicker-main"></div>
  693. <div class="datepicker-footer">
  694. <div class="datepicker-controls">
  695. <button type="button" class="%buttonClass% today-btn"></button>
  696. <button type="button" class="%buttonClass% clear-btn"></button>
  697. </div>
  698. </div>
  699. </div>
  700. </div>`);
  701. const daysTemplate = optimizeTemplateHTML(`<div class="days">
  702. <div class="days-of-week">${createTagRepeat('span', 7, {class: 'dow'})}</div>
  703. <div class="datepicker-grid">${createTagRepeat('span', 42)}</div>
  704. </div>`);
  705. const calendarWeeksTemplate = optimizeTemplateHTML(`<div class="calendar-weeks">
  706. <div class="days-of-week"><span class="dow"></span></div>
  707. <div class="weeks">${createTagRepeat('span', 6, {class: 'week'})}</div>
  708. </div>`);
  709. // Base class of the view classes
  710. class View {
  711. constructor(picker, config) {
  712. Object.assign(this, config, {
  713. picker,
  714. element: parseHTML(`<div class="datepicker-view"></div>`).firstChild,
  715. selected: [],
  716. });
  717. this.init(this.picker.datepicker.config);
  718. }
  719. init(options) {
  720. if (options.pickLevel !== undefined) {
  721. this.isMinView = this.id === options.pickLevel;
  722. }
  723. this.setOptions(options);
  724. this.updateFocus();
  725. this.updateSelection();
  726. }
  727. // Execute beforeShow() callback and apply the result to the element
  728. // args:
  729. // - current - current value on the iteration on view rendering
  730. // - timeValue - time value of the date to pass to beforeShow()
  731. performBeforeHook(el, current, timeValue) {
  732. let result = this.beforeShow(new Date(timeValue));
  733. switch (typeof result) {
  734. case 'boolean':
  735. result = {enabled: result};
  736. break;
  737. case 'string':
  738. result = {classes: result};
  739. }
  740. if (result) {
  741. if (result.enabled === false) {
  742. el.classList.add('disabled');
  743. pushUnique(this.disabled, current);
  744. }
  745. if (result.classes) {
  746. const extraClasses = result.classes.split(/\s+/);
  747. el.classList.add(...extraClasses);
  748. if (extraClasses.includes('disabled')) {
  749. pushUnique(this.disabled, current);
  750. }
  751. }
  752. if (result.content) {
  753. replaceChildNodes(el, result.content);
  754. }
  755. }
  756. }
  757. }
  758. class DaysView extends View {
  759. constructor(picker) {
  760. super(picker, {
  761. id: 0,
  762. name: 'days',
  763. cellClass: 'day',
  764. });
  765. }
  766. init(options, onConstruction = true) {
  767. if (onConstruction) {
  768. const inner = parseHTML(daysTemplate).firstChild;
  769. this.dow = inner.firstChild;
  770. this.grid = inner.lastChild;
  771. this.element.appendChild(inner);
  772. }
  773. super.init(options);
  774. }
  775. setOptions(options) {
  776. let updateDOW;
  777. if (hasProperty(options, 'minDate')) {
  778. this.minDate = options.minDate;
  779. }
  780. if (hasProperty(options, 'maxDate')) {
  781. this.maxDate = options.maxDate;
  782. }
  783. if (options.datesDisabled) {
  784. this.datesDisabled = options.datesDisabled;
  785. }
  786. if (options.daysOfWeekDisabled) {
  787. this.daysOfWeekDisabled = options.daysOfWeekDisabled;
  788. updateDOW = true;
  789. }
  790. if (options.daysOfWeekHighlighted) {
  791. this.daysOfWeekHighlighted = options.daysOfWeekHighlighted;
  792. }
  793. if (options.todayHighlight !== undefined) {
  794. this.todayHighlight = options.todayHighlight;
  795. }
  796. if (options.weekStart !== undefined) {
  797. this.weekStart = options.weekStart;
  798. this.weekEnd = options.weekEnd;
  799. updateDOW = true;
  800. }
  801. if (options.locale) {
  802. const locale = this.locale = options.locale;
  803. this.dayNames = locale.daysMin;
  804. this.switchLabelFormat = locale.titleFormat;
  805. updateDOW = true;
  806. }
  807. if (options.beforeShowDay !== undefined) {
  808. this.beforeShow = typeof options.beforeShowDay === 'function'
  809. ? options.beforeShowDay
  810. : undefined;
  811. }
  812. if (options.calendarWeeks !== undefined) {
  813. if (options.calendarWeeks && !this.calendarWeeks) {
  814. const weeksElem = parseHTML(calendarWeeksTemplate).firstChild;
  815. this.calendarWeeks = {
  816. element: weeksElem,
  817. dow: weeksElem.firstChild,
  818. weeks: weeksElem.lastChild,
  819. };
  820. this.element.insertBefore(weeksElem, this.element.firstChild);
  821. } else if (this.calendarWeeks && !options.calendarWeeks) {
  822. this.element.removeChild(this.calendarWeeks.element);
  823. this.calendarWeeks = null;
  824. }
  825. }
  826. if (options.showDaysOfWeek !== undefined) {
  827. if (options.showDaysOfWeek) {
  828. showElement(this.dow);
  829. if (this.calendarWeeks) {
  830. showElement(this.calendarWeeks.dow);
  831. }
  832. } else {
  833. hideElement(this.dow);
  834. if (this.calendarWeeks) {
  835. hideElement(this.calendarWeeks.dow);
  836. }
  837. }
  838. }
  839. // update days-of-week when locale, daysOfweekDisabled or weekStart is changed
  840. if (updateDOW) {
  841. Array.from(this.dow.children).forEach((el, index) => {
  842. const dow = (this.weekStart + index) % 7;
  843. el.textContent = this.dayNames[dow];
  844. el.className = this.daysOfWeekDisabled.includes(dow) ? 'dow disabled' : 'dow';
  845. });
  846. }
  847. }
  848. // Apply update on the focused date to view's settings
  849. updateFocus() {
  850. const viewDate = new Date(this.picker.viewDate);
  851. const viewYear = viewDate.getFullYear();
  852. const viewMonth = viewDate.getMonth();
  853. const firstOfMonth = dateValue(viewYear, viewMonth, 1);
  854. const start = dayOfTheWeekOf(firstOfMonth, this.weekStart, this.weekStart);
  855. this.first = firstOfMonth;
  856. this.last = dateValue(viewYear, viewMonth + 1, 0);
  857. this.start = start;
  858. this.focused = this.picker.viewDate;
  859. }
  860. // Apply update on the selected dates to view's settings
  861. updateSelection() {
  862. const {dates, rangepicker} = this.picker.datepicker;
  863. this.selected = dates;
  864. if (rangepicker) {
  865. this.range = rangepicker.dates;
  866. }
  867. }
  868. // Update the entire view UI
  869. render() {
  870. // update today marker on ever render
  871. this.today = this.todayHighlight ? today() : undefined;
  872. // refresh disabled dates on every render in order to clear the ones added
  873. // by beforeShow hook at previous render
  874. this.disabled = [...this.datesDisabled];
  875. const switchLabel = formatDate(this.focused, this.switchLabelFormat, this.locale);
  876. this.picker.setViewSwitchLabel(switchLabel);
  877. this.picker.setPrevBtnDisabled(this.first <= this.minDate);
  878. this.picker.setNextBtnDisabled(this.last >= this.maxDate);
  879. if (this.calendarWeeks) {
  880. // start of the UTC week (Monday) of the 1st of the month
  881. const startOfWeek = dayOfTheWeekOf(this.first, 1, 1);
  882. Array.from(this.calendarWeeks.weeks.children).forEach((el, index) => {
  883. el.textContent = getWeek(addWeeks(startOfWeek, index));
  884. });
  885. }
  886. Array.from(this.grid.children).forEach((el, index) => {
  887. const classList = el.classList;
  888. const current = addDays(this.start, index);
  889. const date = new Date(current);
  890. const day = date.getDay();
  891. el.className = `datepicker-cell ${this.cellClass}`;
  892. el.dataset.date = current;
  893. el.textContent = date.getDate();
  894. if (current < this.first) {
  895. classList.add('prev');
  896. } else if (current > this.last) {
  897. classList.add('next');
  898. }
  899. if (this.today === current) {
  900. classList.add('today');
  901. }
  902. if (current < this.minDate || current > this.maxDate || this.disabled.includes(current)) {
  903. classList.add('disabled');
  904. }
  905. if (this.daysOfWeekDisabled.includes(day)) {
  906. classList.add('disabled');
  907. pushUnique(this.disabled, current);
  908. }
  909. if (this.daysOfWeekHighlighted.includes(day)) {
  910. classList.add('highlighted');
  911. }
  912. if (this.range) {
  913. const [rangeStart, rangeEnd] = this.range;
  914. if (current > rangeStart && current < rangeEnd) {
  915. classList.add('range');
  916. }
  917. if (current === rangeStart) {
  918. classList.add('range-start');
  919. }
  920. if (current === rangeEnd) {
  921. classList.add('range-end');
  922. }
  923. }
  924. if (this.selected.includes(current)) {
  925. classList.add('selected');
  926. }
  927. if (current === this.focused) {
  928. classList.add('focused');
  929. }
  930. if (this.beforeShow) {
  931. this.performBeforeHook(el, current, current);
  932. }
  933. });
  934. }
  935. // Update the view UI by applying the changes of selected and focused items
  936. refresh() {
  937. const [rangeStart, rangeEnd] = this.range || [];
  938. this.grid
  939. .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
  940. .forEach((el) => {
  941. el.classList.remove('range', 'range-start', 'range-end', 'selected', 'focused');
  942. });
  943. Array.from(this.grid.children).forEach((el) => {
  944. const current = Number(el.dataset.date);
  945. const classList = el.classList;
  946. if (current > rangeStart && current < rangeEnd) {
  947. classList.add('range');
  948. }
  949. if (current === rangeStart) {
  950. classList.add('range-start');
  951. }
  952. if (current === rangeEnd) {
  953. classList.add('range-end');
  954. }
  955. if (this.selected.includes(current)) {
  956. classList.add('selected');
  957. }
  958. if (current === this.focused) {
  959. classList.add('focused');
  960. }
  961. });
  962. }
  963. // Update the view UI by applying the change of focused item
  964. refreshFocus() {
  965. const index = Math.round((this.focused - this.start) / 86400000);
  966. this.grid.querySelectorAll('.focused').forEach((el) => {
  967. el.classList.remove('focused');
  968. });
  969. this.grid.children[index].classList.add('focused');
  970. }
  971. }
  972. function computeMonthRange(range, thisYear) {
  973. if (!range || !range[0] || !range[1]) {
  974. return;
  975. }
  976. const [[startY, startM], [endY, endM]] = range;
  977. if (startY > thisYear || endY < thisYear) {
  978. return;
  979. }
  980. return [
  981. startY === thisYear ? startM : -1,
  982. endY === thisYear ? endM : 12,
  983. ];
  984. }
  985. class MonthsView extends View {
  986. constructor(picker) {
  987. super(picker, {
  988. id: 1,
  989. name: 'months',
  990. cellClass: 'month',
  991. });
  992. }
  993. init(options, onConstruction = true) {
  994. if (onConstruction) {
  995. this.grid = this.element;
  996. this.element.classList.add('months', 'datepicker-grid');
  997. this.grid.appendChild(parseHTML(createTagRepeat('span', 12, {'data-month': ix => ix})));
  998. }
  999. super.init(options);
  1000. }
  1001. setOptions(options) {
  1002. if (options.locale) {
  1003. this.monthNames = options.locale.monthsShort;
  1004. }
  1005. if (hasProperty(options, 'minDate')) {
  1006. if (options.minDate === undefined) {
  1007. this.minYear = this.minMonth = this.minDate = undefined;
  1008. } else {
  1009. const minDateObj = new Date(options.minDate);
  1010. this.minYear = minDateObj.getFullYear();
  1011. this.minMonth = minDateObj.getMonth();
  1012. this.minDate = minDateObj.setDate(1);
  1013. }
  1014. }
  1015. if (hasProperty(options, 'maxDate')) {
  1016. if (options.maxDate === undefined) {
  1017. this.maxYear = this.maxMonth = this.maxDate = undefined;
  1018. } else {
  1019. const maxDateObj = new Date(options.maxDate);
  1020. this.maxYear = maxDateObj.getFullYear();
  1021. this.maxMonth = maxDateObj.getMonth();
  1022. this.maxDate = dateValue(this.maxYear, this.maxMonth + 1, 0);
  1023. }
  1024. }
  1025. if (options.beforeShowMonth !== undefined) {
  1026. this.beforeShow = typeof options.beforeShowMonth === 'function'
  1027. ? options.beforeShowMonth
  1028. : undefined;
  1029. }
  1030. }
  1031. // Update view's settings to reflect the viewDate set on the picker
  1032. updateFocus() {
  1033. const viewDate = new Date(this.picker.viewDate);
  1034. this.year = viewDate.getFullYear();
  1035. this.focused = viewDate.getMonth();
  1036. }
  1037. // Update view's settings to reflect the selected dates
  1038. updateSelection() {
  1039. const {dates, rangepicker} = this.picker.datepicker;
  1040. this.selected = dates.reduce((selected, timeValue) => {
  1041. const date = new Date(timeValue);
  1042. const year = date.getFullYear();
  1043. const month = date.getMonth();
  1044. if (selected[year] === undefined) {
  1045. selected[year] = [month];
  1046. } else {
  1047. pushUnique(selected[year], month);
  1048. }
  1049. return selected;
  1050. }, {});
  1051. if (rangepicker && rangepicker.dates) {
  1052. this.range = rangepicker.dates.map(timeValue => {
  1053. const date = new Date(timeValue);
  1054. return isNaN(date) ? undefined : [date.getFullYear(), date.getMonth()];
  1055. });
  1056. }
  1057. }
  1058. // Update the entire view UI
  1059. render() {
  1060. // refresh disabled months on every render in order to clear the ones added
  1061. // by beforeShow hook at previous render
  1062. this.disabled = [];
  1063. this.picker.setViewSwitchLabel(this.year);
  1064. this.picker.setPrevBtnDisabled(this.year <= this.minYear);
  1065. this.picker.setNextBtnDisabled(this.year >= this.maxYear);
  1066. const selected = this.selected[this.year] || [];
  1067. const yrOutOfRange = this.year < this.minYear || this.year > this.maxYear;
  1068. const isMinYear = this.year === this.minYear;
  1069. const isMaxYear = this.year === this.maxYear;
  1070. const range = computeMonthRange(this.range, this.year);
  1071. Array.from(this.grid.children).forEach((el, index) => {
  1072. const classList = el.classList;
  1073. const date = dateValue(this.year, index, 1);
  1074. el.className = `datepicker-cell ${this.cellClass}`;
  1075. if (this.isMinView) {
  1076. el.dataset.date = date;
  1077. }
  1078. // reset text on every render to clear the custom content set
  1079. // by beforeShow hook at previous render
  1080. el.textContent = this.monthNames[index];
  1081. if (
  1082. yrOutOfRange
  1083. || isMinYear && index < this.minMonth
  1084. || isMaxYear && index > this.maxMonth
  1085. ) {
  1086. classList.add('disabled');
  1087. }
  1088. if (range) {
  1089. const [rangeStart, rangeEnd] = range;
  1090. if (index > rangeStart && index < rangeEnd) {
  1091. classList.add('range');
  1092. }
  1093. if (index === rangeStart) {
  1094. classList.add('range-start');
  1095. }
  1096. if (index === rangeEnd) {
  1097. classList.add('range-end');
  1098. }
  1099. }
  1100. if (selected.includes(index)) {
  1101. classList.add('selected');
  1102. }
  1103. if (index === this.focused) {
  1104. classList.add('focused');
  1105. }
  1106. if (this.beforeShow) {
  1107. this.performBeforeHook(el, index, date);
  1108. }
  1109. });
  1110. }
  1111. // Update the view UI by applying the changes of selected and focused items
  1112. refresh() {
  1113. const selected = this.selected[this.year] || [];
  1114. const [rangeStart, rangeEnd] = computeMonthRange(this.range, this.year) || [];
  1115. this.grid
  1116. .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
  1117. .forEach((el) => {
  1118. el.classList.remove('range', 'range-start', 'range-end', 'selected', 'focused');
  1119. });
  1120. Array.from(this.grid.children).forEach((el, index) => {
  1121. const classList = el.classList;
  1122. if (index > rangeStart && index < rangeEnd) {
  1123. classList.add('range');
  1124. }
  1125. if (index === rangeStart) {
  1126. classList.add('range-start');
  1127. }
  1128. if (index === rangeEnd) {
  1129. classList.add('range-end');
  1130. }
  1131. if (selected.includes(index)) {
  1132. classList.add('selected');
  1133. }
  1134. if (index === this.focused) {
  1135. classList.add('focused');
  1136. }
  1137. });
  1138. }
  1139. // Update the view UI by applying the change of focused item
  1140. refreshFocus() {
  1141. this.grid.querySelectorAll('.focused').forEach((el) => {
  1142. el.classList.remove('focused');
  1143. });
  1144. this.grid.children[this.focused].classList.add('focused');
  1145. }
  1146. }
  1147. function toTitleCase(word) {
  1148. return [...word].reduce((str, ch, ix) => str += ix ? ch : ch.toUpperCase(), '');
  1149. }
  1150. // Class representing the years and decades view elements
  1151. class YearsView extends View {
  1152. constructor(picker, config) {
  1153. super(picker, config);
  1154. }
  1155. init(options, onConstruction = true) {
  1156. if (onConstruction) {
  1157. this.navStep = this.step * 10;
  1158. this.beforeShowOption = `beforeShow${toTitleCase(this.cellClass)}`;
  1159. this.grid = this.element;
  1160. this.element.classList.add(this.name, 'datepicker-grid');
  1161. this.grid.appendChild(parseHTML(createTagRepeat('span', 12)));
  1162. }
  1163. super.init(options);
  1164. }
  1165. setOptions(options) {
  1166. if (hasProperty(options, 'minDate')) {
  1167. if (options.minDate === undefined) {
  1168. this.minYear = this.minDate = undefined;
  1169. } else {
  1170. this.minYear = startOfYearPeriod(options.minDate, this.step);
  1171. this.minDate = dateValue(this.minYear, 0, 1);
  1172. }
  1173. }
  1174. if (hasProperty(options, 'maxDate')) {
  1175. if (options.maxDate === undefined) {
  1176. this.maxYear = this.maxDate = undefined;
  1177. } else {
  1178. this.maxYear = startOfYearPeriod(options.maxDate, this.step);
  1179. this.maxDate = dateValue(this.maxYear, 11, 31);
  1180. }
  1181. }
  1182. if (options[this.beforeShowOption] !== undefined) {
  1183. const beforeShow = options[this.beforeShowOption];
  1184. this.beforeShow = typeof beforeShow === 'function' ? beforeShow : undefined;
  1185. }
  1186. }
  1187. // Update view's settings to reflect the viewDate set on the picker
  1188. updateFocus() {
  1189. const viewDate = new Date(this.picker.viewDate);
  1190. const first = startOfYearPeriod(viewDate, this.navStep);
  1191. const last = first + 9 * this.step;
  1192. this.first = first;
  1193. this.last = last;
  1194. this.start = first - this.step;
  1195. this.focused = startOfYearPeriod(viewDate, this.step);
  1196. }
  1197. // Update view's settings to reflect the selected dates
  1198. updateSelection() {
  1199. const {dates, rangepicker} = this.picker.datepicker;
  1200. this.selected = dates.reduce((years, timeValue) => {
  1201. return pushUnique(years, startOfYearPeriod(timeValue, this.step));
  1202. }, []);
  1203. if (rangepicker && rangepicker.dates) {
  1204. this.range = rangepicker.dates.map(timeValue => {
  1205. if (timeValue !== undefined) {
  1206. return startOfYearPeriod(timeValue, this.step);
  1207. }
  1208. });
  1209. }
  1210. }
  1211. // Update the entire view UI
  1212. render() {
  1213. // refresh disabled years on every render in order to clear the ones added
  1214. // by beforeShow hook at previous render
  1215. this.disabled = [];
  1216. this.picker.setViewSwitchLabel(`${this.first}-${this.last}`);
  1217. this.picker.setPrevBtnDisabled(this.first <= this.minYear);
  1218. this.picker.setNextBtnDisabled(this.last >= this.maxYear);
  1219. Array.from(this.grid.children).forEach((el, index) => {
  1220. const classList = el.classList;
  1221. const current = this.start + (index * this.step);
  1222. const date = dateValue(current, 0, 1);
  1223. el.className = `datepicker-cell ${this.cellClass}`;
  1224. if (this.isMinView) {
  1225. el.dataset.date = date;
  1226. }
  1227. el.textContent = el.dataset.year = current;
  1228. if (index === 0) {
  1229. classList.add('prev');
  1230. } else if (index === 11) {
  1231. classList.add('next');
  1232. }
  1233. if (current < this.minYear || current > this.maxYear) {
  1234. classList.add('disabled');
  1235. }
  1236. if (this.range) {
  1237. const [rangeStart, rangeEnd] = this.range;
  1238. if (current > rangeStart && current < rangeEnd) {
  1239. classList.add('range');
  1240. }
  1241. if (current === rangeStart) {
  1242. classList.add('range-start');
  1243. }
  1244. if (current === rangeEnd) {
  1245. classList.add('range-end');
  1246. }
  1247. }
  1248. if (this.selected.includes(current)) {
  1249. classList.add('selected');
  1250. }
  1251. if (current === this.focused) {
  1252. classList.add('focused');
  1253. }
  1254. if (this.beforeShow) {
  1255. this.performBeforeHook(el, current, date);
  1256. }
  1257. });
  1258. }
  1259. // Update the view UI by applying the changes of selected and focused items
  1260. refresh() {
  1261. const [rangeStart, rangeEnd] = this.range || [];
  1262. this.grid
  1263. .querySelectorAll('.range, .range-start, .range-end, .selected, .focused')
  1264. .forEach((el) => {
  1265. el.classList.remove('range', 'range-start', 'range-end', 'selected', 'focused');
  1266. });
  1267. Array.from(this.grid.children).forEach((el) => {
  1268. const current = Number(el.textContent);
  1269. const classList = el.classList;
  1270. if (current > rangeStart && current < rangeEnd) {
  1271. classList.add('range');
  1272. }
  1273. if (current === rangeStart) {
  1274. classList.add('range-start');
  1275. }
  1276. if (current === rangeEnd) {
  1277. classList.add('range-end');
  1278. }
  1279. if (this.selected.includes(current)) {
  1280. classList.add('selected');
  1281. }
  1282. if (current === this.focused) {
  1283. classList.add('focused');
  1284. }
  1285. });
  1286. }
  1287. // Update the view UI by applying the change of focused item
  1288. refreshFocus() {
  1289. const index = Math.round((this.focused - this.start) / this.step);
  1290. this.grid.querySelectorAll('.focused').forEach((el) => {
  1291. el.classList.remove('focused');
  1292. });
  1293. this.grid.children[index].classList.add('focused');
  1294. }
  1295. }
  1296. function triggerDatepickerEvent(datepicker, type) {
  1297. const detail = {
  1298. date: datepicker.getDate(),
  1299. viewDate: new Date(datepicker.picker.viewDate),
  1300. viewId: datepicker.picker.currentView.id,
  1301. datepicker,
  1302. };
  1303. datepicker.element.dispatchEvent(new CustomEvent(type, {detail}));
  1304. }
  1305. // direction: -1 (to previous), 1 (to next)
  1306. function goToPrevOrNext(datepicker, direction) {
  1307. const {minDate, maxDate} = datepicker.config;
  1308. const {currentView, viewDate} = datepicker.picker;
  1309. let newViewDate;
  1310. switch (currentView.id) {
  1311. case 0:
  1312. newViewDate = addMonths(viewDate, direction);
  1313. break;
  1314. case 1:
  1315. newViewDate = addYears(viewDate, direction);
  1316. break;
  1317. default:
  1318. newViewDate = addYears(viewDate, direction * currentView.navStep);
  1319. }
  1320. newViewDate = limitToRange(newViewDate, minDate, maxDate);
  1321. datepicker.picker.changeFocus(newViewDate).render();
  1322. }
  1323. function switchView(datepicker) {
  1324. const viewId = datepicker.picker.currentView.id;
  1325. if (viewId === datepicker.config.maxView) {
  1326. return;
  1327. }
  1328. datepicker.picker.changeView(viewId + 1).render();
  1329. }
  1330. function unfocus(datepicker) {
  1331. if (datepicker.config.updateOnBlur) {
  1332. datepicker.update({autohide: true});
  1333. } else {
  1334. datepicker.refresh('input');
  1335. datepicker.hide();
  1336. }
  1337. }
  1338. function goToSelectedMonthOrYear(datepicker, selection) {
  1339. const picker = datepicker.picker;
  1340. const viewDate = new Date(picker.viewDate);
  1341. const viewId = picker.currentView.id;
  1342. const newDate = viewId === 1
  1343. ? addMonths(viewDate, selection - viewDate.getMonth())
  1344. : addYears(viewDate, selection - viewDate.getFullYear());
  1345. picker.changeFocus(newDate).changeView(viewId - 1).render();
  1346. }
  1347. function onClickTodayBtn(datepicker) {
  1348. const picker = datepicker.picker;
  1349. const currentDate = today();
  1350. if (datepicker.config.todayBtnMode === 1) {
  1351. if (datepicker.config.autohide) {
  1352. datepicker.setDate(currentDate);
  1353. return;
  1354. }
  1355. datepicker.setDate(currentDate, {render: false});
  1356. picker.update();
  1357. }
  1358. if (picker.viewDate !== currentDate) {
  1359. picker.changeFocus(currentDate);
  1360. }
  1361. picker.changeView(0).render();
  1362. }
  1363. function onClickClearBtn(datepicker) {
  1364. datepicker.setDate({clear: true});
  1365. }
  1366. function onClickViewSwitch(datepicker) {
  1367. switchView(datepicker);
  1368. }
  1369. function onClickPrevBtn(datepicker) {
  1370. goToPrevOrNext(datepicker, -1);
  1371. }
  1372. function onClickNextBtn(datepicker) {
  1373. goToPrevOrNext(datepicker, 1);
  1374. }
  1375. // For the picker's main block to delegete the events from `datepicker-cell`s
  1376. function onClickView(datepicker, ev) {
  1377. const target = findElementInEventPath(ev, '.datepicker-cell');
  1378. if (!target || target.classList.contains('disabled')) {
  1379. return;
  1380. }
  1381. const {id, isMinView} = datepicker.picker.currentView;
  1382. if (isMinView) {
  1383. datepicker.setDate(Number(target.dataset.date));
  1384. } else if (id === 1) {
  1385. goToSelectedMonthOrYear(datepicker, Number(target.dataset.month));
  1386. } else {
  1387. goToSelectedMonthOrYear(datepicker, Number(target.dataset.year));
  1388. }
  1389. }
  1390. function onClickPicker(datepicker) {
  1391. if (!datepicker.inline && !datepicker.config.disableTouchKeyboard) {
  1392. datepicker.inputField.focus();
  1393. }
  1394. }
  1395. function processPickerOptions(picker, options) {
  1396. if (options.title !== undefined) {
  1397. if (options.title) {
  1398. picker.controls.title.textContent = options.title;
  1399. showElement(picker.controls.title);
  1400. } else {
  1401. picker.controls.title.textContent = '';
  1402. hideElement(picker.controls.title);
  1403. }
  1404. }
  1405. if (options.prevArrow) {
  1406. const prevBtn = picker.controls.prevBtn;
  1407. emptyChildNodes(prevBtn);
  1408. options.prevArrow.forEach((node) => {
  1409. prevBtn.appendChild(node.cloneNode(true));
  1410. });
  1411. }
  1412. if (options.nextArrow) {
  1413. const nextBtn = picker.controls.nextBtn;
  1414. emptyChildNodes(nextBtn);
  1415. options.nextArrow.forEach((node) => {
  1416. nextBtn.appendChild(node.cloneNode(true));
  1417. });
  1418. }
  1419. if (options.locale) {
  1420. picker.controls.todayBtn.textContent = options.locale.today;
  1421. picker.controls.clearBtn.textContent = options.locale.clear;
  1422. }
  1423. if (options.todayBtn !== undefined) {
  1424. if (options.todayBtn) {
  1425. showElement(picker.controls.todayBtn);
  1426. } else {
  1427. hideElement(picker.controls.todayBtn);
  1428. }
  1429. }
  1430. if (hasProperty(options, 'minDate') || hasProperty(options, 'maxDate')) {
  1431. const {minDate, maxDate} = picker.datepicker.config;
  1432. picker.controls.todayBtn.disabled = !isInRange(today(), minDate, maxDate);
  1433. }
  1434. if (options.clearBtn !== undefined) {
  1435. if (options.clearBtn) {
  1436. showElement(picker.controls.clearBtn);
  1437. } else {
  1438. hideElement(picker.controls.clearBtn);
  1439. }
  1440. }
  1441. }
  1442. // Compute view date to reset, which will be...
  1443. // - the last item of the selected dates or defaultViewDate if no selection
  1444. // - limitted to minDate or maxDate if it exceeds the range
  1445. function computeResetViewDate(datepicker) {
  1446. const {dates, config} = datepicker;
  1447. const viewDate = dates.length > 0 ? lastItemOf(dates) : config.defaultViewDate;
  1448. return limitToRange(viewDate, config.minDate, config.maxDate);
  1449. }
  1450. // Change current view's view date
  1451. function setViewDate(picker, newDate) {
  1452. const oldViewDate = new Date(picker.viewDate);
  1453. const newViewDate = new Date(newDate);
  1454. const {id, year, first, last} = picker.currentView;
  1455. const viewYear = newViewDate.getFullYear();
  1456. picker.viewDate = newDate;
  1457. if (viewYear !== oldViewDate.getFullYear()) {
  1458. triggerDatepickerEvent(picker.datepicker, 'changeYear');
  1459. }
  1460. if (newViewDate.getMonth() !== oldViewDate.getMonth()) {
  1461. triggerDatepickerEvent(picker.datepicker, 'changeMonth');
  1462. }
  1463. // return whether the new date is in different period on time from the one
  1464. // displayed in the current view
  1465. // when true, the view needs to be re-rendered on the next UI refresh.
  1466. switch (id) {
  1467. case 0:
  1468. return newDate < first || newDate > last;
  1469. case 1:
  1470. return viewYear !== year;
  1471. default:
  1472. return viewYear < first || viewYear > last;
  1473. }
  1474. }
  1475. function getTextDirection(el) {
  1476. return window.getComputedStyle(el).direction;
  1477. }
  1478. // Class representing the picker UI
  1479. class Picker {
  1480. constructor(datepicker) {
  1481. this.datepicker = datepicker;
  1482. const template = pickerTemplate.replace(/%buttonClass%/g, datepicker.config.buttonClass);
  1483. const element = this.element = parseHTML(template).firstChild;
  1484. const [header, main, footer] = element.firstChild.children;
  1485. const title = header.firstElementChild;
  1486. const [prevBtn, viewSwitch, nextBtn] = header.lastElementChild.children;
  1487. const [todayBtn, clearBtn] = footer.firstChild.children;
  1488. const controls = {
  1489. title,
  1490. prevBtn,
  1491. viewSwitch,
  1492. nextBtn,
  1493. todayBtn,
  1494. clearBtn,
  1495. };
  1496. this.main = main;
  1497. this.controls = controls;
  1498. const elementClass = datepicker.inline ? 'inline' : 'dropdown';
  1499. element.classList.add(`datepicker-${elementClass}`);
  1500. processPickerOptions(this, datepicker.config);
  1501. this.viewDate = computeResetViewDate(datepicker);
  1502. // set up event listeners
  1503. registerListeners(datepicker, [
  1504. [element, 'click', onClickPicker.bind(null, datepicker), {capture: true}],
  1505. [main, 'click', onClickView.bind(null, datepicker)],
  1506. [controls.viewSwitch, 'click', onClickViewSwitch.bind(null, datepicker)],
  1507. [controls.prevBtn, 'click', onClickPrevBtn.bind(null, datepicker)],
  1508. [controls.nextBtn, 'click', onClickNextBtn.bind(null, datepicker)],
  1509. [controls.todayBtn, 'click', onClickTodayBtn.bind(null, datepicker)],
  1510. [controls.clearBtn, 'click', onClickClearBtn.bind(null, datepicker)],
  1511. ]);
  1512. // set up views
  1513. this.views = [
  1514. new DaysView(this),
  1515. new MonthsView(this),
  1516. new YearsView(this, {id: 2, name: 'years', cellClass: 'year', step: 1}),
  1517. new YearsView(this, {id: 3, name: 'decades', cellClass: 'decade', step: 10}),
  1518. ];
  1519. this.currentView = this.views[datepicker.config.startView];
  1520. this.currentView.render();
  1521. this.main.appendChild(this.currentView.element);
  1522. datepicker.config.container.appendChild(this.element);
  1523. }
  1524. setOptions(options) {
  1525. processPickerOptions(this, options);
  1526. this.views.forEach((view) => {
  1527. view.init(options, false);
  1528. });
  1529. this.currentView.render();
  1530. }
  1531. detach() {
  1532. this.datepicker.config.container.removeChild(this.element);
  1533. }
  1534. show() {
  1535. if (this.active) {
  1536. return;
  1537. }
  1538. this.element.classList.add('active');
  1539. this.active = true;
  1540. const datepicker = this.datepicker;
  1541. if (!datepicker.inline) {
  1542. // ensure picker's direction matches input's
  1543. const inputDirection = getTextDirection(datepicker.inputField);
  1544. if (inputDirection !== getTextDirection(datepicker.config.container)) {
  1545. this.element.dir = inputDirection;
  1546. } else if (this.element.dir) {
  1547. this.element.removeAttribute('dir');
  1548. }
  1549. this.place();
  1550. if (datepicker.config.disableTouchKeyboard) {
  1551. datepicker.inputField.blur();
  1552. }
  1553. }
  1554. triggerDatepickerEvent(datepicker, 'show');
  1555. }
  1556. hide() {
  1557. if (!this.active) {
  1558. return;
  1559. }
  1560. this.datepicker.exitEditMode();
  1561. this.element.classList.remove('active');
  1562. this.active = false;
  1563. triggerDatepickerEvent(this.datepicker, 'hide');
  1564. }
  1565. place() {
  1566. const {classList, style} = this.element;
  1567. const {config, inputField} = this.datepicker;
  1568. const container = config.container;
  1569. const {
  1570. width: calendarWidth,
  1571. height: calendarHeight,
  1572. } = this.element.getBoundingClientRect();
  1573. const {
  1574. left: containerLeft,
  1575. top: containerTop,
  1576. width: containerWidth,
  1577. } = container.getBoundingClientRect();
  1578. const {
  1579. left: inputLeft,
  1580. top: inputTop,
  1581. width: inputWidth,
  1582. height: inputHeight
  1583. } = inputField.getBoundingClientRect();
  1584. let {x: orientX, y: orientY} = config.orientation;
  1585. let scrollTop;
  1586. let left;
  1587. let top;
  1588. if (container === document.body) {
  1589. scrollTop = window.scrollY;
  1590. left = inputLeft + window.scrollX;
  1591. top = inputTop + scrollTop;
  1592. } else {
  1593. scrollTop = container.scrollTop;
  1594. left = inputLeft - containerLeft;
  1595. top = inputTop - containerTop + scrollTop;
  1596. }
  1597. if (orientX === 'auto') {
  1598. if (left < 0) {
  1599. // align to the left and move into visible area if input's left edge < window's
  1600. orientX = 'left';
  1601. left = 10;
  1602. } else if (left + calendarWidth > containerWidth) {
  1603. // align to the right if canlendar's right edge > container's
  1604. orientX = 'right';
  1605. } else {
  1606. orientX = getTextDirection(inputField) === 'rtl' ? 'right' : 'left';
  1607. }
  1608. }
  1609. if (orientX === 'right') {
  1610. left -= calendarWidth - inputWidth;
  1611. }
  1612. if (orientY === 'auto') {
  1613. orientY = top - calendarHeight < scrollTop ? 'bottom' : 'top';
  1614. }
  1615. if (orientY === 'top') {
  1616. top -= calendarHeight;
  1617. } else {
  1618. top += inputHeight;
  1619. }
  1620. classList.remove(
  1621. 'datepicker-orient-top',
  1622. 'datepicker-orient-bottom',
  1623. 'datepicker-orient-right',
  1624. 'datepicker-orient-left'
  1625. );
  1626. classList.add(`datepicker-orient-${orientY}`, `datepicker-orient-${orientX}`);
  1627. style.top = top ? `${top}px` : top;
  1628. style.left = left ? `${left}px` : left;
  1629. }
  1630. setViewSwitchLabel(labelText) {
  1631. this.controls.viewSwitch.textContent = labelText;
  1632. }
  1633. setPrevBtnDisabled(disabled) {
  1634. this.controls.prevBtn.disabled = disabled;
  1635. }
  1636. setNextBtnDisabled(disabled) {
  1637. this.controls.nextBtn.disabled = disabled;
  1638. }
  1639. changeView(viewId) {
  1640. const oldView = this.currentView;
  1641. const newView = this.views[viewId];
  1642. if (newView.id !== oldView.id) {
  1643. this.currentView = newView;
  1644. this._renderMethod = 'render';
  1645. triggerDatepickerEvent(this.datepicker, 'changeView');
  1646. this.main.replaceChild(newView.element, oldView.element);
  1647. }
  1648. return this;
  1649. }
  1650. // Change the focused date (view date)
  1651. changeFocus(newViewDate) {
  1652. this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refreshFocus';
  1653. this.views.forEach((view) => {
  1654. view.updateFocus();
  1655. });
  1656. return this;
  1657. }
  1658. // Apply the change of the selected dates
  1659. update() {
  1660. const newViewDate = computeResetViewDate(this.datepicker);
  1661. this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refresh';
  1662. this.views.forEach((view) => {
  1663. view.updateFocus();
  1664. view.updateSelection();
  1665. });
  1666. return this;
  1667. }
  1668. // Refresh the picker UI
  1669. render(quickRender = true) {
  1670. const renderMethod = (quickRender && this._renderMethod) || 'render';
  1671. delete this._renderMethod;
  1672. this.currentView[renderMethod]();
  1673. }
  1674. }
  1675. // Find the closest date that doesn't meet the condition for unavailable date
  1676. // Returns undefined if no available date is found
  1677. // addFn: function to calculate the next date
  1678. // - args: time value, amount
  1679. // increase: amount to pass to addFn
  1680. // testFn: function to test the unavailablity of the date
  1681. // - args: time value; retun: true if unavailable
  1682. function findNextAvailableOne(date, addFn, increase, testFn, min, max) {
  1683. if (!isInRange(date, min, max)) {
  1684. return;
  1685. }
  1686. if (testFn(date)) {
  1687. const newDate = addFn(date, increase);
  1688. return findNextAvailableOne(newDate, addFn, increase, testFn, min, max);
  1689. }
  1690. return date;
  1691. }
  1692. // direction: -1 (left/up), 1 (right/down)
  1693. // vertical: true for up/down, false for left/right
  1694. function moveByArrowKey(datepicker, ev, direction, vertical) {
  1695. const picker = datepicker.picker;
  1696. const currentView = picker.currentView;
  1697. const step = currentView.step || 1;
  1698. let viewDate = picker.viewDate;
  1699. let addFn;
  1700. let testFn;
  1701. switch (currentView.id) {
  1702. case 0:
  1703. if (vertical) {
  1704. viewDate = addDays(viewDate, direction * 7);
  1705. } else if (ev.ctrlKey || ev.metaKey) {
  1706. viewDate = addYears(viewDate, direction);
  1707. } else {
  1708. viewDate = addDays(viewDate, direction);
  1709. }
  1710. addFn = addDays;
  1711. testFn = (date) => currentView.disabled.includes(date);
  1712. break;
  1713. case 1:
  1714. viewDate = addMonths(viewDate, vertical ? direction * 4 : direction);
  1715. addFn = addMonths;
  1716. testFn = (date) => {
  1717. const dt = new Date(date);
  1718. const {year, disabled} = currentView;
  1719. return dt.getFullYear() === year && disabled.includes(dt.getMonth());
  1720. };
  1721. break;
  1722. default:
  1723. viewDate = addYears(viewDate, direction * (vertical ? 4 : 1) * step);
  1724. addFn = addYears;
  1725. testFn = date => currentView.disabled.includes(startOfYearPeriod(date, step));
  1726. }
  1727. viewDate = findNextAvailableOne(
  1728. viewDate,
  1729. addFn,
  1730. direction < 0 ? -step : step,
  1731. testFn,
  1732. currentView.minDate,
  1733. currentView.maxDate
  1734. );
  1735. if (viewDate !== undefined) {
  1736. picker.changeFocus(viewDate).render();
  1737. }
  1738. }
  1739. function onKeydown(datepicker, ev) {
  1740. if (ev.key === 'Tab') {
  1741. unfocus(datepicker);
  1742. return;
  1743. }
  1744. const picker = datepicker.picker;
  1745. const {id, isMinView} = picker.currentView;
  1746. if (!picker.active) {
  1747. switch (ev.key) {
  1748. case 'ArrowDown':
  1749. case 'Escape':
  1750. picker.show();
  1751. break;
  1752. case 'Enter':
  1753. datepicker.update();
  1754. break;
  1755. default:
  1756. return;
  1757. }
  1758. } else if (datepicker.editMode) {
  1759. switch (ev.key) {
  1760. case 'Escape':
  1761. picker.hide();
  1762. break;
  1763. case 'Enter':
  1764. datepicker.exitEditMode({update: true, autohide: datepicker.config.autohide});
  1765. break;
  1766. default:
  1767. return;
  1768. }
  1769. } else {
  1770. switch (ev.key) {
  1771. case 'Escape':
  1772. picker.hide();
  1773. break;
  1774. case 'ArrowLeft':
  1775. if (ev.ctrlKey || ev.metaKey) {
  1776. goToPrevOrNext(datepicker, -1);
  1777. } else if (ev.shiftKey) {
  1778. datepicker.enterEditMode();
  1779. return;
  1780. } else {
  1781. moveByArrowKey(datepicker, ev, -1, false);
  1782. }
  1783. break;
  1784. case 'ArrowRight':
  1785. if (ev.ctrlKey || ev.metaKey) {
  1786. goToPrevOrNext(datepicker, 1);
  1787. } else if (ev.shiftKey) {
  1788. datepicker.enterEditMode();
  1789. return;
  1790. } else {
  1791. moveByArrowKey(datepicker, ev, 1, false);
  1792. }
  1793. break;
  1794. case 'ArrowUp':
  1795. if (ev.ctrlKey || ev.metaKey) {
  1796. switchView(datepicker);
  1797. } else if (ev.shiftKey) {
  1798. datepicker.enterEditMode();
  1799. return;
  1800. } else {
  1801. moveByArrowKey(datepicker, ev, -1, true);
  1802. }
  1803. break;
  1804. case 'ArrowDown':
  1805. if (ev.shiftKey && !ev.ctrlKey && !ev.metaKey) {
  1806. datepicker.enterEditMode();
  1807. return;
  1808. }
  1809. moveByArrowKey(datepicker, ev, 1, true);
  1810. break;
  1811. case 'Enter':
  1812. if (isMinView) {
  1813. datepicker.setDate(picker.viewDate);
  1814. } else {
  1815. picker.changeView(id - 1).render();
  1816. }
  1817. break;
  1818. case 'Backspace':
  1819. case 'Delete':
  1820. datepicker.enterEditMode();
  1821. return;
  1822. default:
  1823. if (ev.key.length === 1 && !ev.ctrlKey && !ev.metaKey) {
  1824. datepicker.enterEditMode();
  1825. }
  1826. return;
  1827. }
  1828. }
  1829. ev.preventDefault();
  1830. ev.stopPropagation();
  1831. }
  1832. function onFocus(datepicker) {
  1833. if (datepicker.config.showOnFocus && !datepicker._showing) {
  1834. datepicker.show();
  1835. }
  1836. }
  1837. // for the prevention for entering edit mode while getting focus on click
  1838. function onMousedown(datepicker, ev) {
  1839. const el = ev.target;
  1840. if (datepicker.picker.active || datepicker.config.showOnClick) {
  1841. el._active = el === document.activeElement;
  1842. el._clicking = setTimeout(() => {
  1843. delete el._active;
  1844. delete el._clicking;
  1845. }, 2000);
  1846. }
  1847. }
  1848. function onClickInput(datepicker, ev) {
  1849. const el = ev.target;
  1850. if (!el._clicking) {
  1851. return;
  1852. }
  1853. clearTimeout(el._clicking);
  1854. delete el._clicking;
  1855. if (el._active) {
  1856. datepicker.enterEditMode();
  1857. }
  1858. delete el._active;
  1859. if (datepicker.config.showOnClick) {
  1860. datepicker.show();
  1861. }
  1862. }
  1863. function onPaste(datepicker, ev) {
  1864. if (ev.clipboardData.types.includes('text/plain')) {
  1865. datepicker.enterEditMode();
  1866. }
  1867. }
  1868. // for the `document` to delegate the events from outside the picker/input field
  1869. function onClickOutside(datepicker, ev) {
  1870. const element = datepicker.element;
  1871. if (element !== document.activeElement) {
  1872. return;
  1873. }
  1874. const pickerElem = datepicker.picker.element;
  1875. if (findElementInEventPath(ev, el => el === element || el === pickerElem)) {
  1876. return;
  1877. }
  1878. unfocus(datepicker);
  1879. }
  1880. function stringifyDates(dates, config) {
  1881. return dates
  1882. .map(dt => formatDate(dt, config.format, config.locale))
  1883. .join(config.dateDelimiter);
  1884. }
  1885. // parse input dates and create an array of time values for selection
  1886. // returns undefined if there are no valid dates in inputDates
  1887. // when origDates (current selection) is passed, the function works to mix
  1888. // the input dates into the current selection
  1889. function processInputDates(datepicker, inputDates, clear = false) {
  1890. const {config, dates: origDates, rangepicker} = datepicker;
  1891. if (inputDates.length === 0) {
  1892. // empty input is considered valid unless origiDates is passed
  1893. return clear ? [] : undefined;
  1894. }
  1895. const rangeEnd = rangepicker && datepicker === rangepicker.datepickers[1];
  1896. let newDates = inputDates.reduce((dates, dt) => {
  1897. let date = parseDate(dt, config.format, config.locale);
  1898. if (date === undefined) {
  1899. return dates;
  1900. }
  1901. if (config.pickLevel > 0) {
  1902. // adjust to 1st of the month/Jan 1st of the year
  1903. // or to the last day of the monh/Dec 31st of the year if the datepicker
  1904. // is the range-end picker of a rangepicker
  1905. const dt = new Date(date);
  1906. if (config.pickLevel === 1) {
  1907. date = rangeEnd
  1908. ? dt.setMonth(dt.getMonth() + 1, 0)
  1909. : dt.setDate(1);
  1910. } else {
  1911. date = rangeEnd
  1912. ? dt.setFullYear(dt.getFullYear() + 1, 0, 0)
  1913. : dt.setMonth(0, 1);
  1914. }
  1915. }
  1916. if (
  1917. isInRange(date, config.minDate, config.maxDate)
  1918. && !dates.includes(date)
  1919. && !config.datesDisabled.includes(date)
  1920. && !config.daysOfWeekDisabled.includes(new Date(date).getDay())
  1921. ) {
  1922. dates.push(date);
  1923. }
  1924. return dates;
  1925. }, []);
  1926. if (newDates.length === 0) {
  1927. return;
  1928. }
  1929. if (config.multidate && !clear) {
  1930. // get the synmetric difference between origDates and newDates
  1931. newDates = newDates.reduce((dates, date) => {
  1932. if (!origDates.includes(date)) {
  1933. dates.push(date);
  1934. }
  1935. return dates;
  1936. }, origDates.filter(date => !newDates.includes(date)));
  1937. }
  1938. // do length check always because user can input multiple dates regardless of the mode
  1939. return config.maxNumberOfDates && newDates.length > config.maxNumberOfDates
  1940. ? newDates.slice(config.maxNumberOfDates * -1)
  1941. : newDates;
  1942. }
  1943. // refresh the UI elements
  1944. // modes: 1: input only, 2, picker only, 3 both
  1945. function refreshUI(datepicker, mode = 3, quickRender = true) {
  1946. const {config, picker, inputField} = datepicker;
  1947. if (mode & 2) {
  1948. const newView = picker.active ? config.pickLevel : config.startView;
  1949. picker.update().changeView(newView).render(quickRender);
  1950. }
  1951. if (mode & 1 && inputField) {
  1952. inputField.value = stringifyDates(datepicker.dates, config);
  1953. }
  1954. }
  1955. function setDate(datepicker, inputDates, options) {
  1956. let {clear, render, autohide} = options;
  1957. if (render === undefined) {
  1958. render = true;
  1959. }
  1960. if (!render) {
  1961. autohide = false;
  1962. } else if (autohide === undefined) {
  1963. autohide = datepicker.config.autohide;
  1964. }
  1965. const newDates = processInputDates(datepicker, inputDates, clear);
  1966. if (!newDates) {
  1967. return;
  1968. }
  1969. if (newDates.toString() !== datepicker.dates.toString()) {
  1970. datepicker.dates = newDates;
  1971. refreshUI(datepicker, render ? 3 : 1);
  1972. triggerDatepickerEvent(datepicker, 'changeDate');
  1973. } else {
  1974. refreshUI(datepicker, 1);
  1975. }
  1976. if (autohide) {
  1977. datepicker.hide();
  1978. }
  1979. }
  1980. /**
  1981. * Class representing a date picker
  1982. */
  1983. class Datepicker {
  1984. /**
  1985. * Create a date picker
  1986. * @param {Element} element - element to bind a date picker
  1987. * @param {Object} [options] - config options
  1988. * @param {DateRangePicker} [rangepicker] - DateRangePicker instance the
  1989. * date picker belongs to. Use this only when creating date picker as a part
  1990. * of date range picker
  1991. */
  1992. constructor(element, options = {}, rangepicker = undefined) {
  1993. element.datepicker = this;
  1994. this.element = element;
  1995. // set up config
  1996. const config = this.config = Object.assign({
  1997. buttonClass: (options.buttonClass && String(options.buttonClass)) || 'button',
  1998. container: document.body,
  1999. defaultViewDate: today(),
  2000. maxDate: undefined,
  2001. minDate: undefined,
  2002. }, processOptions(defaultOptions, this));
  2003. this._options = options;
  2004. Object.assign(config, processOptions(options, this));
  2005. // configure by type
  2006. const inline = this.inline = element.tagName !== 'INPUT';
  2007. let inputField;
  2008. let initialDates;
  2009. if (inline) {
  2010. config.container = element;
  2011. initialDates = stringToArray(element.dataset.date, config.dateDelimiter);
  2012. delete element.dataset.date;
  2013. } else {
  2014. const container = options.container ? document.querySelector(options.container) : null;
  2015. if (container) {
  2016. config.container = container;
  2017. }
  2018. inputField = this.inputField = element;
  2019. inputField.classList.add('datepicker-input');
  2020. initialDates = stringToArray(inputField.value, config.dateDelimiter);
  2021. }
  2022. if (rangepicker) {
  2023. // check validiry
  2024. const index = rangepicker.inputs.indexOf(inputField);
  2025. const datepickers = rangepicker.datepickers;
  2026. if (index < 0 || index > 1 || !Array.isArray(datepickers)) {
  2027. throw Error('Invalid rangepicker object.');
  2028. }
  2029. // attach itaelf to the rangepicker here so that processInputDates() can
  2030. // determine if this is the range-end picker of the rangepicker while
  2031. // setting inital values when pickLevel > 0
  2032. datepickers[index] = this;
  2033. // add getter for rangepicker
  2034. Object.defineProperty(this, 'rangepicker', {
  2035. get() {
  2036. return rangepicker;
  2037. },
  2038. });
  2039. }
  2040. // set initial dates
  2041. this.dates = [];
  2042. // process initial value
  2043. const inputDateValues = processInputDates(this, initialDates);
  2044. if (inputDateValues && inputDateValues.length > 0) {
  2045. this.dates = inputDateValues;
  2046. }
  2047. if (inputField) {
  2048. inputField.value = stringifyDates(this.dates, config);
  2049. }
  2050. const picker = this.picker = new Picker(this);
  2051. if (inline) {
  2052. this.show();
  2053. } else {
  2054. // set up event listeners in other modes
  2055. const onMousedownDocument = onClickOutside.bind(null, this);
  2056. const listeners = [
  2057. [inputField, 'keydown', onKeydown.bind(null, this)],
  2058. [inputField, 'focus', onFocus.bind(null, this)],
  2059. [inputField, 'mousedown', onMousedown.bind(null, this)],
  2060. [inputField, 'click', onClickInput.bind(null, this)],
  2061. [inputField, 'paste', onPaste.bind(null, this)],
  2062. [document, 'mousedown', onMousedownDocument],
  2063. [document, 'touchstart', onMousedownDocument],
  2064. [window, 'resize', picker.place.bind(picker)]
  2065. ];
  2066. registerListeners(this, listeners);
  2067. }
  2068. }
  2069. /**
  2070. * Format Date object or time value in given format and language
  2071. * @param {Date|Number} date - date or time value to format
  2072. * @param {String|Object} format - format string or object that contains
  2073. * toDisplay() custom formatter, whose signature is
  2074. * - args:
  2075. * - date: {Date} - Date instance of the date passed to the method
  2076. * - format: {Object} - the format object passed to the method
  2077. * - locale: {Object} - locale for the language specified by `lang`
  2078. * - return:
  2079. * {String} formatted date
  2080. * @param {String} [lang=en] - language code for the locale to use
  2081. * @return {String} formatted date
  2082. */
  2083. static formatDate(date, format, lang) {
  2084. return formatDate(date, format, lang && locales[lang] || locales.en);
  2085. }
  2086. /**
  2087. * Parse date string
  2088. * @param {String|Date|Number} dateStr - date string, Date object or time
  2089. * value to parse
  2090. * @param {String|Object} format - format string or object that contains
  2091. * toValue() custom parser, whose signature is
  2092. * - args:
  2093. * - dateStr: {String|Date|Number} - the dateStr passed to the method
  2094. * - format: {Object} - the format object passed to the method
  2095. * - locale: {Object} - locale for the language specified by `lang`
  2096. * - return:
  2097. * {Date|Number} parsed date or its time value
  2098. * @param {String} [lang=en] - language code for the locale to use
  2099. * @return {Number} time value of parsed date
  2100. */
  2101. static parseDate(dateStr, format, lang) {
  2102. return parseDate(dateStr, format, lang && locales[lang] || locales.en);
  2103. }
  2104. /**
  2105. * @type {Object} - Installed locales in `[languageCode]: localeObject` format
  2106. * en`:_English (US)_ is pre-installed.
  2107. */
  2108. static get locales() {
  2109. return locales;
  2110. }
  2111. /**
  2112. * @type {Boolean} - Whether the picker element is shown. `true` whne shown
  2113. */
  2114. get active() {
  2115. return !!(this.picker && this.picker.active);
  2116. }
  2117. /**
  2118. * @type {HTMLDivElement} - DOM object of picker element
  2119. */
  2120. get pickerElement() {
  2121. return this.picker ? this.picker.element : undefined;
  2122. }
  2123. /**
  2124. * Set new values to the config options
  2125. * @param {Object} options - config options to update
  2126. */
  2127. setOptions(options) {
  2128. const picker = this.picker;
  2129. const newOptions = processOptions(options, this);
  2130. Object.assign(this._options, options);
  2131. Object.assign(this.config, newOptions);
  2132. picker.setOptions(newOptions);
  2133. refreshUI(this, 3);
  2134. }
  2135. /**
  2136. * Show the picker element
  2137. */
  2138. show() {
  2139. if (this.inputField) {
  2140. if (this.inputField.disabled) {
  2141. return;
  2142. }
  2143. if (this.inputField !== document.activeElement) {
  2144. this._showing = true;
  2145. this.inputField.focus();
  2146. delete this._showing;
  2147. }
  2148. }
  2149. this.picker.show();
  2150. }
  2151. /**
  2152. * Hide the picker element
  2153. * Not available on inline picker
  2154. */
  2155. hide() {
  2156. if (this.inline) {
  2157. return;
  2158. }
  2159. this.picker.hide();
  2160. this.picker.update().changeView(this.config.startView).render();
  2161. }
  2162. /**
  2163. * Destroy the Datepicker instance
  2164. * @return {Detepicker} - the instance destroyed
  2165. */
  2166. destroy() {
  2167. this.hide();
  2168. unregisterListeners(this);
  2169. this.picker.detach();
  2170. if (!this.inline) {
  2171. this.inputField.classList.remove('datepicker-input');
  2172. }
  2173. delete this.element.datepicker;
  2174. return this;
  2175. }
  2176. /**
  2177. * Get the selected date(s)
  2178. *
  2179. * The method returns a Date object of selected date by default, and returns
  2180. * an array of selected dates in multidate mode. If format string is passed,
  2181. * it returns date string(s) formatted in given format.
  2182. *
  2183. * @param {String} [format] - Format string to stringify the date(s)
  2184. * @return {Date|String|Date[]|String[]} - selected date(s), or if none is
  2185. * selected, empty array in multidate mode and untitled in sigledate mode
  2186. */
  2187. getDate(format = undefined) {
  2188. const callback = format
  2189. ? date => formatDate(date, format, this.config.locale)
  2190. : date => new Date(date);
  2191. if (this.config.multidate) {
  2192. return this.dates.map(callback);
  2193. }
  2194. if (this.dates.length > 0) {
  2195. return callback(this.dates[0]);
  2196. }
  2197. }
  2198. /**
  2199. * Set selected date(s)
  2200. *
  2201. * In multidate mode, you can pass multiple dates as a series of arguments
  2202. * or an array. (Since each date is parsed individually, the type of the
  2203. * dates doesn't have to be the same.)
  2204. * The given dates are used to toggle the select status of each date. The
  2205. * number of selected dates is kept from exceeding the length set to
  2206. * maxNumberOfDates.
  2207. *
  2208. * With clear: true option, the method can be used to clear the selection
  2209. * and to replace the selection instead of toggling in multidate mode.
  2210. * If the option is passed with no date arguments or an empty dates array,
  2211. * it works as "clear" (clear the selection then set nothing), and if the
  2212. * option is passed with new dates to select, it works as "replace" (clear
  2213. * the selection then set the given dates)
  2214. *
  2215. * When render: false option is used, the method omits re-rendering the
  2216. * picker element. In this case, you need to call refresh() method later in
  2217. * order for the picker element to reflect the changes. The input field is
  2218. * refreshed always regardless of this option.
  2219. *
  2220. * When invalid (unparsable, repeated, disabled or out-of-range) dates are
  2221. * passed, the method ignores them and applies only valid ones. In the case
  2222. * that all the given dates are invalid, which is distinguished from passing
  2223. * no dates, the method considers it as an error and leaves the selection
  2224. * untouched.
  2225. *
  2226. * @param {...(Date|Number|String)|Array} [dates] - Date strings, Date
  2227. * objects, time values or mix of those for new selection
  2228. * @param {Object} [options] - function options
  2229. * - clear: {boolean} - Whether to clear the existing selection
  2230. * defualt: false
  2231. * - render: {boolean} - Whether to re-render the picker element
  2232. * default: true
  2233. * - autohide: {boolean} - Whether to hide the picker element after re-render
  2234. * Ignored when used with render: false
  2235. * default: config.autohide
  2236. */
  2237. setDate(...args) {
  2238. const dates = [...args];
  2239. const opts = {};
  2240. const lastArg = lastItemOf(args);
  2241. if (
  2242. typeof lastArg === 'object'
  2243. && !Array.isArray(lastArg)
  2244. && !(lastArg instanceof Date)
  2245. && lastArg
  2246. ) {
  2247. Object.assign(opts, dates.pop());
  2248. }
  2249. const inputDates = Array.isArray(dates[0]) ? dates[0] : dates;
  2250. setDate(this, inputDates, opts);
  2251. }
  2252. /**
  2253. * Update the selected date(s) with input field's value
  2254. * Not available on inline picker
  2255. *
  2256. * The input field will be refreshed with properly formatted date string.
  2257. *
  2258. * @param {Object} [options] - function options
  2259. * - autohide: {boolean} - whether to hide the picker element after refresh
  2260. * default: false
  2261. */
  2262. update(options = undefined) {
  2263. if (this.inline) {
  2264. return;
  2265. }
  2266. const opts = {clear: true, autohide: !!(options && options.autohide)};
  2267. const inputDates = stringToArray(this.inputField.value, this.config.dateDelimiter);
  2268. setDate(this, inputDates, opts);
  2269. }
  2270. /**
  2271. * Refresh the picker element and the associated input field
  2272. * @param {String} [target] - target item when refreshing one item only
  2273. * 'picker' or 'input'
  2274. * @param {Boolean} [forceRender] - whether to re-render the picker element
  2275. * regardless of its state instead of optimized refresh
  2276. */
  2277. refresh(target = undefined, forceRender = false) {
  2278. if (target && typeof target !== 'string') {
  2279. forceRender = target;
  2280. target = undefined;
  2281. }
  2282. let mode;
  2283. if (target === 'picker') {
  2284. mode = 2;
  2285. } else if (target === 'input') {
  2286. mode = 1;
  2287. } else {
  2288. mode = 3;
  2289. }
  2290. refreshUI(this, mode, !forceRender);
  2291. }
  2292. /**
  2293. * Enter edit mode
  2294. * Not available on inline picker or when the picker element is hidden
  2295. */
  2296. enterEditMode() {
  2297. if (this.inline || !this.picker.active || this.editMode) {
  2298. return;
  2299. }
  2300. this.editMode = true;
  2301. this.inputField.classList.add('in-edit');
  2302. }
  2303. /**
  2304. * Exit from edit mode
  2305. * Not available on inline picker
  2306. * @param {Object} [options] - function options
  2307. * - update: {boolean} - whether to call update() after exiting
  2308. * If false, input field is revert to the existing selection
  2309. * default: false
  2310. */
  2311. exitEditMode(options = undefined) {
  2312. if (this.inline || !this.editMode) {
  2313. return;
  2314. }
  2315. const opts = Object.assign({update: false}, options);
  2316. delete this.editMode;
  2317. this.inputField.classList.remove('in-edit');
  2318. if (opts.update) {
  2319. this.update(opts);
  2320. }
  2321. }
  2322. }
  2323. return Datepicker;
  2324. }());