参考资料:
https://market.sencha.com/extensions/datetimepicker
上面的扩展在2.2有些问题,参考源码重新写了一个
TimePicker:
1 Ext.define('ux.TimePicker', { 2 extend: 'Ext.picker.Picker', 3 xtype: 'timePicker', 4 alternateClassName: 'timePicker', 5 requires: ['Ext.DateExtras'], 6 7 /** 8 * @event change 9 * Fired when the value of this picker has changed and the done button is pressed. 10 * @param {Ext.picker.Date} this This Picker 11 * @param {Date} value The date value 12 */ 13 14 config: { 15 /** 16 * @cfg {Number} yearFrom 17 * 开始年份,如果他比yearTo大,则选择顺序颠倒 18 * @accessor 19 */ 20 yearFrom: 1980, 21 22 /** 23 * @cfg {Number} [yearTo=new Date().getFullYear()] 24 * 结束年份 25 * @accessor 26 */ 27 yearTo: new Date().getFullYear(), 28 29 /** 30 * @cfg {String} monthText 31 * 月显示值 32 * @accessor 33 */ 34 monthText: '月', 35 36 /** 37 * @cfg {String} dayText 38 * 日显示值 39 * @accessor 40 */ 41 dayText: '日', 42 43 /** 44 * @cfg {String} yearText 45 * 年显示值 46 * @accessor 47 */ 48 yearText: '年', 49 50 /** 51 * @cfg {String} hourText 52 * 小时显示值 53 */ 54 hourText: '时', 55 56 /** 57 * @cfg {String} minuteText 58 * 分显示值 59 */ 60 minuteText: '分', 61 62 /** 63 * @cfg {String} ampmText 64 * 上午/下午显示值 65 */ 66 ampmText: '上午/下午', 67 /** 68 * @cfg {Array} slotOrder 69 * 默认的选项列表 70 * @accessor 71 */ 72 slotOrder: ['year', 'month', 'day', 'hour', 'minute'], // 73 74 /** 75 * @cfg {Int} 76 *分钟间隔 77 * @accessor 78 */ 79 minuteInterval: 1, 80 81 /** 82 * @cfg {Boolean} ampm 83 *是否使用12小时制 84 * @accessor 85 */ 86 ampm: false, 87 useTitles: true 88 }, 89 90 platformConfig: [{ 91 theme: ['Windows'], 92 doneButton: { 93 iconCls: 'check2', 94 ui: 'round', 95 text: '' 96 } 97 }], 98 99 initialize: function () { 100 this.callParent(); 101 102 this.on({ 103 scope: this, 104 delegate: '> slot', 105 slotpick: this.onSlotPick 106 }); 107 108 this.on({ 109 scope: this, 110 show: this.onSlotPick 111 }); 112 }, 113 114 setValue: function (value, animated) { 115 if (Ext.isDate(value)) { 116 var ampm = 'AM', 117 currentHours = hour = value.getHours(); 118 if (this.getAmpm()) { 119 if (currentHours > 12) { 120 ampm = "PM"; 121 hour -= 12; 122 } else if (currentHours == 12) { 123 ampm = "PM"; 124 } else if (currentHours == 0) { 125 hour = 12; 126 } 127 } 128 value = { 129 day: value.getDate(), 130 month: value.getMonth() + 1, 131 year: value.getFullYear(), 132 hour: hour, 133 minute: value.getMinutes(), 134 ampm: ampm 135 }; 136 } 137 138 this.callParent([value, animated]); 139 this.onSlotPick(); 140 }, 141 //获取值 142 getValue: function (useDom) { 143 var values = {}, 144 items = this.getItems().items, 145 ln = items.length, 146 daysInMonth, day, month, year, hour, minute, item, i; 147 148 for (i = 0; i < ln; i++) { 149 item = items[i]; 150 if (item instanceof Ext.picker.Slot) { 151 values[item.getName()] = item.getValue(useDom); 152 } 153 } 154 155 //if all the slots return null, we should not return a date 156 if (values.year === null && values.month === null && values.day === null && values.hour === null && values.minute === null) { 157 return null; 158 } 159 160 year = Ext.isNumber(values.year) ? values.year : 1; 161 month = Ext.isNumber(values.month) ? values.month : 1; 162 day = Ext.isNumber(values.day) ? values.day : 1; 163 hour = Ext.isNumber(values.hour) ? values.hour : 1; 164 minute = Ext.isNumber(values.minute) ? values.minute : 1; 165 166 if (month && year && month && day) { 167 daysInMonth = this.getDaysInMonth(month, year); 168 } 169 day = (daysInMonth) ? Math.min(day, daysInMonth) : day; 170 if (values.ampm && values.ampm == "PM" && hour < 12) { 171 hour = hour + 12; 172 } 173 if (values.ampm && values.ampm == "AM" && hour == 12) { 174 hour = 0; 175 } 176 return new Date(year, month - 1, day, hour, minute); 177 }, 178 179 /** 180 * Updates the yearFrom configuration 181 */ 182 updateYearFrom: function () { 183 if (this.initialized) { 184 this.createSlots(); 185 } 186 }, 187 188 /** 189 * Updates the yearTo configuration 190 */ 191 updateYearTo: function () { 192 if (this.initialized) { 193 this.createSlots(); 194 } 195 }, 196 197 /** 198 * Updates the monthText configuration 199 */ 200 updateMonthText: function (newMonthText, oldMonthText) { 201 var innerItems = this.getInnerItems, 202 ln = innerItems.length, 203 item, i; 204 205 //loop through each of the current items and set the title on the correct slice 206 if (this.initialized) { 207 for (i = 0; i < ln; i++) { 208 item = innerItems[i]; 209 210 if ((typeof item.title == "string" && item.title == oldMonthText) || (item.title.html == oldMonthText)) { 211 item.setTitle(newMonthText); 212 } 213 } 214 } 215 }, 216 217 /** 218 * Updates the {@link #dayText} configuration. 219 */ 220 updateDayText: function (newDayText, oldDayText) { 221 var innerItems = this.getInnerItems, 222 ln = innerItems.length, 223 item, i; 224 225 //loop through each of the current items and set the title on the correct slice 226 if (this.initialized) { 227 for (i = 0; i < ln; i++) { 228 item = innerItems[i]; 229 230 if ((typeof item.title == "string" && item.title == oldDayText) || (item.title.html == oldDayText)) { 231 item.setTitle(newDayText); 232 } 233 } 234 } 235 }, 236 237 /** 238 * Updates the yearText configuration 239 */ 240 updateYearText: function (yearText) { 241 var innerItems = this.getInnerItems, 242 ln = innerItems.length, 243 item, i; 244 245 //loop through each of the current items and set the title on the correct slice 246 if (this.initialized) { 247 for (i = 0; i < ln; i++) { 248 item = innerItems[i]; 249 250 if (item.title == this.yearText) { 251 item.setTitle(yearText); 252 } 253 } 254 } 255 }, 256 257 // @private 258 constructor: function () { 259 this.callParent(arguments); 260 this.createSlots(); 261 }, 262 263 /** 264 * Generates all slots for all years specified by this component, and then sets them on the component 265 * @private 266 */ 267 createSlots: function () { 268 var me = this, 269 slotOrder = me.getSlotOrder(), 270 yearsFrom = me.getYearFrom(), 271 yearsTo = me.getYearTo(), 272 years = [], 273 days = [], 274 months = [], 275 hours = [], 276 minutes = [], 277 ampm = [], 278 reverse = yearsFrom > yearsTo, 279 ln, i, daysInMonth; 280 281 if (!this.getAmpm()) { 282 var index = slotOrder.indexOf('ampm') 283 if (index >= 0) { 284 slotOrder.splice(index); 285 } 286 } 287 //填充年列表 288 while (yearsFrom) { 289 years.push({ 290 text: yearsFrom, 291 value: yearsFrom 292 }); 293 294 if (yearsFrom === yearsTo) { 295 break; 296 } 297 298 if (reverse) { 299 yearsFrom--; 300 } else { 301 yearsFrom++; 302 } 303 } 304 //填充天列表 305 daysInMonth = me.getDaysInMonth(1, new Date().getFullYear()); 306 307 for (i = 0; i < daysInMonth; i++) { 308 days.push({ 309 text: i + 1, 310 value: i + 1 311 }); 312 } 313 //填充月列表 314 for (i = 0, ln = Ext.Date.monthNames.length; i < ln; i++) { 315 months.push({ 316 text: Ext.Date.monthNames[i], 317 value: i + 1 318 }); 319 } 320 //填充小时列表 321 var hourLimit = (this.getAmpm()) ? 12 : 23 322 var hourStart = (this.getAmpm()) ? 1 : 0 323 for (i = hourStart; i <= hourLimit; i++) { 324 hours.push({ 325 text: this.pad2(i), 326 value: i 327 }); 328 } 329 //填充分钟列表 330 for (i = 0; i < 60; i += this.getMinuteInterval()) { 331 minutes.push({ 332 text: this.pad2(i), 333 value: i 334 }); 335 } 336 //填充上午/下午 337 ampm.push({ 338 text: '上午', 339 value: 'AM' 340 }, { 341 text: '下午', 342 value: 'PM' 343 }); 344 345 var slots = []; 346 347 slotOrder.forEach(function (item) { 348 slots.push(me.createSlot(item, days, months, years, hours, minutes, ampm)); 349 }); 350 351 me.setSlots(slots); 352 }, 353 354 /** 355 * Returns a slot config for a specified date. 356 * @private 357 */ 358 createSlot: function (name, days, months, years, hours, minutes, ampm) { 359 switch (name) { 360 case 'year': 361 return { 362 name: 'year', 363 align: 'center', 364 data: years, 365 title: this.getYearText(), 366 flex: 3 367 }; 368 case 'month': 369 return { 370 name: name, 371 align: 'center', 372 data: months, 373 title: this.getMonthText(), 374 flex: 4 375 }; 376 case 'day': 377 return { 378 name: 'day', 379 align: 'center', 380 data: days, 381 width: '1px', 382 title: this.getDayText(), 383 flex: 2 384 }; 385 case 'hour': 386 return { 387 name: 'hour', 388 align: 'center', 389 data: hours, 390 title: this.getHourText(), 391 flex: 2 392 }; 393 case 'minute': 394 return { 395 name: 'minute', 396 align: 'center', 397 data: minutes, 398 title: this.getMinuteText(), 399 flex: 2 400 }; 401 case 'ampm': 402 return { 403 name: 'ampm', 404 align: 'center', 405 data: ampm, 406 title: this.getAmpmText(), 407 flex: 2 408 }; 409 } 410 }, 411 412 onSlotPick: function () { 413 var value = this.getValue(true), 414 slot = this.getDaySlot(), 415 year = value.getFullYear(), 416 month = value.getMonth(), 417 days = [], 418 daysInMonth, i; 419 if (!value || !Ext.isDate(value) || !slot) { 420 return; 421 } 422 423 this.callParent(arguments); 424 425 //get the new days of the month for this new date 426 daysInMonth = this.getDaysInMonth(month + 1, year); 427 for (i = 0; i < daysInMonth; i++) { 428 days.push({ 429 text: i + 1, 430 value: i + 1 431 }); 432 } 433 // We don't need to update the slot days unless it has changed 434 if (slot.getStore().getCount() == days.length) { 435 return; 436 } 437 438 slot.getStore().setData(days); 439 440 // Now we have the correct amount of days for the day slot, lets update it 441 var store = slot.getStore(), 442 viewItems = slot.getViewItems(), 443 valueField = slot.getValueField(), 444 index, item; 445 446 index = store.find(valueField, value.getDate()); 447 if (index == -1) { 448 return; 449 } 450 451 item = Ext.get(viewItems[index]); 452 453 slot.selectedIndex = index; 454 slot.scrollToItem(item); 455 slot.setValue(slot.getValue(true)); 456 457 458 }, 459 460 getDaySlot: function () { 461 var innerItems = this.getInnerItems(), 462 ln = innerItems.length, 463 i, slot; 464 465 if (this.daySlot) { 466 return this.daySlot; 467 } 468 469 for (i = 0; i < ln; i++) { 470 slot = innerItems[i]; 471 if (slot.isSlot && slot.getName() == "day") { 472 this.daySlot = slot; 473 return slot; 474 } 475 } 476 477 return null; 478 }, 479 480 // @private 481 getDaysInMonth: function (month, year) { 482 var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 483 return month == 2 && this.isLeapYear(year) ? 29 : daysInMonth[month - 1]; 484 }, 485 486 // @private 487 isLeapYear: function (year) { 488 return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))); 489 }, 490 491 onDoneButtonTap: function () { 492 var oldValue = this._value, 493 newValue = this.getValue(true), 494 testValue = newValue; 495 496 if (Ext.isDate(newValue)) { 497 testValue = newValue.toDateString(); 498 } 499 if (Ext.isDate(oldValue)) { 500 oldValue = oldValue.toDateString(); 501 } 502 if (testValue != oldValue) { 503 this.fireEvent('change', this, newValue); 504 } 505 this.hide(); 506 this.inputBlocker.unblockInputs(); 507 }, 508 pad2: function (number) { 509 return (number < 10 ? '0' : '') + number; 510 } 511 });
使用方法参考:datepicker控件
timefield:
1 Ext.define('ux.Timefield', { 2 extend: 'Ext.field.Text', 3 alternateClassName: 'timefield', 4 xtype: 'timefield', 5 requires: [ 6 'ux.TimePicker', 7 'Ext.DateExtras' 8 ], 9 10 /** 11 * @event change 12 * Fires when a date is selected 13 * @param {Ext.field.DatePicker} this 14 * @param {Date} newDate The new date 15 * @param {Date} oldDate The old date 16 */ 17 18 config: { 19 ui: 'select', 20 21 /** 22 * @cfg {Object/ux.TimePicker} picker 23 * An object that is used when creating the internal {@link ux.TimePicker} component or a direct instance of {@link ux.TimePicker}. 24 * @accessor 25 */ 26 picker: true, 27 28 /** 29 * @cfg {Boolean} 30 * @hide 31 * @accessor 32 */ 33 clearIcon: false, 34 35 /** 36 * @cfg {Object/Date} value 37 * Default value for the field and the internal {@link ux.TimePicker} component. Accepts an object of 'year', 38 * 'month' and 'day' values, all of which should be numbers, or a {@link Date}. 39 * 40 * Example: {year: 1989, day: 1, month: 5} = 1st May 1989 or new Date() 41 * @accessor 42 */ 43 44 45 /** 46 * @cfg {Boolean} destroyPickerOnHide 47 * 完成选择时隐藏或者销毁该控件,默认销毁 48 * @accessor 49 */ 50 destroyPickerOnHide: false, 51 52 /** 53 * @cfg {String} dateFormat 默认时间格式 54 * 接受任何有效的时间格式. 请参考 {@link Ext.Date}. 55 */ 56 dateFormat: 'Y-m-d H:i', 57 58 /** 59 * @cfg {Object} 60 * @hide 61 */ 62 component: { 63 useMask: true 64 } 65 }, 66 67 initialize: function () { 68 var me = this, 69 component = me.getComponent(); 70 71 me.callParent(); 72 73 component.on({ 74 scope: me, 75 masktap: 'onMaskTap' 76 }); 77 78 79 component.doMaskTap = Ext.emptyFn; 80 81 if (Ext.browser.is.AndroidStock2) { 82 component.input.dom.disabled = true; 83 } 84 }, 85 86 syncEmptyCls: Ext.emptyFn, 87 88 applyValue: function (value) { 89 if (!Ext.isDate(value) && !Ext.isObject(value)) { 90 return null; 91 } 92 93 if (Ext.isObject(value)) { 94 return new Date(value.year, value.month - 1, value.day, value.hour, value.minute); 95 } 96 97 return value; 98 }, 99 100 updateValue: function (newValue, oldValue) { 101 var me = this, 102 picker = me._picker; 103 104 if (picker && picker.isPicker) { 105 picker.setValue(newValue); 106 } 107 108 // Ext.Date.format expects a Date 109 if (newValue !== null) { 110 me.getComponent().setValue(Ext.Date.format(newValue, me.getDateFormat() || Ext.util.Format.defaultDateFormat)); 111 } else { 112 me.getComponent().setValue(''); 113 } 114 115 if (newValue !== oldValue) { 116 me.fireEvent('change', me, newValue, oldValue); 117 } 118 }, 119 120 /** 121 * Updates the date format in the field. 122 * @private 123 */ 124 updateDateFormat: function (newDateFormat, oldDateFormat) { 125 var value = this.getValue(); 126 if (newDateFormat != oldDateFormat && Ext.isDate(value)) { 127 this.getComponent().setValue(Ext.Date.format(value, newDateFormat || Ext.util.Format.defaultDateFormat)); 128 } 129 }, 130 131 /** 132 * Returns the {@link Date} value of this field. 133 * If you wanted a formated date 134 * @return {Date} The date selected 135 */ 136 getValue: function () { 137 if (this._picker && this._picker instanceof ux.TimePicker) { 138 return this._picker.getValue(); 139 } 140 return this._value; 141 }, 142 143 /** 144 * Returns the value of the field formatted using the specified format. If it is not specified, it will default to 145 * {@link #dateFormat} and then {@link Ext.util.Format#defaultDateFormat}. 146 * @param {String} format The format to be returned. 147 * @return {String} The formatted date. 148 */ 149 getFormattedValue: function (format) { 150 var value = this.getValue(); 151 return (Ext.isDate(value)) ? Ext.Date.format(value, format || this.getDateFormat() || Ext.util.Format.defaultDateFormat) : value; 152 }, 153 154 applyPicker: function (picker, pickerInstance) { 155 if (pickerInstance && pickerInstance.isPicker) { 156 picker = pickerInstance.setConfig(picker); 157 } 158 159 return picker; 160 }, 161 162 getPicker: function () { 163 var picker = this._picker, 164 value = this.getValue(); 165 166 if (picker && !picker.isPicker) { 167 picker = Ext.factory(picker, ux.TimePicker); 168 if (value != null) { 169 picker.setValue(value); 170 } 171 } 172 173 picker.on({ 174 scope: this, 175 change: 'onPickerChange', 176 hide: 'onPickerHide' 177 }); 178 179 this._picker = picker; 180 181 return picker; 182 }, 183 184 /** 185 * @private 186 * Listener to the tap event of the mask element. Shows the internal DatePicker component when the button has been tapped. 187 */ 188 onMaskTap: function () { 189 if (this.getDisabled()) { 190 return false; 191 } 192 193 this.onFocus(); 194 195 return false; 196 }, 197 198 /** 199 * Called when the picker changes its value. 200 * @param {ux.TimePicker} picker The date picker. 201 * @param {Object} value The new value from the date picker. 202 * @private 203 */ 204 onPickerChange: function (picker, value) { 205 var me = this, 206 oldValue = me.getValue(); 207 208 me.setValue(value); 209 me.fireEvent('select', me, value); 210 me.onChange(me, value, oldValue); 211 }, 212 213 /** 214 * Override this or change event will be fired twice. change event is fired in updateValue 215 * for this field. TOUCH-2861 216 */ 217 onChange: Ext.emptyFn, 218 219 /** 220 * Destroys the picker when it is hidden, if 221 * {@link Ext.field.DatePicker#destroyPickerOnHide destroyPickerOnHide} is set to `true`. 222 * @private 223 */ 224 onPickerHide: function () { 225 var me = this, 226 picker = me.getPicker(); 227 228 if (me.getDestroyPickerOnHide() && picker) { 229 picker.destroy(); 230 me._picker = me.getInitialConfig().picker || true; 231 } 232 }, 233 234 reset: function () { 235 this.setValue(this.originalValue); 236 }, 237 238 onFocus: function (e) { 239 var component = this.getComponent(); 240 this.fireEvent('focus', this, e); 241 242 if (Ext.os.is.Android4) { 243 component.input.dom.focus(); 244 } 245 component.input.dom.blur(); 246 247 if (this.getReadOnly()) { 248 return false; 249 } 250 251 this.isFocused = true; 252 253 this.getPicker().show(); 254 }, 255 256 // @private 257 destroy: function () { 258 var picker = this._picker; 259 260 if (picker && picker.isPicker) { 261 picker.destroy(); 262 } 263 264 this.callParent(arguments); 265 } 266 //<deprecated product=touch since=2.0> 267 }, function () { 268 this.override({ 269 getValue: function (format) { 270 if (format) { 271 //<debug warn> 272 Ext.Logger.deprecate("format argument of the getValue method is deprecated, please use getFormattedValue instead", this); 273 //</debug> 274 return this.getFormattedValue(format); 275 } 276 return this.callOverridden(); 277 } 278 }); 279 280 /** 281 * @method getDatePicker 282 * @inheritdoc Ext.field.DatePicker#getPicker 283 * @deprecated 2.0.0 Please use #getPicker instead 284 */ 285 Ext.deprecateMethod(this, 'getDatePicker', 'getPicker'); 286 //</deprecated> 287 });
使用方法参考datepickerfield控件
效果图:
只需要时、分选项如下:
1 xtype: 'timefield', 2 picker: { 3 slotOrder: ['hour', 'minute'] 4 }, 5 dateFormat: 'H:i', 6 value: new Date()
一些额外的css:
1 /*#region pick */ 2 3 .x-picker-slot-title { 4 height:auto; 5 text-align:center; 6 padding:0.2em 0; 7 } 8 .x-picker-slot .x-dataview-item { 9 padding:0 8px !important; 10 } 11 .x-webkit .x-layout-box.x-horizontal > .x-layout-box-item.x-picker-slot { 12 width:auto !important; 13 } 14 /*#endregion*/