Repository: flatpickr/flatpickr
Pull request #2130 (2020-03-11 00:53:35)

Update date onBlur when allowInput is true
Attempting to fix https://github.com/flatpickr/flatpickr/issues/2109 (which is a duplicate of https://github.com/flatpickr/flatpickr/issues/1551)

Proposed fix comes from @aximili (https://github.com/flatpickr/flatpickr/issues/1551#issuecomment-491677052).

chmln: Thanks for the PR @matt17r 

Would you please add a test case as well to prevent future regressions?

Patch:
diff --git a/src/index.ts b/src/index.ts
index 52c0999e..f3274dc6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -479,6 +479,9 @@ function FlatpickrInstance(
         );
       }
     }
+    
+    if (self.config.allowInput)
+      bind(self._input, "blur", onBlur);
   }
 
   /**
@@ -832,7 +835,10 @@ function FlatpickrInstance(
     const firstOfMonth =
       (new Date(year, month, 1).getDay() - self.l10n.firstDayOfWeek + 7) % 7;
 
-    const prevMonthDays = self.utils.getDaysInMonth((month - 1 + 12) % 12, year);
+    const prevMonthDays = self.utils.getDaysInMonth(
+      (month - 1 + 12) % 12,
+      year
+    );
 
     const daysInMonth = self.utils.getDaysInMonth(month, year),
       days = window.document.createDocumentFragment(),
@@ -982,8 +988,10 @@ function FlatpickrInstance(
         "flatpickr-monthDropdown-months"
       );
 
-      self.monthsDropdownContainer.setAttribute("aria-label", self.l10n.monthAriaLabel);
-
+      self.monthsDropdownContainer.setAttribute(
+        "aria-label",
+        self.l10n.monthAriaLabel
+      );
 
       bind(self.monthsDropdownContainer, "change", (e: Event) => {
         const target = getEventTarget(e) as HTMLSelectElement;
@@ -1175,9 +1183,10 @@ function FlatpickrInstance(
           : self.config.defaultSeconds
       );
 
-      self.secondElement.setAttribute("step", self.minuteElement.getAttribute(
-        "step"
-      ) as string);
+      self.secondElement.setAttribute(
+        "step",
+        self.minuteElement.getAttribute("step") as string
+      );
       self.secondElement.setAttribute("min", "0");
       self.secondElement.setAttribute("max", "59");
 
@@ -1382,7 +1391,6 @@ function FlatpickrInstance(
       self.input.type = (self.input as any)._type;
       self.input.classList.remove("flatpickr-input");
       self.input.removeAttribute("readonly");
-      self.input.value = "";
     }
 
     ([
@@ -1468,7 +1476,7 @@ function FlatpickrInstance(
 
         self.close();
 
-        if (self.config.mode === "range" && self.selectedDates.length === 1) {
+        if (self.config && self.config.mode === "range" && self.selectedDates.length === 1) {
           self.clear(false);
           self.redraw();
         }
@@ -1586,6 +1594,16 @@ function FlatpickrInstance(
       );
     return false;
   }
+    
+  function onBlur(e: FocusEvent) {
+    var isInput = e.target === self._input;
+    
+    if (isInput) {
+      self.setDate(self._input.value, true, e.target === self.altInput
+                   ? self.config.altFormat
+                   : self.config.dateFormat);
+    }
+  }
 
   function onKeyDown(e: KeyboardEvent) {
     // e.key                      e.keyCode
@@ -1848,6 +1866,10 @@ function FlatpickrInstance(
   }
 
   function setDefaultTime() {
+    if (!self.config.autoFillDefaultTime) {
+      return;
+    }
+
     self.setDate(
       self.config.minDate !== undefined
         ? new Date(self.config.minDate.getTime())
@@ -2013,11 +2035,6 @@ function FlatpickrInstance(
           : defaultAltFormat + ` h:i${userConfig.enableSeconds ? ":S" : ""} K`;
     }
 
-    if (!userConfig.altInputClass) {
-      self.config.altInputClass =
-        self.input.className + " " + self.config.altInputClass;
-    }
-
     Object.defineProperty(self.config, "minDate", {
       get: () => self.config._minDate,
       set: minMaxDateSetter("min"),
@@ -2077,9 +2094,9 @@ function FlatpickrInstance(
       const pluginConf = self.config.plugins[i](self) || ({} as Options);
       for (const key in pluginConf) {
         if (HOOKS.indexOf(key as HookKey) > -1) {
-          (self.config as any)[key] = arrayify(pluginConf[
-            key as HookKey
-          ] as Hook)
+          (self.config as any)[key] = arrayify(
+            pluginConf[key as HookKey] as Hook
+          )
             .map(bindToInstance)
             .concat(self.config[key as HookKey]);
         } else if (typeof userConfig[key as keyof Options] === "undefined")
@@ -2087,9 +2104,20 @@ function FlatpickrInstance(
       }
     }
 
+    if (!userConfig.altInputClass) {
+      self.config.altInputClass =
+        getInputElem().className + " " + self.config.altInputClass;
+    }
+
     triggerEvent("onParseConfig");
   }
 
+  function getInputElem() {
+    return self.config.wrap
+      ? (element.querySelector("[data-input]") as HTMLInputElement)
+      : (element as HTMLInputElement);
+  }
+
   function setupLocale() {
     if (
       typeof self.config.locale !== "object" &&
@@ -2546,9 +2574,7 @@ function FlatpickrInstance(
   }
 
   function setupInputs() {
-    self.input = self.config.wrap
-      ? (element.querySelector("[data-input]") as HTMLInputElement)
-      : (element as HTMLInputElement);
+    self.input = getInputElem();
 
     /* istanbul ignore next */
     if (!self.input) {
diff --git a/src/plugins/momentPlugin.ts b/src/plugins/momentPlugin.ts
new file mode 100644
index 00000000..10eb8243
--- /dev/null
+++ b/src/plugins/momentPlugin.ts
@@ -0,0 +1,61 @@
+import { Plugin } from '../types/options'
+import { IncrementEvent } from '../utils'
+import { getEventTarget } from '../utils/dom'
+
+export interface Config {
+  moment: Function;
+}
+
+function momentPlugin(config: Config): Plugin {
+
+  const moment = config.moment
+
+  return function (fp) {
+    function captureIncrement(e: Event) {
+      const event = e as IncrementEvent
+      event.stopPropagation()
+      const date = moment(fp.selectedDates[0])
+
+      const input = getEventTarget(event) as HTMLInputElement
+      const unit = Array.from(input.classList)
+        .filter(name => name.startsWith('flatpickr-'))
+        .map(name => name.substring(10))[0]
+      const step = parseFloat(input.getAttribute("step") as string)
+
+      date.add(step * event.delta, unit)
+      fp.setDate(date.toDate())
+    }
+
+    return {
+      parseDate: (datestr, format) => {
+        return moment(datestr, format, true).toDate()
+      },
+      formatDate: (date, format) => {
+        // locale can also be used
+        const momentDate = moment(date)
+        if (typeof fp.config.locale === 'string') {
+          momentDate.locale(fp.config.locale)
+        }
+        return momentDate.format(format)
+      },
+      onReady() {
+        [fp.hourElement, fp.minuteElement, fp.secondElement]
+          .forEach(element =>
+            element && element.addEventListener('increment', captureIncrement, {
+              capture: true
+            })
+          )
+      },
+      onDestroy() {
+        [fp.hourElement, fp.minuteElement, fp.secondElement]
+          .forEach(element =>
+            element && element.removeEventListener('increment', captureIncrement, {
+              capture: true
+            })
+          )
+      }
+    }
+  }
+}
+
+export default momentPlugin
diff --git a/src/types/options.ts b/src/types/options.ts
index 1d1e17c4..0a8fe41d 100644
--- a/src/types/options.ts
+++ b/src/types/options.ts
@@ -71,6 +71,10 @@ export interface BaseOptions {
   /* Defaults to "F j, Y" */
   ariaDateFormat: string;
 
+  /* Whether the default time should be auto-filled when the input is empty and gains or loses focus. */
+  /* Defaults to true */
+  autoFillDefaultTime: boolean;
+
   /*
     Whether clicking on the input should open the picker.
     Set it to false if you only want to open the calendar programmatically
@@ -264,6 +268,7 @@ export interface ParsedOptions {
   animate: boolean;
   appendTo?: HTMLElement;
   ariaDateFormat: string;
+  autoFillDefaultTime: boolean;
   clickOpens: boolean;
   closeOnSelect: boolean;
   conjunction: string;
@@ -330,6 +335,7 @@ export const defaults: ParsedOptions = {
     typeof window === "object" &&
     window.navigator.userAgent.indexOf("MSIE") === -1,
   ariaDateFormat: "F j, Y",
+  autoFillDefaultTime: true,
   clickOpens: true,
   closeOnSelect: true,
   conjunction: ", ",


Test patch:
diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts
index c2acba5b..f6738aea 100644
--- a/src/__tests__/index.spec.ts
+++ b/src/__tests__/index.spec.ts
@@ -18,6 +18,7 @@ let mockAgent: string | undefined;
 });
 
 function createInstance(config?: Options, el?: HTMLElement) {
+  destroyInstance();
   fp = flatpickr(
     el || elem || document.createElement("input"),
     config || {}
@@ -25,6 +26,13 @@ function createInstance(config?: Options, el?: HTMLElement) {
   return fp;
 }
 
+function destroyInstance() {
+  if (fp) {
+    fp.input.value = "";
+    fp.destroy && fp.destroy();
+  }
+}
+
 function beforeEachTest() {
   mockAgent = undefined;
   jest.runAllTimers();
@@ -33,7 +41,7 @@ function beforeEachTest() {
     (document.activeElement as HTMLElement).blur();
   }
 
-  fp && fp.destroy && fp.destroy();
+  destroyInstance();
 
   if (elem === undefined) {
     elem = document.createElement("input");
@@ -828,6 +836,45 @@ describe("flatpickr", () => {
       expect(fp.currentMonth).toEqual(0);
     });
 
+    it("sets the date on direct entry when allowInput is true", () => {
+      createInstance({ allowInput: true });
+      expect(fp.selectedDates[0]).toBeUndefined();
+
+      fp.input.focus();
+      fp.input.value = "1999-12-31";
+      fp.input.blur();
+
+      expect(fp.selectedDates[0]).toBeDefined();
+      expect(fp.selectedDates[0].getFullYear()).toEqual(1999);
+      expect(fp.selectedDates[0].getMonth()).toEqual(11); // 11 === December
+      expect(fp.selectedDates[0].getDate()).toEqual(31);
+    });
+
+    it("updates the date on direct entry when allowInput is true", () => {
+      createInstance({
+        allowInput: true,
+        enableTime: true,
+        defaultDate: "2001-01-01 01:01",
+      });
+      expect(fp.selectedDates[0]).toBeDefined();
+      expect(fp.selectedDates[0].getFullYear()).toEqual(2001);
+      expect(fp.selectedDates[0].getMonth()).toEqual(0); // 0 === January
+      expect(fp.selectedDates[0].getDate()).toEqual(1);
+      expect(fp.selectedDates[0].getHours()).toEqual(1);
+      expect(fp.selectedDates[0].getMinutes()).toEqual(1);
+
+      fp.input.focus();
+      fp.input.value = "1969-07-20 20:17";
+      fp.input.blur();
+
+      expect(fp.selectedDates[0]).toBeDefined();
+      expect(fp.selectedDates[0].getFullYear()).toEqual(1969);
+      expect(fp.selectedDates[0].getMonth()).toEqual(6); // 6 === July
+      expect(fp.selectedDates[0].getDate()).toEqual(20);
+      expect(fp.selectedDates[0].getHours()).toEqual(20);
+      expect(fp.selectedDates[0].getMinutes()).toEqual(17);
+    });
+
     describe("mobile calendar", () => {
       describe(".isMobile", () => {
         it("returns true", () => {