1e2024ee0d21a38bc66ca12621b3ca86f5cc078fda6c8e805aeb43f3ce967146c01e7dc25678e3a90a7acfe8e0a463f2df78d483a7899e6b02eb431a19538c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <transition name="el-zoom-in-top" @after-enter="handleEnter" @after-leave="handleLeave">
  3. <div
  4. v-show="visible"
  5. class="el-picker-panel el-date-picker el-popper"
  6. :class="[{
  7. 'has-sidebar': $slots.sidebar || shortcuts,
  8. 'has-time': showTime
  9. }, popperClass]">
  10. <div class="el-picker-panel__body-wrapper">
  11. <slot name="sidebar" class="el-picker-panel__sidebar"></slot>
  12. <div class="el-picker-panel__sidebar" v-if="shortcuts">
  13. <button
  14. type="button"
  15. class="el-picker-panel__shortcut"
  16. v-for="(shortcut, key) in shortcuts"
  17. :key="key"
  18. @click="handleShortcutClick(shortcut)">{{ shortcut.text }}</button>
  19. </div>
  20. <div class="el-picker-panel__body">
  21. <div class="el-date-picker__time-header" v-if="showTime">
  22. <span class="el-date-picker__editor-wrap">
  23. <el-input
  24. :placeholder="t('el.datepicker.selectDate')"
  25. :value="visibleDate"
  26. size="small"
  27. @input="val => userInputDate = val"
  28. @change="handleVisibleDateChange" />
  29. </span>
  30. <span class="el-date-picker__editor-wrap" v-clickoutside="handleTimePickClose">
  31. <el-input
  32. ref="input"
  33. @focus="timePickerVisible = true"
  34. :placeholder="t('el.datepicker.selectTime')"
  35. :value="visibleTime"
  36. size="small"
  37. @input="val => userInputTime = val"
  38. @change="handleVisibleTimeChange" />
  39. <time-picker
  40. ref="timepicker"
  41. :time-arrow-control="arrowControl"
  42. @pick="handleTimePick"
  43. :visible="timePickerVisible"
  44. @mounted="proxyTimePickerDataProperties">
  45. </time-picker>
  46. </span>
  47. </div>
  48. <div
  49. class="el-date-picker__header"
  50. :class="{ 'el-date-picker__header--bordered': currentView === 'year' || currentView === 'month' }"
  51. v-show="currentView !== 'time'">
  52. <button
  53. type="button"
  54. @click="prevYear"
  55. :aria-label="t(`el.datepicker.prevYear`)"
  56. class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left">
  57. </button>
  58. <button
  59. type="button"
  60. @click="prevMonth"
  61. v-show="currentView === 'date'"
  62. :aria-label="t(`el.datepicker.prevMonth`)"
  63. class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-arrow-left">
  64. </button>
  65. <span
  66. @click="showYearPicker"
  67. role="button"
  68. class="el-date-picker__header-label">{{ yearLabel }}</span>
  69. <span
  70. @click="showMonthPicker"
  71. v-show="currentView === 'date'"
  72. role="button"
  73. class="el-date-picker__header-label"
  74. :class="{ active: currentView === 'month' }">{{t(`el.datepicker.month${ month + 1 }`)}}</span>
  75. <button
  76. type="button"
  77. @click="nextYear"
  78. :aria-label="t(`el.datepicker.nextYear`)"
  79. class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right">
  80. </button>
  81. <button
  82. type="button"
  83. @click="nextMonth"
  84. v-show="currentView === 'date'"
  85. :aria-label="t(`el.datepicker.nextMonth`)"
  86. class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-arrow-right">
  87. </button>
  88. </div>
  89. <div class="el-picker-panel__content">
  90. <date-table
  91. v-show="currentView === 'date'"
  92. @pick="handleDatePick"
  93. :selection-mode="selectionMode"
  94. :first-day-of-week="firstDayOfWeek"
  95. :value="value"
  96. :default-value="defaultValue ? new Date(defaultValue) : null"
  97. :date="date"
  98. :cell-class-name="cellClassName"
  99. :disabled-date="disabledDate">
  100. </date-table>
  101. <year-table
  102. v-show="currentView === 'year'"
  103. @pick="handleYearPick"
  104. :selection-mode="selectionMode"
  105. :value="value"
  106. :default-value="defaultValue ? new Date(defaultValue) : null"
  107. :date="date"
  108. :disabled-date="disabledDate">
  109. </year-table>
  110. <month-table
  111. v-show="currentView === 'month'"
  112. @pick="handleMonthPick"
  113. :selection-mode="selectionMode"
  114. :value="value"
  115. :default-value="defaultValue ? new Date(defaultValue) : null"
  116. :date="date"
  117. :disabled-date="disabledDate">
  118. </month-table>
  119. </div>
  120. </div>
  121. </div>
  122. <div
  123. class="el-picker-panel__footer"
  124. v-show="footerVisible && (currentView === 'date' || currentView === 'month' || currentView === 'year')">
  125. <el-button
  126. size="mini"
  127. type="text"
  128. class="el-picker-panel__link-btn"
  129. @click="changeToNow"
  130. v-show="selectionMode !== 'dates' && selectionMode !== 'months' && selectionMode !== 'years'">
  131. {{ t('el.datepicker.now') }}
  132. </el-button>
  133. <el-button
  134. plain
  135. size="mini"
  136. class="el-picker-panel__link-btn"
  137. @click="confirm">
  138. {{ t('el.datepicker.confirm') }}
  139. </el-button>
  140. </div>
  141. </div>
  142. </transition>
  143. </template>
  144. <script type="text/babel">
  145. import {
  146. formatDate,
  147. parseDate,
  148. getWeekNumber,
  149. isDate,
  150. modifyDate,
  151. modifyTime,
  152. modifyWithTimeString,
  153. clearMilliseconds,
  154. clearTime,
  155. prevYear,
  156. nextYear,
  157. prevMonth,
  158. nextMonth,
  159. changeYearMonthAndClampDate,
  160. extractDateFormat,
  161. extractTimeFormat,
  162. timeWithinRange
  163. } from 'element-ui/src/utils/date-util';
  164. import Clickoutside from 'element-ui/src/utils/clickoutside';
  165. import Locale from 'element-ui/src/mixins/locale';
  166. import ElInput from 'element-ui/packages/input';
  167. import ElButton from 'element-ui/packages/button';
  168. import TimePicker from './time';
  169. import YearTable from '../basic/year-table';
  170. import MonthTable from '../basic/month-table';
  171. import DateTable from '../basic/date-table';
  172. export default {
  173. mixins: [Locale],
  174. directives: { Clickoutside },
  175. watch: {
  176. showTime(val) {
  177. /* istanbul ignore if */
  178. if (!val) return;
  179. this.$nextTick(_ => {
  180. const inputElm = this.$refs.input.$el;
  181. if (inputElm) {
  182. this.pickerWidth = inputElm.getBoundingClientRect().width + 10;
  183. }
  184. });
  185. },
  186. value(val) {
  187. if (this.selectionMode === 'dates' && this.value) return;
  188. if (this.selectionMode === 'months' && this.value) return;
  189. if (this.selectionMode === 'years' && this.value) return;
  190. if (isDate(val)) {
  191. this.date = new Date(val);
  192. } else {
  193. this.date = this.getDefaultValue();
  194. }
  195. },
  196. defaultValue(val) {
  197. if (!isDate(this.value)) {
  198. this.date = val ? new Date(val) : new Date();
  199. }
  200. },
  201. timePickerVisible(val) {
  202. if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners());
  203. },
  204. selectionMode(newVal) {
  205. if (newVal === 'month') {
  206. /* istanbul ignore next */
  207. if (this.currentView !== 'year' || this.currentView !== 'month') {
  208. this.currentView = 'month';
  209. }
  210. } else if (newVal === 'dates') {
  211. this.currentView = 'date';
  212. } else if (newVal === 'years') {
  213. this.currentView = 'year';
  214. } else if (newVal === 'months') {
  215. this.currentView = 'month';
  216. }
  217. }
  218. },
  219. methods: {
  220. proxyTimePickerDataProperties() {
  221. const format = timeFormat => {this.$refs.timepicker.format = timeFormat;};
  222. const value = value => {this.$refs.timepicker.value = value;};
  223. const date = date => {this.$refs.timepicker.date = date;};
  224. const selectableRange = selectableRange => {this.$refs.timepicker.selectableRange = selectableRange;};
  225. this.$watch('value', value);
  226. this.$watch('date', date);
  227. this.$watch('selectableRange', selectableRange);
  228. format(this.timeFormat);
  229. value(this.value);
  230. date(this.date);
  231. selectableRange(this.selectableRange);
  232. },
  233. handleClear() {
  234. this.date = this.getDefaultValue();
  235. this.$emit('pick', null);
  236. },
  237. emit(value, ...args) {
  238. if (!value) {
  239. this.$emit('pick', value, ...args);
  240. } else if (Array.isArray(value)) {
  241. const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date));
  242. this.$emit('pick', dates, ...args);
  243. } else {
  244. this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args);
  245. }
  246. this.userInputDate = null;
  247. this.userInputTime = null;
  248. },
  249. // resetDate() {
  250. // this.date = new Date(this.date);
  251. // },
  252. showMonthPicker() {
  253. this.currentView = 'month';
  254. },
  255. showYearPicker() {
  256. this.currentView = 'year';
  257. },
  258. // XXX: 没用到
  259. // handleLabelClick() {
  260. // if (this.currentView === 'date') {
  261. // this.showMonthPicker();
  262. // } else if (this.currentView === 'month') {
  263. // this.showYearPicker();
  264. // }
  265. // },
  266. prevMonth() {
  267. this.date = prevMonth(this.date);
  268. },
  269. nextMonth() {
  270. this.date = nextMonth(this.date);
  271. },
  272. prevYear() {
  273. if (this.currentView === 'year') {
  274. this.date = prevYear(this.date, 10);
  275. } else {
  276. this.date = prevYear(this.date);
  277. }
  278. },
  279. nextYear() {
  280. if (this.currentView === 'year') {
  281. this.date = nextYear(this.date, 10);
  282. } else {
  283. this.date = nextYear(this.date);
  284. }
  285. },
  286. handleShortcutClick(shortcut) {
  287. if (shortcut.onClick) {
  288. shortcut.onClick(this);
  289. }
  290. },
  291. handleTimePick(value, visible, first) {
  292. if (isDate(value)) {
  293. const newDate = this.value
  294. ? modifyTime(this.value, value.getHours(), value.getMinutes(), value.getSeconds())
  295. : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
  296. this.date = newDate;
  297. this.emit(this.date, true);
  298. } else {
  299. this.emit(value, true);
  300. }
  301. if (!first) {
  302. this.timePickerVisible = visible;
  303. }
  304. },
  305. handleTimePickClose() {
  306. this.timePickerVisible = false;
  307. },
  308. handleMonthPick(month) {
  309. if (this.selectionMode === 'month') {
  310. this.date = modifyDate(this.date, this.year, month, 1);
  311. this.emit(this.date);
  312. } else if (this.selectionMode === 'months') {
  313. this.emit(month, true);
  314. } else {
  315. this.date = changeYearMonthAndClampDate(this.date, this.year, month);
  316. // TODO: should emit intermediate value ??
  317. // this.emit(this.date);
  318. this.currentView = 'date';
  319. }
  320. },
  321. handleDatePick(value) {
  322. if (this.selectionMode === 'day') {
  323. let newDate = this.value
  324. ? modifyDate(this.value, value.getFullYear(), value.getMonth(), value.getDate())
  325. : modifyWithTimeString(value, this.defaultTime);
  326. // change default time while out of selectableRange
  327. if (!this.checkDateWithinRange(newDate)) {
  328. newDate = modifyDate(this.selectableRange[0][0], value.getFullYear(), value.getMonth(), value.getDate());
  329. }
  330. this.date = newDate;
  331. this.emit(this.date, this.showTime);
  332. } else if (this.selectionMode === 'week') {
  333. this.emit(value.date);
  334. } else if (this.selectionMode === 'dates') {
  335. this.emit(value, true); // set false to keep panel open
  336. }
  337. },
  338. handleYearPick(year) {
  339. if (this.selectionMode === 'year') {
  340. this.date = modifyDate(this.date, year, 0, 1);
  341. this.emit(this.date);
  342. } else if (this.selectionMode === 'years') {
  343. this.emit(year, true);
  344. } else {
  345. this.date = changeYearMonthAndClampDate(this.date, year, this.month);
  346. // TODO: should emit intermediate value ??
  347. // this.emit(this.date, true);
  348. this.currentView = 'month';
  349. }
  350. },
  351. changeToNow() {
  352. // NOTE: not a permanent solution
  353. // consider disable "now" button in the future
  354. if ((!this.disabledDate || !this.disabledDate(new Date())) && this.checkDateWithinRange(new Date())) {
  355. this.date = new Date();
  356. this.emit(this.date);
  357. }
  358. },
  359. confirm() {
  360. if (this.selectionMode === 'dates' || this.selectionMode === 'months' || this.selectionMode === 'years') {
  361. this.emit(this.value);
  362. } else {
  363. // value were emitted in handle{Date,Time}Pick, nothing to update here
  364. // deal with the scenario where: user opens the picker, then confirm without doing anything
  365. const value = this.value
  366. ? this.value
  367. : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
  368. this.date = new Date(value); // refresh date
  369. this.emit(value);
  370. }
  371. },
  372. resetView() {
  373. if (this.selectionMode === 'month' || this.selectionMode === 'months') {
  374. this.currentView = 'month';
  375. } else if (this.selectionMode === 'year' || this.selectionMode === 'years') {
  376. this.currentView = 'year';
  377. } else {
  378. this.currentView = 'date';
  379. }
  380. },
  381. handleEnter() {
  382. document.body.addEventListener('keydown', this.handleKeydown);
  383. },
  384. handleLeave() {
  385. this.$emit('dodestroy');
  386. document.body.removeEventListener('keydown', this.handleKeydown);
  387. },
  388. handleKeydown(event) {
  389. const keyCode = event.keyCode;
  390. const list = [38, 40, 37, 39];
  391. if (this.visible && !this.timePickerVisible) {
  392. if (list.indexOf(keyCode) !== -1) {
  393. this.handleKeyControl(keyCode);
  394. event.stopPropagation();
  395. event.preventDefault();
  396. }
  397. if (keyCode === 13 && this.userInputDate === null && this.userInputTime === null) { // Enter
  398. this.emit(this.date, false);
  399. }
  400. }
  401. },
  402. handleKeyControl(keyCode) {
  403. const mapping = {
  404. 'year': {
  405. 38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setFullYear(date.getFullYear() + step)
  406. },
  407. 'month': {
  408. 38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setMonth(date.getMonth() + step)
  409. },
  410. 'week': {
  411. 38: -1, 40: 1, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step * 7)
  412. },
  413. 'day': {
  414. 38: -7, 40: 7, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step)
  415. }
  416. };
  417. const mode = this.selectionMode;
  418. const year = 3.1536e10;
  419. const now = this.date.getTime();
  420. const newDate = new Date(this.date.getTime());
  421. while (Math.abs(now - newDate.getTime()) <= year) {
  422. const map = mapping[mode];
  423. map.offset(newDate, map[keyCode]);
  424. if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) {
  425. continue;
  426. }
  427. this.date = newDate;
  428. this.$emit('pick', newDate, true);
  429. break;
  430. }
  431. },
  432. handleVisibleTimeChange(value) {
  433. const time = parseDate(value, this.timeFormat);
  434. if (time && this.checkDateWithinRange(time)) {
  435. this.date = modifyDate(time, this.year, this.month, this.monthDate);
  436. this.userInputTime = null;
  437. this.$refs.timepicker.value = this.date;
  438. this.timePickerVisible = false;
  439. this.emit(this.date, true);
  440. }
  441. },
  442. handleVisibleDateChange(value) {
  443. const date = parseDate(value, this.dateFormat);
  444. if (date) {
  445. if (typeof this.disabledDate === 'function' && this.disabledDate(date)) {
  446. return;
  447. }
  448. this.date = modifyTime(date, this.date.getHours(), this.date.getMinutes(), this.date.getSeconds());
  449. this.userInputDate = null;
  450. this.resetView();
  451. this.emit(this.date, true);
  452. }
  453. },
  454. isValidValue(value) {
  455. return value && !isNaN(value) && (
  456. typeof this.disabledDate === 'function'
  457. ? !this.disabledDate(value)
  458. : true
  459. ) && this.checkDateWithinRange(value);
  460. },
  461. getDefaultValue() {
  462. // if default-value is set, return it
  463. // otherwise, return now (the moment this method gets called)
  464. return this.defaultValue ? new Date(this.defaultValue) : new Date();
  465. },
  466. checkDateWithinRange(date) {
  467. return this.selectableRange.length > 0
  468. ? timeWithinRange(date, this.selectableRange, this.format || 'HH:mm:ss')
  469. : true;
  470. }
  471. },
  472. components: {
  473. TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton
  474. },
  475. data() {
  476. return {
  477. popperClass: '',
  478. date: new Date(),
  479. value: '',
  480. defaultValue: null, // use getDefaultValue() for time computation
  481. defaultTime: null,
  482. showTime: false,
  483. selectionMode: 'day',
  484. shortcuts: '',
  485. visible: false,
  486. currentView: 'date',
  487. disabledDate: '',
  488. cellClassName: '',
  489. selectableRange: [],
  490. firstDayOfWeek: 7,
  491. showWeekNumber: false,
  492. timePickerVisible: false,
  493. format: '',
  494. arrowControl: false,
  495. userInputDate: null,
  496. userInputTime: null
  497. };
  498. },
  499. computed: {
  500. year() {
  501. return this.date.getFullYear();
  502. },
  503. month() {
  504. return this.date.getMonth();
  505. },
  506. week() {
  507. return getWeekNumber(this.date);
  508. },
  509. monthDate() {
  510. return this.date.getDate();
  511. },
  512. footerVisible() {
  513. return this.showTime || this.selectionMode === 'dates' || this.selectionMode === 'months' || this.selectionMode === 'years';
  514. },
  515. visibleTime() {
  516. if (this.userInputTime !== null) {
  517. return this.userInputTime;
  518. } else {
  519. return formatDate(this.value || this.defaultValue, this.timeFormat);
  520. }
  521. },
  522. visibleDate() {
  523. if (this.userInputDate !== null) {
  524. return this.userInputDate;
  525. } else {
  526. return formatDate(this.value || this.defaultValue, this.dateFormat);
  527. }
  528. },
  529. yearLabel() {
  530. const yearTranslation = this.t('el.datepicker.year');
  531. if (this.currentView === 'year') {
  532. const startYear = Math.floor(this.year / 10) * 10;
  533. if (yearTranslation) {
  534. return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
  535. }
  536. return startYear + ' - ' + (startYear + 9);
  537. }
  538. return this.year + ' ' + yearTranslation;
  539. },
  540. timeFormat() {
  541. if (this.format) {
  542. return extractTimeFormat(this.format);
  543. } else {
  544. return 'HH:mm:ss';
  545. }
  546. },
  547. dateFormat() {
  548. if (this.format) {
  549. return extractDateFormat(this.format);
  550. } else {
  551. return 'yyyy-MM-dd';
  552. }
  553. }
  554. }
  555. };
  556. </script>