Contents
■前説
仕事上、JQueryのカレンダー(Datepicker)を使って、日付の選択を行っていますが、追加要件で、年月のみ選択機能を求められました。
JQueryはこのくらい機能は当然持っていると勝手に信じて、いざ調べたら、ねぇ!!!びっくりしました。ぐっぐる先生に聞いたら、簡単に実現できないため、お前自分で作れ!という回答を頂きました。
実際に、(英語圏を含めて)ネット上にいくつの対策がありますが、UIや実現方法にあんまり気にならないため(恐らくうちのお客様も納得しない)、自力で作りました。
■考え方
年月日と年月両方とも、使いたいため、jquery-ui.jsをコピーし、もう一つを作ったら、バッティングで、javascriptエラーが発生する恐れがあると考えて、jquery-ui.jsを直接に手を入れると決断しました。(技術レベル低いくせに、フレームワークの改造が大好きです)
また、使う側に負担を掛からないように、初期設定で、フラグ(オプション)を立て、それをオンにすれば、自動的に、年月日から年月のみ選択モードに切り替えることを考えました。
改造方法は
1.カレンダー頭部分の年月表示を年のみの表示にする
2.カレンダーの左右矢印をクリックした際に、年のみ変わる
3.日の表示部分を月の表示にする
4.月の選択したら、入力項目に、年月のみを表示する
5.カレンダーを再表示時に、入力項目の年月をカレンダーに反映する
をすれば、上手く行けるはずと思いました。
地味にjquery-ui.jsの中身を調査し、上記の改造方法を実現できると思って、早速改造を着手します。
■実現
1.初期化時に、新しいオプションを追加
ディフォルト値regionalの中に、showOnlyMonthsフラグ(オプション)を追加します。入力項目をクリックする際に、このフラグ(オプション)を見て、年月日選択モードか年月のみ選択モードかを判断します。
1 2 3 4 5 |
showOnlyMonths: !1, |
2._updateDatepickerファンクションを改造
調べた結果、カレンダーを描画する処理は、_updateDatepickerファンクションで行っているため、処理の初めに、showOnlyMonthsフラグ(オプション)の値を見て、false(ディフォルト値)であれば、JQueryのカレンダーの挙動の通りで動きます。trueであれば、今回の新機能:年月のみ選択モードに切り替えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
_updateDatepicker: function (t) { // showOnlyMonthsフラグ(オプション)の値を取得 var opt = this._get(t, "showOnlyMonths"); // 年月のみ選択モード if(opt){ this.maxRows = 4, n = t, t.dpDiv.empty().append(this._generateHTML4YearMonth(t)), this._attachHandlers(t), t.dpDiv.find("." + this._dayOverClass + " a").mouseover(); } // 既存の通り else{ this.maxRows = 4, n = t, t.dpDiv.empty().append(this._generateHTML(t)), this._attachHandlers(t), t.dpDiv.find("." + this._dayOverClass + " a").mouseover(); } // 以下、既存のまま } |
3._generateHTML4YearMonthファンクションを追加
既存の_generateHTMLファンクションを真似し、カレンダーの年月表示を年のみ表示にし、日の表示を月の表示に改造します。(_generateYearHeaderに任せます)
また、カレンダーの左右矢印をクリックした際の挙動を年月の変更から年のみの変更に改造します。既存処理を出来る限りに改造したくないため、prev、nextファンクションを真似し、prevYear、nextYearファンクションを新規に作りました。
同じ思想で、月を選択した際の処理は、既存selectMonthファンクションを真似し、新規selectMonthPotファンクションに任せます。
簡単に言うと、_generateHTML4YearMonthファンクションから下記の新規ファンクションを呼び出しました。
ファンクション名 | 機能 |
_generateYearHeader | カレンダーの頭に、年のみを描画する |
prevYear | カレンダーの左矢印をクリックした際に、カレンダーを前年に変更する |
nextYear | カレンダーの右矢印をクリックした際に、カレンダーを翌年に変更する |
selectMonthPot | カレンダーの月を選択した際に、選択した月を入力項目に反映する |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
_generateHTML4YearMonth: function (e) { var t, i, a, s, n, r, o, u, c, h, l, d, p, g, m, f, _, v, k, y, b, D, w, M, C, x, I, N, T, A, E, S, Y, F, P, O, j, K, R, H = new Date, W = this._daylightSavingAdjust(new Date(H.getFullYear(), H.getMonth(), H.getDate())), L = this._get(e, "isRTL"), U = this._get(e, "showButtonPanel"), B = this._get(e, "hideIfNoPrevNext"), z = this._get(e, "navigationAsDateFormat"), q = this._getNumberOfMonths(e), G = this._get(e, "showCurrentAtPos"), J = this._get(e, "stepMonths"), Q = 1 !== q[0] || 1 !== q[1], V = this._daylightSavingAdjust(e.currentDay ? new Date(e.currentYear, e.currentMonth, e.currentDay) : new Date(9999, 9, 9)), $ = this._getMinMaxDate(e, "min"), X = this._getMinMaxDate(e, "max"), Z = e.drawMonth - G, et = e.drawYear; if (0 > Z && (Z += 12, et--), X) for (t = this._daylightSavingAdjust(new Date(X.getFullYear(), X.getMonth() - q[0] * q[1] + 1, X.getDate())), t = $ && $ > t ? $ : t; this._daylightSavingAdjust(new Date(et, Z, 1)) > t;) Z--, 0 > Z && (Z = 11, et--); for (e.drawMonth = Z, e.drawYear = et, i = this._get(e, "prevText"), i = z ? this.formatDate(i, this._daylightSavingAdjust(new Date(et, Z - J, 1)), this._getFormatConfig(e)) : i, a = this._canAdjustMonth(e, -1, et, Z) ? "<a class='ui-datepicker-prev ui-corner-all' data-handler='prevYear' data-event='click' title='" + i + "'><span class='ui-icon ui-icon-circle-triangle-" + (L ? "e" : "w") + "'>" + i + "</span></a>" : B ? "" : "<a class='ui-datepicker-prev ui-corner-all ui-state-disabled' title='" + i + "'><span class='ui-icon ui-icon-circle-triangle-" + (L ? "e" : "w") + "'>" + i + "</span></a>", s = this._get(e, "nextText"), s = z ? this.formatDate(s, this._daylightSavingAdjust(new Date(et, Z + J, 1)), this._getFormatConfig(e)) : s, n = this._canAdjustMonth(e, 1, et, Z) ? "<a class='ui-datepicker-next ui-corner-all' data-handler='nextYear' data-event='click' title='" + s + "'><span class='ui-icon ui-icon-circle-triangle-" + (L ? "w" : "e") + "'>" + s + "</span></a>" : B ? "" : "<a class='ui-datepicker-next ui-corner-all ui-state-disabled' title='" + s + "'><span class='ui-icon ui-icon-circle-triangle-" + (L ? "w" : "e") + "'>" + s + "</span></a>", r = this._get(e, "currentText"), o = this._get(e, "gotoCurrent") && e.currentDay ? V : W, r = z ? this.formatDate(r, o, this._getFormatConfig(e)) : r, u = e.inline ? "" : "<button type='button' class='ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all' data-handler='hide' data-event='click'>" + this._get(e, "closeText") + "</button>", c = U ? "<div class='ui-datepicker-buttonpane ui-widget-content'>" + (L ? u : "") + (this._isInRange(e, o) ? "<button type='button' class='ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all' data-handler='today' data-event='click'>" + r + "</button>" : "") + (L ? "" : u) + "</div>" : "", h = parseInt(this._get(e, "firstDay"), 10), h = isNaN(h) ? 0 : h, l = this._get(e, "showWeek"), d = this._get(e, "dayNames"), p = this._get(e, "dayNamesMin"), g = this._get(e, "monthNames"), m = this._get(e, "monthNamesShort"), f = this._get(e, "beforeShowDay"), _ = this._get(e, "showOtherMonths"), v = this._get(e, "selectOtherMonths"), k = this._getDefaultDate(e), y = "", D = 0; q[0] > D; D++) { for (w = "", this.maxRows = 4, M = 0; q[1] > M; M++) { の if (C = this._daylightSavingAdjust(new Date(et, Z, e.selectedDay)), x = " ui-corner-all", I = "", Q) { if (I += "<div class='ui-datepicker-group", q[1] > 1) switch (M) { case 0: I += " ui-datepicker-group-first", x = " ui-corner-" + (L ? "right" : "left"); break; case q[1] - 1: I += " ui-datepicker-group-last", x = " ui-corner-" + (L ? "left" : "right"); break; default: I += " ui-datepicker-group-middle", x = "" } I += "'>" } // カレンダーの年月表示を年表示に改造 // カレンダーの左右矢印をクリックした際に、呼び出しファンクションをprevYear、nextYearに変更 I += "<div class='ui-datepicker-header ui-widget-header ui-helper-clearfix" + x + "'>" + (/all|left/.test(x) && 0 === D ? L ? n : a : "") + (/all|right/.test(x) && 0 === D ? L ? a : n : "") + this._generateYearHeader(e, Z, et, $, X, D > 0 || M > 0, g, m) + "</div><table class='ui-datepicker-calendar'><thead>"; // 1月~12月をカレンダーに表示 for (I += "</tr></thead><tbody>", et === e.selectedYear && Z === e.selectedMonth , E = 11, S = 0, F = this._daylightSavingAdjust(new Date(et, Z)), this.maxRows = 3, P = 0; P < 12; P++) { if(P%4 == 0){ I += "<tr>"; } // 月を選択する際に、呼び出しファンクションをselectMonthPotに変更 I += "<td class='ui-datepicker-month-col' data-handler='selectMonthPot' data-event='click' data-month='" + P + "' data-year='" + e.selectedYear + "'" + "><a class='ui-state-default " + ( (e.selectedYear == e.currentYear && F.getMonth() == P) ? " ui-state-highlight'" : "'") + "href='#'>" + g[P] + "</a></td>" ; if(P%4 == 3){ I += "</tr>"; } } Z++, Z > 11 && (Z = 0, et++), I += "</tbody></table>" + (Q ? "</div>" + (q[0] > 0 && M === q[1] - 1 ? "<div class='ui-datepicker-row-break'></div>" : "") : ""), w += I } y += w } return y += c, e._keyEvent = !1, y } |
4._generateYearHeaderファンクションを追加
処理の中身はほとんど同じですが、出来る限りに既存を改造したくないため、既存_generateMonthYearHeaderファンクションを真似し、_generateYearHeaderを新規に作りました。
年月を表示したところ、年のみを表示するように改造するだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
_generateYearHeader: function (e, t, i, a, s, n, r, o) { var u, c, h, l, d, p, g, m, f = this._get(e, "changeMonth"), _ = this._get(e, "changeYear"), v = this._get(e, "showMonthAfterYear"), k = "<div class='ui-datepicker-title'>", y = ""; if (v || (k += y + (!n && f && _ ? "" : " ")), !e.yearshtml) if (e.yearshtml = "", n || !_) k += "<span class='ui-datepicker-year'>" + i + "</span>"; else { for (l = this._get(e, "yearRange").split(":"), d = (new Date).getFullYear(), p = function (e) { var t = e.match(/c[+\-].*/) ? i + parseInt(e.substring(1), 10) : e.match(/[+\-].*/) ? d + parseInt(e, 10) : parseInt(e, 10); return isNaN(t) ? d : t }, g = p(l[0]), m = Math.max(g, p(l[1] || "")), g = a ? Math.max(g, a.getFullYear()) : g, m = s ? Math.min(m, s.getFullYear()) : m, e.yearshtml += "<select class='ui-datepicker-year' data-handler='selectYear' data-event='change'>"; m >= g; g++) e.yearshtml += "<option value='" + g + "'" + (g === i ? " selected='selected'" : "") + ">" + g + "</option>"; e.yearshtml += "</select>", k += e.yearshtml, e.yearshtml = null } return k += this._get(e, "yearSuffix"), v && (k += (!n && f && _ ? "" : " ") + y), k += "</div>" }, |
5.prevYear、nextYearファンクションを追加
ハンドラーなので、_追加場所は、attachHandlersファンクションの中です。中身は、第三パラメータの値以外に、prev、nextYearと同じです。
年月のみ選択モードを実現するために、重要な処理の一つです。
1 2 3 4 5 6 7 8 9 10 |
prevYear: function () { e.datepicker._adjustDate(a, -i, "Y") // 第三パラメータの値はprevの【M】から【Y】に変更するだけ }, nextYear: function () { e.datepicker._adjustDate(a, +i, "Y") // 第三パラメータの値はnextの【M】から【Y】に変更するだけ }, |
6.selectMonthPotファンクションを追加
prevYear、nextYearと同じく、ハンドラーなので、_追加場所も同じく、attachHandlersファンクションの中です。
年月のみ選択モードを実現するために、一番重要な処理です。コア処理は_selectMonth新規ファンクションを任せます。
1 2 3 4 5 6 7 8 |
selectMonthPot: function () { // _selectMonthファンクションを呼び出し return e.datepicker._selectMonth(a, +this.getAttribute("data-month"), +this.getAttribute("data-year"), this), !1 }, |
7._selectMonthファンクションを追加
既存_selectDayファンクションの真似です。年月のみ選択モードのため、selectedDay、currentDayの値は常に【1】にします。
1 2 3 4 5 6 7 8 |
_selectMonth: function (t, i, a, s) { var n, r = e(t); e(s).hasClass(this._unselectableClass) || this._isDisabledDatepicker(r[0]) || (n = this._getInst(r[0]), n.selectedDay = n.currentDay = 1, n.selectedMonth = n.currentMonth = i, n.selectedYear = n.currentYear = a, this._selectDate(t, this._formatDate(n, n.currentDay, n.currentMonth, n.currentYear))) }, |
8.既存parseDateファンクションを改造
No.7までは、年月のみ選択モードでカレンダーの表示から入力項目への反映までの処理です。入力項目に既に値を入れた場合、年月のみ選択モードでカレンダーを表示する際に、その値を年月のみ選択モードのカレンダーに反映しなければならないので、既存parseDateファンクションの改造が必要となります。
parseDateファンクションを呼び出し場所は三箇所があって、いちいち修正するのに、面倒なので、既存処理を最低限に手を入れることを決めました。(少し言い訳が見えますが。。)
1 2 3 4 5 6 7 8 9 10 11 |
parseDate: function (i, a, s) { if(i == "yy/mm"){ i = "yy/mm/dd"; a = a + "/01"; } // 以下、既存のまま }, |
この改造ですが、正直、下手です。この改造によって、年月のみ選択モードは実際に、日付フォーマット【yy/mm】しか対応できなくなるためです。例えば、日付フォーマット【yy-mm】を設定した場合、年月のみ選択モード自体はエラーがなく動けますが、初期表示時に、年月のみ選択モードのカレンダーに既存日付に表示できないという不具合があります。
この改修は皆さんに任せます。
9.CSSを追加
年月のみ選択モードを表示する際に、4か月を一行に表示するため、【jquery.datetimepicker.css】に下記のCSSを追加します。
1 2 3 4 5 6 7 |
.ui-datepicker-month-col{ width: 24%; } |
【ui-datepicker-month-col】は_generateHTML4YearMonthファンクションが年月のみ選択モードのカレンダーを描画する際に、指定しました。
■サンプル
改造後のサンプルです。既存機能とのバッティングがなく、同時に動作できることを証明したいため、既存の年月日選択モードをここに出しました。
■ソース
サンプルのソース一式です。
ファイル名 | 説明 |
jquery-ui.min-1.10.4.js | 今回の改造対象です。 |
jquery.ui.datepicker-ja.min-1.10.4.js | 日本語バージョンなので、このファイルが必要となります。今回の改造対象ではないです。 |
jquery.datetimepicker.css | No.9のCSSファイルです。 |
jqueryの本体は、下記のバージョンで動作確認済みです
・jquery-2.2.4.min.js
・jquery-3.2.1.min.js
JQuery UIのテーマは皆さん(お客様)が好きなテーマを設定してください。サンプルは、【ui/1.10.4/themes/black-tie/jquery-ui.css】を使っています。
JQueryのカレンダー(Datepicker)で年月のみ選択モードのソースをダウンロードして試してみてください。
■最後
ここまでは、JQueryのカレンダー(Datepicker)を改造し、年月のみ選択モードを正常に動作することを確認しましたが、いくつの不満/不具合があります。
・dateFormatが【yy/mm】以外の値を設定した場合、該当年月はカレンダーに上手く反映できない。
・日本語しか対応できない
・JQuery UIのバージョンは【1.10.4】に固定されてしまいう
皆さんにはもっといい方法があると思って、もっと素晴らしい改造方法を実現したら、コメントでご連絡ください。
それでは、以上。