Author: robertdzeigler Date: Wed Jul 27 23:46:17 2011 New Revision: 1151673
URL: http://svn.apache.org/viewvc?rev=1151673&view=rev Log: TAP5-1408: datefield - cannot select same day in different month TAP5-1409: datefield popup does not have option to cancel Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/datepicker_106/js/datepicker.js tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/DateFieldDemo.java Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/datepicker_106/js/datepicker.js URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/datepicker_106/js/datepicker.js?rev=1151673&r1=1151672&r2=1151673&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/datepicker_106/js/datepicker.js (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/datepicker_106/js/datepicker.js Wed Jul 27 23:46:17 2011 @@ -1,691 +1,738 @@ -/*----------------------------------------------------------------------------\ -| Date Picker 1.06 | -|-----------------------------------------------------------------------------| -| Created by Erik Arvidsson | -| (http://webfx.eae.net/contact.html#erik) | -| For WebFX (http://webfx.eae.net/) | -|-----------------------------------------------------------------------------| -| A DOM based Date Picker | -|-----------------------------------------------------------------------------| -| Copyright (c) 1999, 2002, 2002, 2003, 2004, 2006 Erik Arvidsson | -|-----------------------------------------------------------------------------| -| Licensed under the Apache License, Version 2.0 (the "License"); you may not | -| use this file except in compliance with the License. You may obtain a copy | -| of the License at http://www.apache.org/licenses/LICENSE-2.0 | -| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | -| Unless required by applicable law or agreed to in writing, software | -| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | -| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | -| License for the specific language governing permissions and limitations | -| under the License. | -|-----------------------------------------------------------------------------| -| Dependencies: datepicker.css Date picker style declarations | -|-----------------------------------------------------------------------------| -| 2002-02-10 | Changed _update method to only update the text nodes instead | -| | rewriting the entire table. Also added support for mouse wheel | -| | in IE6. | -| 2002-01-14 | Cleaned up for 1.0 public version | -| 2002-01-15 | Replace all innerHTML calls with DOM1 methods | -| 2002-01-18 | Minor IE6 bug that occured when dragging the mouse | -| 2002-01-19 | Added a popup that is shown when the user clicks on the month. | -| | This allows navigation to 6 adjacent months. | -| 2002-04-10 | Fixed a bug that occured in the popup when a date was selected | -| | that caused surroundung months to "overflow" | -| | This had the effect that one could get two October months | -| | listed. | -| 2002-09-06 | I had missed one place were window was used instead of | -| | doc.parentWindow | -| 2003-08-28 | Added support for ensurin no date overflow when changing | -| | months. | -| 2004-01-10 | Adding type on the buttons to ensure they are not submit | -| | buttons. Minor CSS change for CSS2 | -| 2006-05-28 | Changed license to Apache Software License 2.0. | -|-----------------------------------------------------------------------------| -| Created 2001-10-?? | All changes are in the log above. | Updated 2006-05-28 | -\----------------------------------------------------------------------------*/ - -// The DatePicker constructor -// oDate : Date Optional argument representing the date to select -// Note: some minor modifications for Tapestry, to work well as a popup. -function DatePicker(oDate) -{ - // check arguments - if (arguments.length == 0) - { - this._selectedDate = new Date; - this._none = false; - } - else - { - this._selectedDate = oDate || new Date(); - this._none = oDate == null; - } - - this._matrix = [[],[],[],[],[],[],[]]; - this._showNone = true; - this._showToday = true; - this._firstWeekDay = 0; // start week with monday according to standards - this._redWeekDay = 6; // sunday is the default red day. - - this._dontChangeNone = false; -} - -// two static fields describing the name of the months abd days -DatePicker.months = [ - "January", "February", "March", "April", - "May", "June", "July", "August", - "September", "October", "November", "December"]; -DatePicker.days = ["m", "t", "w", "t", "f", "s", "s"]; - - -// Function invoked whenever the selected date changes, whether by -// navigation or when the user selects a date. -DatePicker.prototype.onchange = function () -{ -}; - -// onselect is more specified than onchange, and is triggered only when the user makes a specific selection -// using the calendar (rather than navigating to a new month). For Tapestry, -// this will dismiss the popup. -DatePicker.prototype.onselect = function() -{ -} - - -// create the nodes inside the date picker -DatePicker.prototype.create = function (doc) -{ - if (doc == null) doc = document; - - this._document = doc; - - // create elements - this._el = doc.createElement("div"); - this._el.className = "datePicker"; - - // header - var div = doc.createElement("div"); - div.className = "header"; - this._el.appendChild(div); - - var headerTable = doc.createElement("table"); - headerTable.className = "headerTable"; - headerTable.cellSpacing = 0; - div.appendChild(headerTable); - - var tBody = doc.createElement("tbody"); - headerTable.appendChild(tBody); - - var tr = doc.createElement("tr"); - tBody.appendChild(tr); - - var td = doc.createElement("td"); - this._previousMonth = doc.createElement("button"); - this._previousMonth.className = "previousButton"; - this._previousMonth.setAttribute("type", "button"); - td.appendChild(this._previousMonth); - tr.appendChild(td); - - td = doc.createElement("td"); - td.className = "labelContainer"; - tr.appendChild(td); - - this._topLabel = doc.createElement("a"); - this._topLabel.className = "topLabel"; - this._topLabel.href = "#"; - this._topLabel.appendChild(doc.createTextNode(String.fromCharCode(160))); - td.appendChild(this._topLabel); - - this._labelPopup = doc.createElement("div"); - this._labelPopup.className = "labelPopup"; - // no insertion - - td = doc.createElement("td"); - this._nextMonth = doc.createElement("button"); - this._nextMonth.className = "nextButton"; - this._nextMonth.setAttribute("type", "button"); - td.appendChild(this._nextMonth); - tr.appendChild(td); - - // grid - div = doc.createElement("div"); - div.className = "grid"; - this._el.appendChild(div); - this._table = div; - - // footer - div = doc.createElement("div"); - div.className = "footer"; - this._el.appendChild(div); - - var footerTable = doc.createElement("table"); - footerTable.className = "footerTable"; - footerTable.cellSpacing = 0; - div.appendChild(footerTable); - - tBody = doc.createElement("tbody"); - footerTable.appendChild(tBody); - - tr = doc.createElement("tr"); - tBody.appendChild(tr); - - td = doc.createElement("td"); - this._todayButton = doc.createElement("button"); - this._todayButton.className = "todayButton"; - this._todayButton.setAttribute("type", "button"); - this._todayButton.appendChild(doc.createTextNode("Today")); - td.appendChild(this._todayButton); - tr.appendChild(td); - - td = doc.createElement("td"); - td.className = "filler"; - td.appendChild(doc.createTextNode(String.fromCharCode(160))); - tr.appendChild(td); - - td = doc.createElement("td"); - this._noneButton = doc.createElement("button"); - this._noneButton.className = "noneButton"; - this._noneButton.setAttribute("type", "button"); - this._noneButton.appendChild(doc.createTextNode("None")); - td.appendChild(this._noneButton); - tr.appendChild(td); - - - this._createTable(doc); - - this._updateTable(); - this._setTopLabel(); - - if (!this._showNone) - this._noneButton.style.visibility = "hidden"; - if (!this._showToday) - this._todayButton.style.visibility = "hidden"; - - // IE55+ extension - this._previousMonth.hideFocus = true; - this._nextMonth.hideFocus = true; - this._todayButton.hideFocus = true; - this._noneButton.hideFocus = true; - // end IE55+ extension - - // hook up events - var dp = this; - // buttons - this._previousMonth.onclick = function () - { - dp._dontChangeNone = true; - dp.goToPreviousMonth(); - dp._dontChangeNone = false; - }; - this._nextMonth.onclick = function () - { - dp._dontChangeNone = true; - dp.goToNextMonth(); - dp._dontChangeNone = false; - }; - this._todayButton.onclick = function () - { - dp.goToToday(); - }; - this._noneButton.onclick = function () - { - dp.setDate(null, true); - }; - - this._el.onselectstart = function () - { - return false; - }; - - this._table.onclick = function (e) - { - // find event - if (e == null) e = doc.parentWindow.event; - - // find td - var el = e.target != null ? e.target : e.srcElement; - while (el.nodeType != 1) - el = el.parentNode; - while (el != null && el.tagName && el.tagName.toLowerCase() != "td") - el = el.parentNode; - - // if no td found, return - if (el == null || el.tagName == null || el.tagName.toLowerCase() != "td") - return; - - var d = new Date(dp._selectedDate); - var n = Number(el.firstChild.data); - if (isNaN(n) || n <= 0 || n == null) - return; - - d.setDate(n); - dp.setDate(d, true); - }; - - // show popup - this._topLabel.onclick = function (e) - { - dp._showLabelPopup(); - return false; - }; - - this._el.onkeydown = function (e) - { - if (e == null) e = doc.parentWindow.event; - var kc = e.keyCode != null ? e.keyCode : e.charCode; - - if (kc < 37 || kc > 40) return true; - - var d = new Date(dp._selectedDate).valueOf(); - if (kc == 37) // left - d -= 24 * 60 * 60 * 1000; - else if (kc == 39) // right - d += 24 * 60 * 60 * 1000; - else if (kc == 38) // up - d -= 7 * 24 * 60 * 60 * 1000; - else if (kc == 40) // down - d += 7 * 24 * 60 * 60 * 1000; - - dp.setDate(new Date(d), false); - return false; - } - - // ie6 extension - this._el.onmousewheel = function (e) - { - if (e == null) e = doc.parentWindow.event; - var n = - e.wheelDelta / 120; - var d = new Date(dp._selectedDate); - var m = d.getMonth() + n; - d.setMonth(m); - - - dp._dontChangeNone = true; - dp.setDate(d, false); - dp._dontChangeNone = false; - - return false; - } - - return this._el; -}; - -DatePicker.prototype.setDate = function (oDate, isSelection) -{ - - this._hideLabelPopup(); - - // if null then set None - if (oDate == null) - { - if (!this._none) - { - this._none = true; - this._setTopLabel(); - this._updateTable(); - - if (typeof this.onchange == "function") - this.onchange(); - } - - if (isSelection) - this.onselect(); - - return; - } - - // if string or number create a Date object - if (typeof oDate == "string" || typeof oDate == "number") - { - oDate = new Date(oDate); - } - - - // do not update if not really changed - if (this._selectedDate.getDate() != oDate.getDate() || - this._selectedDate.getMonth() != oDate.getMonth() || - this._selectedDate.getFullYear() != oDate.getFullYear() || - this._none) - { - - if (!this._dontChangeNone) - this._none = false; - - this._selectedDate = new Date(oDate); - - this._setTopLabel(); - this._updateTable(); - - if (typeof this.onchange == "function") - this.onchange(); - - if (isSelection) - this.onselect(); - } - - if (!this._dontChangeNone) - this._none = false; - -} - - -DatePicker.prototype.getDate = function () -{ - if (this._none) return null; - return new Date(this._selectedDate); // create a new instance -} - -// creates the table elements and inserts them into the date picker -DatePicker.prototype._createTable = function (doc) -{ - var str, i; - var rows = 6; - var cols = 7; - var currentWeek = 0; - - var table = doc.createElement("table"); - table.className = "gridTable"; - table.cellSpacing = 0; - - var tBody = doc.createElement("tbody"); - table.appendChild(tBody); - - // days row - var tr = doc.createElement("tr"); - tr.className = "daysRow"; - - var td, tn; - var nbsp = String.fromCharCode(160); - for (i = 0; i < cols; i++) - { - td = doc.createElement("td"); - td.appendChild(doc.createTextNode(nbsp)); - tr.appendChild(td); - } - tBody.appendChild(tr); - - // upper line - tr = doc.createElement("tr"); - td = doc.createElement("td"); - td.className = "upperLine"; - td.colSpan = 7; - tr.appendChild(td); - tBody.appendChild(tr); - - // rest - for (i = 0; i < rows; i++) - { - tr = doc.createElement("tr"); - for (var j = 0; j < cols; j++) - { - td = doc.createElement("td"); - td.appendChild(doc.createTextNode(nbsp)); - tr.appendChild(td); - } - tBody.appendChild(tr); - } - str += "</table>"; - - if (this._table != null) - this._table.appendChild(table) -}; -// this method updates all the text nodes inside the table as well -// as all the classNames on the tds -DatePicker.prototype._updateTable = function () -{ - // if no element no need to continue - if (this._table == null) return; - - var i; - var str = ""; - var rows = 6; - var cols = 7; - var currentWeek = 0; - - var cells = new Array(rows); - this._matrix = new Array(rows) - for (i = 0; i < rows; i++) - { - cells[i] = new Array(cols); - this._matrix[i] = new Array(cols); - } - - // Set the tmpDate to this month - var tmpDate = new Date(this._selectedDate.getFullYear(), - this._selectedDate.getMonth(), 1); - var today = new Date(); - // go thorugh all days this month and store the text - // and the class name in the cells matrix - for (i = 1; i < 32; i++) - { - tmpDate.setDate(i); - // convert to ISO, Monday is 0 and 6 is Sunday - var weekDay = ( tmpDate.getDay() + 6 ) % 7; - var colIndex = ( weekDay - this._firstWeekDay + 7 ) % 7; - if (tmpDate.getMonth() == this._selectedDate.getMonth()) - { - - var isToday = tmpDate.getDate() == today.getDate() && - tmpDate.getMonth() == today.getMonth() && - tmpDate.getFullYear() == today.getFullYear(); - - cells[currentWeek][colIndex] = { text: "", className: "" }; - - if (this._selectedDate.getDate() == tmpDate.getDate() && !this._none) - cells[currentWeek][colIndex].className += "selected "; - if (isToday) - cells[currentWeek][colIndex].className += "today "; - if (( tmpDate.getDay() + 6 ) % 7 == this._redWeekDay) // ISO - cells[currentWeek][colIndex].className += "red"; - - cells[currentWeek][colIndex].text = - this._matrix[currentWeek][colIndex] = tmpDate.getDate(); - - if (colIndex == 6) - currentWeek++; - } - } - - // fix day letter order if not standard - var weekDays = DatePicker.days; - if (this._firstWeekDay != 0) - { - weekDays = new Array(7); - for (i = 0; i < 7; i++) - weekDays[i] = DatePicker.days[ (i + this._firstWeekDay) % 7]; - } - - // update text in days row - var tds = this._table.firstChild.tBodies[0].rows[0].cells; - for (i = 0; i < cols; i++) - tds[i].firstChild.data = weekDays[i]; - - // update the text nodes and class names - var trs = this._table.firstChild.tBodies[0].rows; - var tmpCell; - var nbsp = String.fromCharCode(160); - for (var y = 0; y < rows; y++) - { - for (var x = 0; x < cols; x++) - { - tmpCell = trs[y + 2].cells[x]; - if (typeof cells[y][x] != "undefined") - { - tmpCell.className = cells[y][x].className; - tmpCell.firstChild.data = cells[y][x].text; - } - else - { - tmpCell.className = ""; - tmpCell.firstChild.data = nbsp; - } - } - } -} - -// sets the label showing the year and selected month -DatePicker.prototype._setTopLabel = function () -{ - var str = this._selectedDate.getFullYear() + " " + DatePicker.months[ this._selectedDate.getMonth() ]; - if (this._topLabel != null) - this._topLabel.lastChild.data = str; -} - -DatePicker.prototype.goToNextMonth = function () -{ - var d = new Date(this._selectedDate); - d.setDate(Math.min(d.getDate(), DatePicker.getDaysPerMonth(d.getMonth() + 1, - d.getFullYear()))); // no need to catch dec -> jan for the year - d.setMonth(d.getMonth() + 1); - this.setDate(d); -} - -DatePicker.prototype.goToPreviousMonth = function () -{ - var d = new Date(this._selectedDate); - d.setDate(Math.min(d.getDate(), DatePicker.getDaysPerMonth(d.getMonth() - 1, - d.getFullYear()))); // no need to catch jan -> dec for the year - d.setMonth(d.getMonth() - 1); - this.setDate(d); -} - -DatePicker.prototype.goToToday = function () -{ - if (this._none) - // change the selectedDate to force update if none was true - this._selectedDate = new Date(this._selectedDate + 10000000000); - this._none = false; - this.setDate(new Date(), true); -} - -DatePicker.prototype.setShowToday = function (bShowToday) -{ - if (typeof bShowToday == "string") - bShowToday = !/false|0|no/i.test(bShowToday); - - if (this._todayButton != null) - this._todayButton.style.visibility = bShowToday ? "visible" : "hidden"; - this._showToday = bShowToday; -} - -DatePicker.prototype.getShowToday = function () -{ - return this._showToday; -} - -DatePicker.prototype.setShowNone = function (bShowNone) -{ - if (typeof bShowNone == "string") - bShowNone = !/false|0|no/i.test(bShowNone); - - if (this._noneButton != null) - this._noneButton.style.visibility = bShowNone ? "visible" : "hidden"; - this._showNone = bShowNone; -} - -DatePicker.prototype.getShowNone = function () -{ - return this._showNone; -} - -// 0 is monday and 6 is sunday as in the ISO standard -DatePicker.prototype.setFirstWeekDay = function (nFirstWeekDay) -{ - if (this._firstWeekDay != nFirstWeekDay) - { - this._firstWeekDay = nFirstWeekDay; - this._updateTable(); - } -} - -DatePicker.prototype.getFirstWeekDay = function () -{ - return this._firstWeekDay; -} - -// 0 is monday and 6 is sunday as in the ISO standard -DatePicker.prototype.setRedWeekDay = function (nRedWeekDay) -{ - if (this._redWeekDay != nRedWeekDay) - { - this._redWeekDay = nRedWeekDay; - this._updateTable(); - } -} - -DatePicker.prototype.getRedWeekDay = function () -{ - return this._redWeekDay; -} - - -DatePicker.prototype._showLabelPopup = function () -{ - - /* - this._labelPopup document.createElement( "DIV" ); - div.className = "month-popup"; - div.noWrap = true; - el.unselectable = div.unselectable = "on"; - el.onselectstart = div.onselectstart = function () { return false; }; - */ - - var dateContext = function (dp, d) - { - return function (e) - { - dp._dontChangeNone = true; - dp._hideLabelPopup(); - dp.setDate(d); - dp._dontChangeNone = false; - return false; - }; - }; - - var dp = this; - - // clear all old elements in the popup - while (this._labelPopup.hasChildNodes()) - this._labelPopup.removeChild(this._labelPopup.firstChild); - - var a, tmp, tmp2; - for (var i = -3; i < 4; i++) - { - tmp = new Date(this._selectedDate); - tmp2 = new Date(this._selectedDate); // need another tmp to catch year change when checking leap - tmp2.setDate(1); - tmp2.setMonth(tmp2.getMonth() + i); - tmp.setDate(Math.min(tmp.getDate(), DatePicker.getDaysPerMonth(tmp.getMonth() + i, - tmp2.getFullYear()))); - tmp.setMonth(tmp.getMonth() + i); - - a = this._document.createElement("a"); - a.href = "javascript:void 0;"; - a.onclick = dateContext(dp, tmp); - a.appendChild(this._document.createTextNode(tmp.getFullYear() + " " + - DatePicker.months[ tmp.getMonth() ])); - if (i == 0) - a.className = "selected"; - this._labelPopup.appendChild(a); - } - - this._topLabel.parentNode.insertBefore(this._labelPopup, this._topLabel.parentNode.firstChild); -}; - -DatePicker.prototype._hideLabelPopup = function () -{ - if (this._labelPopup.parentNode) - this._labelPopup.parentNode.removeChild(this._labelPopup); -}; - -DatePicker._daysPerMonth = [31,28,31,30,31,30,31,31,30,31,30,31]; -DatePicker.getDaysPerMonth = function (nMonth, nYear) -{ - nMonth = (nMonth + 12) % 12; - var res = DatePicker._daysPerMonth[nMonth]; - if (nMonth == 1) - { - res += nYear % 4 == 0 && !(nYear % 400 == 0) ? 1 : 0; - } - return res; -}; \ No newline at end of file +/*----------------------------------------------------------------------------\ +| Date Picker 1.06 | +|-----------------------------------------------------------------------------| +| Created by Erik Arvidsson | +| (http://webfx.eae.net/contact.html#erik) | +| For WebFX (http://webfx.eae.net/) | +|-----------------------------------------------------------------------------| +| A DOM based Date Picker | +|-----------------------------------------------------------------------------| +| Copyright (c) 1999, 2002, 2002, 2003, 2004, 2006 Erik Arvidsson | +|-----------------------------------------------------------------------------| +| Licensed under the Apache License, Version 2.0 (the "License"); you may not | +| use this file except in compliance with the License. You may obtain a copy | +| of the License at http://www.apache.org/licenses/LICENSE-2.0 | +| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | +| WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | +| License for the specific language governing permissions and limitations | +| under the License. | +|-----------------------------------------------------------------------------| +| Dependencies: datepicker.css Date picker style declarations | +|-----------------------------------------------------------------------------| +| 2002-02-10 | Changed _update method to only update the text nodes instead | +| | rewriting the entire table. Also added support for mouse wheel | +| | in IE6. | +| 2002-01-14 | Cleaned up for 1.0 public version | +| 2002-01-15 | Replace all innerHTML calls with DOM1 methods | +| 2002-01-18 | Minor IE6 bug that occured when dragging the mouse | +| 2002-01-19 | Added a popup that is shown when the user clicks on the month. | +| | This allows navigation to 6 adjacent months. | +| 2002-04-10 | Fixed a bug that occured in the popup when a date was selected | +| | that caused surroundung months to "overflow" | +| | This had the effect that one could get two October months | +| | listed. | +| 2002-09-06 | I had missed one place were window was used instead of | +| | doc.parentWindow | +| 2003-08-28 | Added support for ensurin no date overflow when changing | +| | months. | +| 2004-01-10 | Adding type on the buttons to ensure they are not submit | +| | buttons. Minor CSS change for CSS2 | +| 2006-05-28 | Changed license to Apache Software License 2.0. | +| 2011-07-27 | Separated "selected date" and "calendar date" concepts. | +| | Selected date is the date specifically selected by the user to | +| | put into the form field. Calendar date reflects the currently | +| | displayed month. These are often, but not always, the same | +| | value. Separating them simplifies a lot of logic and resolves | +| | TAP5-1409. Also somewhat smarter for whether to trigger | +| | onselect when clicking "today" (and/or "none") | +|-----------------------------------------------------------------------------| +| Created 2001-10-?? | All changes are in the log above. | Updated 2006-05-28 | +\----------------------------------------------------------------------------*/ + +// The DatePicker constructor +// oDate : Date Optional argument representing the date to select +// Note: some minor modifications for Tapestry, to work well as a popup. +function DatePicker(oDate) +{ + // check arguments + if (arguments.length == 0) + { + this._selectedDate = null; + this._calendarDate = new Date; + this._selectedInited = false; + } + else + { + this._selectedDate = oDate; + if (!oDate) + { + this._calendarDate = new Date; + } else + { + this._calendarDate = new Date(oDate); + } + this._selectedInited = true; + + } + + this._matrix = [[],[],[],[],[],[],[]]; + this._showNone = true; + this._showToday = true; + this._firstWeekDay = 0; // start week with monday according to standards + this._redWeekDay = 6; // sunday is the default red day. +} + +// two static fields describing the name of the months abd days +DatePicker.months = [ + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December"]; +DatePicker.days = ["m", "t", "w", "t", "f", "s", "s"]; + + +// Function invoked whenever the selected date changes, whether by +// navigation or when the user selects a date. +DatePicker.prototype.onchange = function () +{ +}; + +// onselect is more specified than onchange, and is triggered only when the user makes a specific selection +// using the calendar (rather than navigating to a new month). For Tapestry, +// this will dismiss the popup. +DatePicker.prototype.onselect = function() +{ +} + + +// create the nodes inside the date picker +DatePicker.prototype.create = function (doc) +{ + if (doc == null) doc = document; + + this._document = doc; + + // create elements + this._el = doc.createElement("div"); + this._el.className = "datePicker"; + + // header + var div = doc.createElement("div"); + div.className = "header"; + this._el.appendChild(div); + + var headerTable = doc.createElement("table"); + headerTable.className = "headerTable"; + headerTable.cellSpacing = 0; + div.appendChild(headerTable); + + var tBody = doc.createElement("tbody"); + headerTable.appendChild(tBody); + + var tr = doc.createElement("tr"); + tBody.appendChild(tr); + + var td = doc.createElement("td"); + this._previousMonth = doc.createElement("button"); + this._previousMonth.className = "previousButton"; + this._previousMonth.setAttribute("type", "button"); + td.appendChild(this._previousMonth); + tr.appendChild(td); + + td = doc.createElement("td"); + td.className = "labelContainer"; + tr.appendChild(td); + + this._topLabel = doc.createElement("a"); + this._topLabel.className = "topLabel"; + this._topLabel.href = "#"; + this._topLabel.appendChild(doc.createTextNode(String.fromCharCode(160))); + td.appendChild(this._topLabel); + + this._labelPopup = doc.createElement("div"); + this._labelPopup.className = "labelPopup"; + // no insertion + + td = doc.createElement("td"); + this._nextMonth = doc.createElement("button"); + this._nextMonth.className = "nextButton"; + this._nextMonth.setAttribute("type", "button"); + td.appendChild(this._nextMonth); + tr.appendChild(td); + + // grid + div = doc.createElement("div"); + div.className = "grid"; + this._el.appendChild(div); + this._table = div; + + // footer + div = doc.createElement("div"); + div.className = "footer"; + this._el.appendChild(div); + + var footerTable = doc.createElement("table"); + footerTable.className = "footerTable"; + footerTable.cellSpacing = 0; + div.appendChild(footerTable); + + tBody = doc.createElement("tbody"); + footerTable.appendChild(tBody); + + tr = doc.createElement("tr"); + tBody.appendChild(tr); + + td = doc.createElement("td"); + this._todayButton = doc.createElement("button"); + this._todayButton.className = "todayButton"; + this._todayButton.setAttribute("type", "button"); + this._todayButton.appendChild(doc.createTextNode("Today")); + td.appendChild(this._todayButton); + tr.appendChild(td); + + td = doc.createElement("td"); + td.className = "filler"; + td.appendChild(doc.createTextNode(String.fromCharCode(160))); + tr.appendChild(td); + + td = doc.createElement("td"); + this._noneButton = doc.createElement("button"); + this._noneButton.className = "noneButton"; + this._noneButton.setAttribute("type", "button"); + this._noneButton.appendChild(doc.createTextNode("None")); + td.appendChild(this._noneButton); + tr.appendChild(td); + + + this._createTable(doc); + + this._updateTable(); + this._setTopLabel(); + + if (!this._showNone) + this._noneButton.style.visibility = "hidden"; + if (!this._showToday) + this._todayButton.style.visibility = "hidden"; + + // IE55+ extension + this._previousMonth.hideFocus = true; + this._nextMonth.hideFocus = true; + this._todayButton.hideFocus = true; + this._noneButton.hideFocus = true; + // end IE55+ extension + + // hook up events + var dp = this; + // buttons + this._previousMonth.onclick = function () + { + dp.goToPreviousMonth(); + }; + this._nextMonth.onclick = function () + { + dp.goToNextMonth(); + }; + this._todayButton.onclick = function () + { + dp.goToToday(); + }; + this._noneButton.onclick = function () + { + //this should always clear the date and trigger onselected... + dp.setDate(null, true); + }; + + this._el.onselectstart = function () + { + return false; + }; + + this._table.onclick = function (e) + { + // find event + if (e == null) e = doc.parentWindow.event; + + // find td + var el = e.target != null ? e.target : e.srcElement; + while (el.nodeType != 1) + el = el.parentNode; + while (el != null && el.tagName && el.tagName.toLowerCase() != "td") + el = el.parentNode; + + // if no td found, return + if (el == null || el.tagName == null || el.tagName.toLowerCase() != "td") + return; + + var d = new Date(dp._calendarDate); + var n = Number(el.firstChild.data); + if (isNaN(n) || n <= 0 || n == null) + return; + + d.setDate(n); + dp.setDate(d); + }; + + // show popup + this._topLabel.onclick = function (e) + { + dp._showLabelPopup(); + return false; + }; + + this._el.onkeydown = function (e) + { + if (e == null) e = doc.parentWindow.event; + var kc = e.keyCode != null ? e.keyCode : e.charCode; + + if (kc < 37 || kc > 40) return true; + + var d = new Date(dp._calendarDate).valueOf(); + if (kc == 37) // left + d -= 24 * 60 * 60 * 1000; + else if (kc == 39) // right + d += 24 * 60 * 60 * 1000; + else if (kc == 38) // up + d -= 7 * 24 * 60 * 60 * 1000; + else if (kc == 40) // down + d += 7 * 24 * 60 * 60 * 1000; + + dp.setCalendarDate(new Date(d)); + return false; + } + + // ie6 extension + this._el.onmousewheel = function (e) + { + if (e == null) e = doc.parentWindow.event; + var n = - e.wheelDelta / 120; + var d = new Date(dp._calendarDate); + var m = d.getMonth() + n; + d.setMonth(m); + + + dp.setCalendarDate(d); + + return false; + } + + doc.onclick = function (e) { + var targ; + + // find event + if (e == null) e = doc.parentWindow.event; + + if (e.target) targ = e.target; + else if (e.srcElement) targ = e.srcElement; + // find classname 'datePicker' as parent + var insideDatePicker = null; + var parent = targ.parentNode; + while (parent != null) { + if (parent.className == 'datePicker') { + insideDatePicker = parent; + break; + } + parent = parent.parentNode; + } + + if (Tapestry.DateField.activeDateField != null) { + + if (insideDatePicker == null && targ.className != 't-calendar-trigger') { + Tapestry.DateField.activeDateField.hidePopup(); + Tapestry.DateField.activeDateField = null; + } + } + } + return this._el; +}; + +DatePicker.prototype.setCalendarDate = function(oDate) +{ + if (oDate != null) + { + //note that calendarDate should never be null! + this._calendarDate = oDate; + } + this._hideLabelPopup(); + this._setTopLabel(); + this._updateTable(); +} + +DatePicker.prototype.setDate = function (oDate, forceOnSelect) +{ + + // if null then set None + if (oDate == null) + { + //if _selectedDate isn't null, then this is an actual change... + //but if it /is/ null, we have to see if we were inited or not. If we weren't inited, then we're + //setting this to null now, and we shouldn't fire a select... + //but the problem occurs on subsequent... hm... + if (this._selectedDate != null) + { + this._selectedDate = null; + if (typeof this.onchange == "function") + this.onchange(); + this.onselect(); + } else if (forceOnSelect) + this.onselect(); + //note: setDate must inherently set the calendar date + this._selectedInited=true; + this.setCalendarDate(null); + + return; + } + + // if string or number create a Date object + if (typeof oDate == "string" || typeof oDate == "number") + { + oDate = new Date(oDate); + } + + // do not update if not really changed + if (this._selectedDate == null || !this._datesAreSame(this._selectedDate, oDate)) + { + this._selectedDate = new Date(oDate); + + if (typeof this.onchange == "function") + this.onchange(); + + //so if _selectedInited is false, then the value is different only because we set the value programmatically, post-initialization. + //that handles the creation + set event. Subsequent reveals will set it to whatever _selectedDate already was, so it's handled. + if (this._selectedInited) + this.onselect(); + else + this._selectedInited=true; + } else if (forceOnSelect) + this.onselect(); + //note: setDate must inherently set the calendar date + this.setCalendarDate(oDate); + +} + + +DatePicker.prototype.getDate = function () +{ + if (!this._selectedDate) return null; + return new Date(this._selectedDate); // create a new instance +} + +// creates the table elements and inserts them into the date picker +DatePicker.prototype._createTable = function (doc) +{ + var str, i; + var rows = 6; + var cols = 7; + var currentWeek = 0; + + var table = doc.createElement("table"); + table.className = "gridTable"; + table.cellSpacing = 0; + + var tBody = doc.createElement("tbody"); + table.appendChild(tBody); + + // days row + var tr = doc.createElement("tr"); + tr.className = "daysRow"; + + var td, tn; + var nbsp = String.fromCharCode(160); + for (i = 0; i < cols; i++) + { + td = doc.createElement("td"); + td.appendChild(doc.createTextNode(nbsp)); + tr.appendChild(td); + } + tBody.appendChild(tr); + + // upper line + tr = doc.createElement("tr"); + td = doc.createElement("td"); + td.className = "upperLine"; + td.colSpan = 7; + tr.appendChild(td); + tBody.appendChild(tr); + + // rest + for (i = 0; i < rows; i++) + { + tr = doc.createElement("tr"); + for (var j = 0; j < cols; j++) + { + td = doc.createElement("td"); + td.appendChild(doc.createTextNode(nbsp)); + tr.appendChild(td); + } + tBody.appendChild(tr); + } + str += "</table>"; + + if (this._table != null) + this._table.appendChild(table) +}; +// this method updates all the text nodes inside the table as well +// as all the classNames on the tds +DatePicker.prototype._updateTable = function () +{ + // if no element no need to continue + if (this._table == null) return; + + var i; + var str = ""; + var rows = 6; + var cols = 7; + var currentWeek = 0; + + var cells = new Array(rows); + this._matrix = new Array(rows) + for (i = 0; i < rows; i++) + { + cells[i] = new Array(cols); + this._matrix[i] = new Array(cols); + } + + // Set the tmpDate to this month + var tmpDate = new Date(this._calendarDate.getFullYear(), + this._calendarDate.getMonth(), 1); + var today = new Date(); + // go thorugh all days this month and store the text + // and the class name in the cells matrix + for (i = 1; i < 32; i++) + { + tmpDate.setDate(i); + // convert to ISO, Monday is 0 and 6 is Sunday + var weekDay = ( tmpDate.getDay() + 6 ) % 7; + var colIndex = ( weekDay - this._firstWeekDay + 7 ) % 7; + if (tmpDate.getMonth() == this._calendarDate.getMonth()) + { + + var isToday = this._datesAreSame(tmpDate, today); + + cells[currentWeek][colIndex] = { text: "", className: "" }; + + if (this._datesAreSame(this._selectedDate, tmpDate)) + cells[currentWeek][colIndex].className += "selected "; + if (isToday) + cells[currentWeek][colIndex].className += "today "; + if (( tmpDate.getDay() + 6 ) % 7 == this._redWeekDay) // ISO + cells[currentWeek][colIndex].className += "red"; + + cells[currentWeek][colIndex].text = + this._matrix[currentWeek][colIndex] = tmpDate.getDate(); + + if (colIndex == 6) + currentWeek++; + } + } + + // fix day letter order if not standard + var weekDays = DatePicker.days; + if (this._firstWeekDay != 0) + { + weekDays = new Array(7); + for (i = 0; i < 7; i++) + weekDays[i] = DatePicker.days[ (i + this._firstWeekDay) % 7]; + } + + // update text in days row + var tds = this._table.firstChild.tBodies[0].rows[0].cells; + for (i = 0; i < cols; i++) + tds[i].firstChild.data = weekDays[i]; + + // update the text nodes and class names + var trs = this._table.firstChild.tBodies[0].rows; + var tmpCell; + var nbsp = String.fromCharCode(160); + for (var y = 0; y < rows; y++) + { + for (var x = 0; x < cols; x++) + { + tmpCell = trs[y + 2].cells[x]; + if (typeof cells[y][x] != "undefined") + { + tmpCell.className = cells[y][x].className; + tmpCell.firstChild.data = cells[y][x].text; + } + else + { + tmpCell.className = ""; + tmpCell.firstChild.data = nbsp; + } + } + } +} + +// sets the label showing the year and selected month +DatePicker.prototype._setTopLabel = function () +{ + var str = this._calendarDate.getFullYear() + " " + DatePicker.months[ this._calendarDate.getMonth() ]; + if (this._topLabel != null) + this._topLabel.lastChild.data = str; +} + +DatePicker.prototype.goToNextMonth = function () +{ + var d = new Date(this._calendarDate); + d.setDate(Math.min(d.getDate(), DatePicker.getDaysPerMonth(d.getMonth() + 1, + d.getFullYear()))); // no need to catch dec -> jan for the year + d.setMonth(d.getMonth() + 1); + this.setCalendarDate(d); +} + +DatePicker.prototype.goToPreviousMonth = function () +{ + var d = new Date(this._calendarDate); + d.setDate(Math.min(d.getDate(), DatePicker.getDaysPerMonth(d.getMonth() - 1, + d.getFullYear()))); // no need to catch jan -> dec for the year + d.setMonth(d.getMonth() - 1); + this.setCalendarDate(d); +} + +DatePicker.prototype.goToToday = function () +{ + //note: small tweak here so that clicking the "Today" button will properly update the selected date and trigger selected + //but note that we want this behavior iff "today" is already selected and visible. + //For instance: If you're looking at some date months away from today and want to jump back to today AND today is the selectedDate + //then we don't want that to close the calendar. + var today = new Date(); + var forceOnSelect=false; + if (this._selectedDate == null || (this._datesAreSame(today, this._selectedDate) && this._calendarDate.getMonth() == today.getMonth() && this._calendarDate.getFullYear() == today.getFullYear())) { + //then go ahead and force the selection... + forceOnSelect=true; + } + this.setDate(new Date(), forceOnSelect);//note that setDate calls setCalendarDate... +} + +DatePicker.prototype.setShowToday = function (bShowToday) +{ + if (typeof bShowToday == "string") + bShowToday = !/false|0|no/i.test(bShowToday); + + if (this._todayButton != null) + this._todayButton.style.visibility = bShowToday ? "visible" : "hidden"; + this._showToday = bShowToday; +} + +DatePicker.prototype.getShowToday = function () +{ + return this._showToday; +} + +DatePicker.prototype.setShowNone = function (bShowNone) +{ + if (typeof bShowNone == "string") + bShowNone = !/false|0|no/i.test(bShowNone); + + if (this._noneButton != null) + this._noneButton.style.visibility = bShowNone ? "visible" : "hidden"; + this._showNone = bShowNone; +} + +DatePicker.prototype.getShowNone = function () +{ + return this._showNone; +} + +// 0 is monday and 6 is sunday as in the ISO standard +DatePicker.prototype.setFirstWeekDay = function (nFirstWeekDay) +{ + if (this._firstWeekDay != nFirstWeekDay) + { + this._firstWeekDay = nFirstWeekDay; + this._updateTable(); + } +} + +DatePicker.prototype.getFirstWeekDay = function () +{ + return this._firstWeekDay; +} + +// 0 is monday and 6 is sunday as in the ISO standard +DatePicker.prototype.setRedWeekDay = function (nRedWeekDay) +{ + if (this._redWeekDay != nRedWeekDay) + { + this._redWeekDay = nRedWeekDay; + this._updateTable(); + } +} + +DatePicker.prototype.getRedWeekDay = function () +{ + return this._redWeekDay; +} + + +DatePicker.prototype._showLabelPopup = function () +{ + + var dateContext = function (dp, d) + { + return function (e) + { + dp._hideLabelPopup(); + dp.setCalendarDate(d); + return false; + }; + }; + + var dp = this; + + // clear all old elements in the popup + while (this._labelPopup.hasChildNodes()) + this._labelPopup.removeChild(this._labelPopup.firstChild); + + var a, tmp, tmp2; + for (var i = -3; i < 4; i++) + { + tmp = new Date(this._calendarDate); + tmp2 = new Date(this._calendarDate); // need another tmp to catch year change when checking leap + tmp2.setDate(1); + tmp2.setMonth(tmp2.getMonth() + i); + tmp.setDate(Math.min(tmp.getDate(), DatePicker.getDaysPerMonth(tmp.getMonth() + i, + tmp2.getFullYear()))); + tmp.setMonth(tmp.getMonth() + i); + + a = this._document.createElement("a"); + a.href = "javascript:void 0;"; + a.onclick = dateContext(dp, tmp); + a.appendChild(this._document.createTextNode(tmp.getFullYear() + " " + + DatePicker.months[ tmp.getMonth() ])); + if (i == 0) + a.className = "selected"; + this._labelPopup.appendChild(a); + } + + this._topLabel.parentNode.insertBefore(this._labelPopup, this._topLabel.parentNode.firstChild); +}; + +DatePicker.prototype._hideLabelPopup = function () +{ + if (this._labelPopup.parentNode) + this._labelPopup.parentNode.removeChild(this._labelPopup); +}; + +DatePicker.prototype._datesAreSame = function(d1,d2) +{ + if (d1 == null && d2 == null) + return true; + else if (d1 == null) + return false; + else if (d2 == null) + return false; + return d1.getDate() == d2.getDate() && d1.getMonth() == d2.getMonth() && d1.getFullYear() == d2.getFullYear(); +} + +DatePicker._daysPerMonth = [31,28,31,30,31,30,31,31,30,31,30,31]; +DatePicker.getDaysPerMonth = function (nMonth, nYear) +{ + nMonth = (nMonth + 12) % 12; + var res = DatePicker._daysPerMonth[nMonth]; + if (nMonth == 1) + { + res += nYear % 4 == 0 && !(nYear % 400 == 0) ? 1 : 0; + } + return res; +}; Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java?rev=1151673&r1=1151672&r2=1151673&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/FormTests.java Wed Jul 27 23:46:17 2011 @@ -18,6 +18,9 @@ import org.apache.tapestry5.corelib.comp import org.apache.tapestry5.integration.TapestryCoreTestCase; import org.testng.annotations.Test; +import java.text.SimpleDateFormat; +import java.util.Date; + /** * Tests for the {@link Form} component as well as many form control components. */ @@ -245,6 +248,96 @@ public class FormTests extends TapestryC assertBubbleMessage("asteroidImpact", "Unparseable date: \"<script>alert('T5 is great'); </script>\""); } + // TAP5-1409 + @Test + public void datefield_select_newmonth_samedate() + { + openLinks("DateField Demo", "clear", "english"); + //start with a known date... + type("asteroidImpact", "05/28/2046"); + + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + assertEquals(getText("css=td.selected"), "28"); + + //move to the next month. + click("css=button.nextButton"); + //first, make sure that NOTHING shows as selected! The selected date is still 5/28/46 + String selectedGoneCondition = "window.$$(\"td.selected\").size() == 0"; + waitForCondition(selectedGoneCondition, PAGE_LOAD_TIMEOUT); + + //make sure it's still selected if we navigate back... + click("css=button.previousButton"); + waitForCSSSelectedElementToAppear("td.selected"); + + click("css=button.nextButton"); + waitForCondition(selectedGoneCondition, PAGE_LOAD_TIMEOUT); + + click("xpath=//td[text()='28']"); + String pickerGoneCondition = "!selenium.isVisible('css=div.datePicker')"; + waitForCondition(pickerGoneCondition, PAGE_LOAD_TIMEOUT); + + assertFieldValue("asteroidImpact", "6/28/2046"); + + //a few other behaviors to check on as a side-effect of implementing the fix for 1409: + //1) If today is selected and it's the current month, pressing the "Today" button should close the popup + //2) If today is selected and we're on some other month, pressing the "Today" button should just take us + // back to the today. + //3) If today is not selected, pressing the "Today" button should set the date and close the popup. + //4) Pressing the "None" button should always close the popup and result in no date. + + //#3 + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + click("css=button.todayButton"); + waitForCondition(pickerGoneCondition, PAGE_LOAD_TIMEOUT); + + String value = getValue("asteroidImpact"); + assertEquals(value, new SimpleDateFormat("M/d/yyyy").format(new Date())); + + //#2... + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + click("css=button.nextButton"); + waitForCondition(selectedGoneCondition, PAGE_LOAD_TIMEOUT); + + click("css=button.todayButton"); + waitForCSSSelectedElementToAppear("td.selected"); + + //#1 + click("css=button.todayButton"); + waitForCondition(pickerGoneCondition, PAGE_LOAD_TIMEOUT); + assertEquals(getValue("asteroidImpact"), value); + + //#4... + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + click("css=button.noneButton"); + waitForCondition(pickerGoneCondition, PAGE_LOAD_TIMEOUT); + assertEquals(getValue("asteroidImpact"), ""); + + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + assertFalse(isElementPresent("css=td.selected")); + click("css=button.noneButton"); + waitForCondition(pickerGoneCondition, PAGE_LOAD_TIMEOUT); + assertEquals(getValue("asteroidImpact"), ""); + } + + // TAP4-1408 + @Test + public void datefield_clickoutside_closes() + { + openLinks("DateField Demo", "clear", "english"); + type("asteroidImpact", "05/28/2046"); + + click("id=asteroidImpact-trigger"); + waitForCSSSelectedElementToAppear("div.datePicker"); + + click("id=asteroidImpact"); + waitForCondition("!selenium.isVisible('css=div.datePicker')", PAGE_LOAD_TIMEOUT); + } + @Test public void event_based_translate() throws Exception { Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/DateFieldDemo.java URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/DateFieldDemo.java?rev=1151673&r1=1151672&r2=1151673&view=diff ============================================================================== --- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/DateFieldDemo.java (original) +++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/DateFieldDemo.java Wed Jul 27 23:46:17 2011 @@ -16,6 +16,7 @@ package org.apache.tapestry5.integration import org.apache.tapestry5.annotations.Persist; import org.apache.tapestry5.beaneditor.Validate; +import org.apache.tapestry5.corelib.components.DateField; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.services.PersistentLocale; @@ -48,6 +49,7 @@ public class DateFieldDemo public DateFormat getDateFormat() { + DateField df; return new SimpleDateFormat("MM/dd/yyyy"); }