date-table.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. <template>
  2. <table
  3. cellspacing="0"
  4. cellpadding="0"
  5. class="el-date-table"
  6. @click="handleClick"
  7. @mousemove="handleMouseMove"
  8. :class="{ 'is-week-mode': selectionMode === 'week' }">
  9. <tbody>
  10. <tr>
  11. <th v-if="showWeekNumber">{{ t('el.datepicker.week') }}</th>
  12. <th v-for="(week, key) in WEEKS" :key="key">{{ t('el.datepicker.weeks.' + week) }}</th>
  13. </tr>
  14. <tr
  15. class="el-date-table__row"
  16. v-for="(row, key) in rows"
  17. :class="{ current: isWeekActive(row[1]) }"
  18. :key="key">
  19. <td
  20. v-for="(cell, key) in row"
  21. :class="getCellClasses(cell)"
  22. :key="key">
  23. <div>
  24. <span>
  25. {{ cell.text }}
  26. </span>
  27. </div>
  28. </td>
  29. </tr>
  30. </tbody>
  31. </table>
  32. </template>
  33. <script>
  34. import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, nextDate, isDate } from '../util';
  35. import { hasClass } from 'element-ui/src/utils/dom';
  36. import Locale from 'element-ui/src/mixins/locale';
  37. const WEEKS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
  38. const clearHours = function(time) {
  39. const cloneDate = new Date(time);
  40. cloneDate.setHours(0, 0, 0, 0);
  41. return cloneDate.getTime();
  42. };
  43. export default {
  44. mixins: [Locale],
  45. props: {
  46. firstDayOfWeek: {
  47. default: 7,
  48. type: Number,
  49. validator: val => val >= 1 && val <= 7
  50. },
  51. value: {},
  52. defaultValue: {
  53. validator(val) {
  54. // either: null, valid Date object, Array of valid Date objects
  55. return val === null || isDate(val) || (Array.isArray(val) && val.every(isDate));
  56. }
  57. },
  58. date: {},
  59. selectionMode: {
  60. default: 'day'
  61. },
  62. showWeekNumber: {
  63. type: Boolean,
  64. default: false
  65. },
  66. disabledDate: {},
  67. selectedDate: {
  68. type: Array
  69. },
  70. minDate: {},
  71. maxDate: {},
  72. rangeState: {
  73. default() {
  74. return {
  75. endDate: null,
  76. selecting: false,
  77. row: null,
  78. column: null
  79. };
  80. }
  81. }
  82. },
  83. computed: {
  84. offsetDay() {
  85. const week = this.firstDayOfWeek;
  86. // 周日为界限,左右偏移的天数,3217654 例如周一就是 -1,目的是调整前两行日期的位置
  87. return week > 3 ? 7 - week : -week;
  88. },
  89. WEEKS() {
  90. const week = this.firstDayOfWeek;
  91. return WEEKS.concat(WEEKS).slice(week, week + 7);
  92. },
  93. year() {
  94. return this.date.getFullYear();
  95. },
  96. month() {
  97. return this.date.getMonth();
  98. },
  99. startDate() {
  100. return getStartDateOfMonth(this.year, this.month);
  101. },
  102. rows() {
  103. // TODO: refactory rows / getCellClasses
  104. const date = new Date(this.year, this.month, 1);
  105. let day = getFirstDayOfMonth(date); // day of first day
  106. const dateCountOfMonth = getDayCountOfMonth(date.getFullYear(), date.getMonth());
  107. const dateCountOfLastMonth = getDayCountOfMonth(date.getFullYear(), (date.getMonth() === 0 ? 11 : date.getMonth() - 1));
  108. day = (day === 0 ? 7 : day);
  109. const offset = this.offsetDay;
  110. const rows = this.tableRows;
  111. let count = 1;
  112. let firstDayPosition;
  113. const startDate = this.startDate;
  114. const disabledDate = this.disabledDate;
  115. const selectedDate = this.selectedDate || this.value;
  116. const now = clearHours(new Date());
  117. for (let i = 0; i < 6; i++) {
  118. const row = rows[i];
  119. if (this.showWeekNumber) {
  120. if (!row[0]) {
  121. row[0] = { type: 'week', text: getWeekNumber(nextDate(startDate, i * 7 + 1)) };
  122. }
  123. }
  124. for (let j = 0; j < 7; j++) {
  125. let cell = row[this.showWeekNumber ? j + 1 : j];
  126. if (!cell) {
  127. cell = { row: i, column: j, type: 'normal', inRange: false, start: false, end: false };
  128. }
  129. cell.type = 'normal';
  130. const index = i * 7 + j;
  131. const time = nextDate(startDate, index - offset).getTime();
  132. cell.inRange = time >= clearHours(this.minDate) && time <= clearHours(this.maxDate);
  133. cell.start = this.minDate && time === clearHours(this.minDate);
  134. cell.end = this.maxDate && time === clearHours(this.maxDate);
  135. const isToday = time === now;
  136. if (isToday) {
  137. cell.type = 'today';
  138. }
  139. if (i >= 0 && i <= 1) {
  140. if (j + i * 7 >= (day + offset)) {
  141. cell.text = count++;
  142. if (count === 2) {
  143. firstDayPosition = i * 7 + j;
  144. }
  145. } else {
  146. cell.text = dateCountOfLastMonth - (day + offset - j % 7) + 1 + i * 7;
  147. cell.type = 'prev-month';
  148. }
  149. } else {
  150. if (count <= dateCountOfMonth) {
  151. cell.text = count++;
  152. if (count === 2) {
  153. firstDayPosition = i * 7 + j;
  154. }
  155. } else {
  156. cell.text = count++ - dateCountOfMonth;
  157. cell.type = 'next-month';
  158. }
  159. }
  160. let newDate = new Date(time);
  161. cell.disabled = typeof disabledDate === 'function' && disabledDate(newDate);
  162. cell.selected = Array.isArray(selectedDate) &&
  163. selectedDate.filter(date => date.toString() === newDate.toString())[0];
  164. this.$set(row, this.showWeekNumber ? j + 1 : j, cell);
  165. }
  166. if (this.selectionMode === 'week') {
  167. const start = this.showWeekNumber ? 1 : 0;
  168. const end = this.showWeekNumber ? 7 : 6;
  169. const isWeekActive = this.isWeekActive(row[start + 1]);
  170. row[start].inRange = isWeekActive;
  171. row[start].start = isWeekActive;
  172. row[end].inRange = isWeekActive;
  173. row[end].end = isWeekActive;
  174. }
  175. }
  176. rows.firstDayPosition = firstDayPosition;
  177. return rows;
  178. }
  179. },
  180. watch: {
  181. 'rangeState.endDate'(newVal) {
  182. this.markRange(newVal);
  183. },
  184. minDate(newVal, oldVal) {
  185. if (newVal && !oldVal) {
  186. this.rangeState.selecting = true;
  187. this.markRange(newVal);
  188. } else if (!newVal) {
  189. this.rangeState.selecting = false;
  190. this.markRange(newVal);
  191. } else {
  192. this.markRange();
  193. }
  194. },
  195. maxDate(newVal, oldVal) {
  196. if (newVal && !oldVal) {
  197. this.rangeState.selecting = false;
  198. this.markRange(newVal);
  199. }
  200. }
  201. },
  202. data() {
  203. return {
  204. tableRows: [ [], [], [], [], [], [] ]
  205. };
  206. },
  207. methods: {
  208. cellMatchesDate(cell, date) {
  209. const value = new Date(date);
  210. return this.year === value.getFullYear() &&
  211. this.month === value.getMonth() &&
  212. Number(cell.text) === value.getDate();
  213. },
  214. getCellClasses(cell) {
  215. const selectionMode = this.selectionMode;
  216. const defaultValue = this.defaultValue ? Array.isArray(this.defaultValue) ? this.defaultValue : [this.defaultValue] : [];
  217. let classes = [];
  218. if ((cell.type === 'normal' || cell.type === 'today') && !cell.disabled) {
  219. classes.push('available');
  220. if (cell.type === 'today') {
  221. classes.push('today');
  222. }
  223. } else {
  224. classes.push(cell.type);
  225. }
  226. if (cell.type === 'normal' && defaultValue.some(date => this.cellMatchesDate(cell, date))) {
  227. classes.push('default');
  228. }
  229. if (selectionMode === 'day' && (cell.type === 'normal' || cell.type === 'today') && this.cellMatchesDate(cell, this.value)) {
  230. classes.push('current');
  231. }
  232. if (cell.inRange && ((cell.type === 'normal' || cell.type === 'today') || this.selectionMode === 'week')) {
  233. classes.push('in-range');
  234. if (cell.start) {
  235. classes.push('start-date');
  236. }
  237. if (cell.end) {
  238. classes.push('end-date');
  239. }
  240. }
  241. if (cell.disabled) {
  242. classes.push('disabled');
  243. }
  244. if (cell.selected) {
  245. classes.push('selected');
  246. }
  247. return classes.join(' ');
  248. },
  249. getDateOfCell(row, column) {
  250. const offsetFromStart = row * 7 + (column - (this.showWeekNumber ? 1 : 0)) - this.offsetDay;
  251. return nextDate(this.startDate, offsetFromStart);
  252. },
  253. isWeekActive(cell) {
  254. if (this.selectionMode !== 'week') return false;
  255. const newDate = new Date(this.year, this.month, 1);
  256. const year = newDate.getFullYear();
  257. const month = newDate.getMonth();
  258. if (cell.type === 'prev-month') {
  259. newDate.setMonth(month === 0 ? 11 : month - 1);
  260. newDate.setFullYear(month === 0 ? year - 1 : year);
  261. }
  262. if (cell.type === 'next-month') {
  263. newDate.setMonth(month === 11 ? 0 : month + 1);
  264. newDate.setFullYear(month === 11 ? year + 1 : year);
  265. }
  266. newDate.setDate(parseInt(cell.text, 10));
  267. const valueYear = isDate(this.value) ? this.value.getFullYear() : null;
  268. return year === valueYear && getWeekNumber(newDate) === getWeekNumber(this.value);
  269. },
  270. markRange(maxDate) {
  271. const startDate = this.startDate;
  272. if (!maxDate) {
  273. maxDate = this.maxDate;
  274. }
  275. const rows = this.rows;
  276. const minDate = this.minDate;
  277. for (let i = 0, k = rows.length; i < k; i++) {
  278. const row = rows[i];
  279. for (let j = 0, l = row.length; j < l; j++) {
  280. if (this.showWeekNumber && j === 0) continue;
  281. const cell = row[j];
  282. const index = i * 7 + j + (this.showWeekNumber ? -1 : 0);
  283. const time = nextDate(startDate, index - this.offsetDay).getTime();
  284. if (maxDate && maxDate < minDate) {
  285. cell.inRange = minDate && time >= clearHours(maxDate) && time <= clearHours(minDate);
  286. cell.start = maxDate && time === clearHours(maxDate.getTime());
  287. cell.end = minDate && time === clearHours(minDate.getTime());
  288. } else {
  289. cell.inRange = minDate && time >= clearHours(minDate) && time <= clearHours(maxDate);
  290. cell.start = minDate && time === clearHours(minDate.getTime());
  291. cell.end = maxDate && time === clearHours(maxDate.getTime());
  292. }
  293. }
  294. }
  295. },
  296. handleMouseMove(event) {
  297. if (!this.rangeState.selecting) return;
  298. this.$emit('changerange', {
  299. minDate: this.minDate,
  300. maxDate: this.maxDate,
  301. rangeState: this.rangeState
  302. });
  303. let target = event.target;
  304. if (target.tagName === 'SPAN') {
  305. target = target.parentNode.parentNode;
  306. }
  307. if (target.tagName === 'DIV') {
  308. target = target.parentNode;
  309. }
  310. if (target.tagName !== 'TD') return;
  311. const column = target.cellIndex;
  312. const row = target.parentNode.rowIndex - 1;
  313. const { row: oldRow, column: oldColumn } = this.rangeState;
  314. if (oldRow !== row || oldColumn !== column) {
  315. this.rangeState.row = row;
  316. this.rangeState.column = column;
  317. this.rangeState.endDate = this.getDateOfCell(row, column);
  318. }
  319. },
  320. handleClick(event) {
  321. let target = event.target;
  322. if (target.tagName === 'SPAN') {
  323. target = target.parentNode.parentNode;
  324. }
  325. if (target.tagName === 'DIV') {
  326. target = target.parentNode;
  327. }
  328. if (target.tagName !== 'TD') return;
  329. if (hasClass(target, 'disabled') || hasClass(target, 'week')) return;
  330. const selectionMode = this.selectionMode;
  331. if (selectionMode === 'week') {
  332. target = target.parentNode.cells[1];
  333. }
  334. let year = Number(this.year);
  335. let month = Number(this.month);
  336. const cellIndex = target.cellIndex;
  337. const rowIndex = target.parentNode.rowIndex;
  338. const cell = this.rows[rowIndex - 1][cellIndex];
  339. const text = cell.text;
  340. const className = target.className;
  341. const newDate = new Date(year, month, 1);
  342. if (className.indexOf('prev') !== -1) {
  343. if (month === 0) {
  344. year = year - 1;
  345. month = 11;
  346. } else {
  347. month = month - 1;
  348. }
  349. newDate.setFullYear(year);
  350. newDate.setMonth(month);
  351. } else if (className.indexOf('next') !== -1) {
  352. if (month === 11) {
  353. year = year + 1;
  354. month = 0;
  355. } else {
  356. month = month + 1;
  357. }
  358. newDate.setFullYear(year);
  359. newDate.setMonth(month);
  360. }
  361. newDate.setDate(parseInt(text, 10));
  362. if (this.selectionMode === 'range') {
  363. if (this.minDate && this.maxDate) {
  364. const minDate = new Date(newDate.getTime());
  365. const maxDate = null;
  366. this.$emit('pick', { minDate, maxDate }, false);
  367. this.rangeState.selecting = true;
  368. this.markRange(this.minDate);
  369. this.$nextTick(() => {
  370. this.handleMouseMove(event);
  371. });
  372. } else if (this.minDate && !this.maxDate) {
  373. if (newDate >= this.minDate) {
  374. const maxDate = new Date(newDate.getTime());
  375. this.rangeState.selecting = false;
  376. this.$emit('pick', {
  377. minDate: this.minDate,
  378. maxDate
  379. });
  380. } else {
  381. const minDate = new Date(newDate.getTime());
  382. this.rangeState.selecting = false;
  383. this.$emit('pick', { minDate, maxDate: this.minDate });
  384. }
  385. } else if (!this.minDate) {
  386. const minDate = new Date(newDate.getTime());
  387. this.$emit('pick', { minDate, maxDate: this.maxDate }, false);
  388. this.rangeState.selecting = true;
  389. this.markRange(this.minDate);
  390. }
  391. } else if (selectionMode === 'day') {
  392. this.$emit('pick', newDate);
  393. } else if (selectionMode === 'week') {
  394. const weekNumber = getWeekNumber(newDate);
  395. const value = newDate.getFullYear() + 'w' + weekNumber;
  396. this.$emit('pick', {
  397. year: newDate.getFullYear(),
  398. week: weekNumber,
  399. value: value,
  400. date: newDate
  401. });
  402. } else if (selectionMode === 'dates') {
  403. let selectedDate = this.selectedDate;
  404. if (!cell.selected) {
  405. selectedDate.push(newDate);
  406. } else {
  407. selectedDate.forEach((date, index) => {
  408. if (date.toString() === newDate.toString()) {
  409. selectedDate.splice(index, 1);
  410. }
  411. });
  412. }
  413. this.$emit('select', selectedDate);
  414. }
  415. }
  416. }
  417. };
  418. </script>