Repository: flatpickr/flatpickr
Pull request #1738 (2019-04-12 09:53:42)

Added month dropdown plugin
Added a plugin that replaces the month name with a dropdown for a quick month selection. It plays nice with min and max date, localization and with different themes (shown below). This feature was requested in #985, #1582 and #1614.

![image](https://user-images.githubusercontent.com/793221/56028808-4255de00-5d19-11e9-93ff-e6ca3d61eeb8.png)
![image](https://user-images.githubusercontent.com/793221/56028831-51d52700-5d19-11e9-887f-e50e3ed4ddb1.png)



Patch:
diff --git a/src/index.ts b/src/index.ts
index 120095cd..1b8b5910 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -2630,8 +2630,6 @@ function FlatpickrInstance(
   }
 
   function onMonthNavClick(e: MouseEvent) {
-    e.preventDefault();
-
     const isPrevMonth = self.prevMonthNav.contains(e.target as Node);
     const isNextMonth = self.nextMonthNav.contains(e.target as Node);
 
diff --git a/src/plugins/monthDropdown/index.ts b/src/plugins/monthDropdown/index.ts
new file mode 100644
index 00000000..f8cba1ea
--- /dev/null
+++ b/src/plugins/monthDropdown/index.ts
@@ -0,0 +1,154 @@
+import { Plugin } from "../../types/options";
+import { Instance } from "../../types/instance";
+import { monthToStr } from "../../utils/formatting";
+
+export interface Config {
+  shorthand: boolean;
+}
+
+const defaultConfig: Config = {
+  shorthand: false,
+};
+
+function monthDropdownPlugin(pluginConfig?: Partial<Config>): Plugin {
+  const config = { ...defaultConfig, ...pluginConfig };
+  const self = { monthsDropdownContainer: null as null | HTMLSelectElement };
+
+  return (fp: Instance) => {
+    function clearUnnecessaryDOMElements() {
+      fp.monthElements.forEach(element => {
+        if (!element.classList.contains("cur-month")) {
+          return;
+        }
+
+        element.style.display = "none";
+      });
+    }
+
+    function buildSelector() {
+      const shouldBuildMonth = function(month: number): boolean {
+        if (
+          fp.config.minDate !== undefined &&
+          fp.currentYear === fp.config.minDate.getFullYear() &&
+          month < fp.config.minDate.getMonth()
+        ) {
+          return false;
+        }
+
+        return !(
+          fp.config.maxDate !== undefined &&
+          fp.currentYear === fp.config.maxDate.getFullYear() &&
+          month > fp.config.maxDate.getMonth()
+        );
+      };
+
+      const currentMonthContainer = fp.monthNav.querySelector(
+        ".flatpickr-current-month"
+      );
+      const currentYearElement = fp.yearElements.find(element => {
+        return element.classList.contains("cur-year");
+      });
+
+      if (
+        !currentMonthContainer ||
+        !currentYearElement ||
+        !currentYearElement.parentNode
+      )
+        return;
+
+      self.monthsDropdownContainer = fp._createElement<HTMLSelectElement>(
+        "select",
+        "flatpickr-monthDropdown-months"
+      );
+
+      self.monthsDropdownContainer.style.backgroundColor = getComputedStyle(
+        fp.calendarContainer
+      ).backgroundColor;
+
+      self.monthsDropdownContainer.tabIndex = -1;
+
+      for (let i = 0; i < 12; i++) {
+        if (!shouldBuildMonth(i)) continue;
+
+        const month = fp._createElement<HTMLOptionElement>(
+          "option",
+          "flatpickr-monthDropdown-month"
+        );
+
+        month.value = new Date(fp.currentYear, i).getMonth().toString();
+        month.textContent = monthToStr(i, config.shorthand, fp.l10n);
+        month.tabIndex = -1;
+
+        if (fp.currentMonth === i) {
+          month.selected = true;
+        }
+
+        self.monthsDropdownContainer.appendChild(month);
+      }
+
+      self.monthsDropdownContainer.addEventListener("change", changeMonth);
+
+      currentMonthContainer.insertBefore(
+        self.monthsDropdownContainer,
+        currentYearElement.parentNode
+      );
+    }
+
+    function markMonthAsSelected() {
+      const currentlySelected = self.monthsDropdownContainer!.querySelector(
+        ".flatpickr-monthDropdown-month:checked"
+      ) as HTMLOptionElement;
+
+      const monthToSelect = self.monthsDropdownContainer!.querySelector(
+        `.flatpickr-monthDropdown-month[value="${fp.currentMonth}"]`
+      ) as HTMLOptionElement;
+
+      if (currentlySelected) {
+        currentlySelected.selected = false;
+      }
+
+      if (monthToSelect) {
+        monthToSelect.selected = true;
+      }
+    }
+
+    function rebuildSelector() {
+      if (self.monthsDropdownContainer) {
+        self.monthsDropdownContainer.parentNode!.removeChild(
+          self.monthsDropdownContainer
+        );
+      }
+
+      buildSelector();
+    }
+
+    function addListeners() {}
+
+    function changeMonth(e: Event) {
+      const target = e.target as HTMLSelectElement;
+      const selectedMonth = parseInt(target.value, 10);
+
+      fp.changeMonth(selectedMonth - fp.currentMonth);
+    }
+
+    function destroyPluginInstance() {}
+
+    return {
+      onParseConfig() {},
+      onValueUpdate: markMonthAsSelected,
+      onReady: [
+        clearUnnecessaryDOMElements,
+        buildSelector,
+        addListeners,
+        () => {
+          fp.loadedPlugins.push("monthSelect");
+        },
+      ],
+      onMonthChange: markMonthAsSelected,
+      onYearChange: rebuildSelector,
+      onDestroy: destroyPluginInstance,
+    };
+  };
+}
+
+export default monthDropdownPlugin;
diff --git a/src/plugins/monthDropdown/style.css b/src/plugins/monthDropdown/style.css
new file mode 100644
index 00000000..8d6d2478
--- /dev/null
+++ b/src/plugins/monthDropdown/style.css
@@ -0,0 +1,32 @@
+.flatpickr-monthDropdown-months {
+  background: transparent;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  color: inherit;
+  cursor: pointer;
+  padding: 0 0 0 0.5ch;
+  margin: 0;
+  display: inline-block;
+  font-size: inherit;
+  font-family: inherit;
+  font-weight: 300;
+  line-height: inherit;
+  height: auto;
+  border: 0;
+  border-radius: 0;
+  vertical-align: initial;
+  -webkit-appearance: menulist;
+  -moz-appearance: menulist;
+  appearance: menulist;
+  width: auto;
+}
+
+.flatpickr-monthDropdown-months:focus,
+.flatpickr-monthDropdown-months:active {
+  border: 0;
+}
+
+.flatpickr-monthDropdown-month {
+  background-color: inherit;
+  /*color: #393939;*/
+}


Test patch:
diff --git a/src/plugins/monthDropdown/tests.spec.ts b/src/plugins/monthDropdown/tests.spec.ts
new file mode 100644
index 00000000..88e27d27
--- /dev/null
+++ b/src/plugins/monthDropdown/tests.spec.ts
@@ -0,0 +1,121 @@
+import flatpickr from "../../index";
+import { Instance } from "types/instance";
+import { Options } from "types/options";
+import { German } from "l10n/de";
+import monthDropdownPlugin from "./index";
+
+flatpickr.defaultConfig.animate = false;
+
+jest.useFakeTimers();
+
+const createInstance = (config?: Options): Instance => {
+  return flatpickr(
+    document.createElement("input"),
+    config as Options
+  ) as Instance;
+};
+
+describe("monthDropdown", () => {
+  it("should correctly load", () => {
+    const fp = createInstance({
+      plugins: [monthDropdownPlugin({})],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    );
+
+    expect(monthsDropdown!.constructor.name).toEqual("HTMLSelectElement");
+    expect(
+      monthsDropdown!.querySelectorAll(".flatpickr-monthDropdown-month").length
+    ).toEqual(12);
+  });
+
+  it("should correctly load shorthand months", () => {
+    const fp = createInstance({
+      plugins: [monthDropdownPlugin({ shorthand: true })],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    );
+
+    expect(
+      monthsDropdown!.querySelector(".flatpickr-monthDropdown-month")!.innerHTML
+    ).toEqual("Jan");
+  });
+
+  it("should correctly load localized months", () => {
+    const fp = createInstance({
+      locale: German,
+      plugins: [monthDropdownPlugin({})],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    );
+
+    expect(
+      monthsDropdown!.querySelector(".flatpickr-monthDropdown-month")!.innerHTML
+    ).toEqual("Januar");
+  });
+
+  it("should correctly load months with minDate", () => {
+    const fp = createInstance({
+      defaultDate: new Date(2019, 5, 11),
+      minDate: new Date(2019, 4, 11),
+      plugins: [monthDropdownPlugin({})],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    );
+    const months = monthsDropdown!.querySelectorAll(
+      ".flatpickr-monthDropdown-month"
+    );
+
+    expect(months.length).toEqual(8);
+
+    expect(months[0]!.innerHTML).toEqual("May");
+    expect(months[7]!.innerHTML).toEqual("December");
+  });
+
+  it("should correctly load months with maxDate", () => {
+    const fp = createInstance({
+      defaultDate: new Date(2019, 4, 11),
+      maxDate: new Date(2019, 8, 11),
+      plugins: [monthDropdownPlugin({})],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    );
+    const months = monthsDropdown!.querySelectorAll(
+      ".flatpickr-monthDropdown-month"
+    );
+
+    expect(months.length).toEqual(9);
+
+    expect(months[0]!.innerHTML).toEqual("January");
+    expect(months[months.length - 1]!.innerHTML).toEqual("September");
+  });
+
+  it("should change month", () => {
+    const fp = createInstance({
+      defaultDate: new Date(2019, 1, 1),
+      plugins: [monthDropdownPlugin({})],
+    }) as Instance;
+
+    const monthsDropdown = fp.calendarContainer.querySelector(
+      ".flatpickr-monthDropdown-months"
+    ) as HTMLSelectElement;
+
+    monthsDropdown.value = "3";
+
+    var evt = document.createEvent("HTMLEvents");
+    evt.initEvent("change", false, true);
+    monthsDropdown.dispatchEvent(evt);
+
+    expect(fp.currentMonth).toEqual(3);
+  });
+});