index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. // Utils
  2. import { raf } from '../utils/dom/raf';
  3. import { isDate } from '../utils/validate/date';
  4. import { getScrollTop } from '../utils/dom/scroll';
  5. import { t, bem, copyDate, copyDates, getNextDay, compareDay, calcDateNum, compareMonth, createComponent, getDayByOffset } from './utils'; // Components
  6. import Popup from '../popup';
  7. import Button from '../button';
  8. import Toast from '../toast';
  9. import Month from './components/Month';
  10. import Header from './components/Header';
  11. export default createComponent({
  12. props: {
  13. title: String,
  14. color: String,
  15. value: Boolean,
  16. readonly: Boolean,
  17. formatter: Function,
  18. rowHeight: [Number, String],
  19. confirmText: String,
  20. rangePrompt: String,
  21. defaultDate: [Date, Array],
  22. getContainer: [String, Function],
  23. allowSameDay: Boolean,
  24. confirmDisabledText: String,
  25. type: {
  26. type: String,
  27. default: 'single'
  28. },
  29. round: {
  30. type: Boolean,
  31. default: true
  32. },
  33. position: {
  34. type: String,
  35. default: 'bottom'
  36. },
  37. poppable: {
  38. type: Boolean,
  39. default: true
  40. },
  41. maxRange: {
  42. type: [Number, String],
  43. default: null
  44. },
  45. lazyRender: {
  46. type: Boolean,
  47. default: true
  48. },
  49. showMark: {
  50. type: Boolean,
  51. default: true
  52. },
  53. showTitle: {
  54. type: Boolean,
  55. default: true
  56. },
  57. showConfirm: {
  58. type: Boolean,
  59. default: true
  60. },
  61. showSubtitle: {
  62. type: Boolean,
  63. default: true
  64. },
  65. closeOnPopstate: {
  66. type: Boolean,
  67. default: true
  68. },
  69. closeOnClickOverlay: {
  70. type: Boolean,
  71. default: true
  72. },
  73. safeAreaInsetBottom: {
  74. type: Boolean,
  75. default: true
  76. },
  77. minDate: {
  78. type: Date,
  79. validator: isDate,
  80. default: function _default() {
  81. return new Date();
  82. }
  83. },
  84. maxDate: {
  85. type: Date,
  86. validator: isDate,
  87. default: function _default() {
  88. var now = new Date();
  89. return new Date(now.getFullYear(), now.getMonth() + 6, now.getDate());
  90. }
  91. },
  92. firstDayOfWeek: {
  93. type: [Number, String],
  94. default: 0,
  95. validator: function validator(val) {
  96. return val >= 0 && val <= 6;
  97. }
  98. }
  99. },
  100. inject: {
  101. vanPopup: {
  102. default: null
  103. }
  104. },
  105. data: function data() {
  106. return {
  107. subtitle: '',
  108. currentDate: this.getInitialDate()
  109. };
  110. },
  111. computed: {
  112. months: function months() {
  113. var months = [];
  114. var cursor = new Date(this.minDate);
  115. cursor.setDate(1);
  116. do {
  117. months.push(new Date(cursor));
  118. cursor.setMonth(cursor.getMonth() + 1);
  119. } while (compareMonth(cursor, this.maxDate) !== 1);
  120. return months;
  121. },
  122. buttonDisabled: function buttonDisabled() {
  123. var type = this.type,
  124. currentDate = this.currentDate;
  125. if (currentDate) {
  126. if (type === 'range') {
  127. return !currentDate[0] || !currentDate[1];
  128. }
  129. if (type === 'multiple') {
  130. return !currentDate.length;
  131. }
  132. }
  133. return !currentDate;
  134. },
  135. dayOffset: function dayOffset() {
  136. return this.firstDayOfWeek ? this.firstDayOfWeek % 7 : 0;
  137. }
  138. },
  139. watch: {
  140. value: 'init',
  141. type: function type() {
  142. this.reset();
  143. },
  144. defaultDate: function defaultDate(val) {
  145. this.currentDate = val;
  146. this.scrollIntoView();
  147. }
  148. },
  149. mounted: function mounted() {
  150. this.init(); // https://github.com/vant-ui/vant/issues/9845
  151. if (!this.poppable) {
  152. var _this$vanPopup;
  153. (_this$vanPopup = this.vanPopup) == null ? void 0 : _this$vanPopup.$on('opened', this.onScroll);
  154. }
  155. },
  156. /* istanbul ignore next */
  157. activated: function activated() {
  158. this.init();
  159. },
  160. methods: {
  161. // @exposed-api
  162. reset: function reset(date) {
  163. if (date === void 0) {
  164. date = this.getInitialDate();
  165. }
  166. this.currentDate = date;
  167. this.scrollIntoView();
  168. },
  169. init: function init() {
  170. var _this = this;
  171. if (this.poppable && !this.value) {
  172. return;
  173. }
  174. this.$nextTick(function () {
  175. // add Math.floor to avoid decimal height issues
  176. // https://github.com/vant-ui/vant/issues/5640
  177. _this.bodyHeight = Math.floor(_this.$refs.body.getBoundingClientRect().height);
  178. _this.onScroll();
  179. _this.scrollIntoView();
  180. });
  181. },
  182. // @exposed-api
  183. scrollToDate: function scrollToDate(targetDate) {
  184. var _this2 = this;
  185. raf(function () {
  186. var displayed = _this2.value || !_this2.poppable;
  187. /* istanbul ignore if */
  188. if (!targetDate || !displayed) {
  189. return;
  190. }
  191. _this2.months.some(function (month, index) {
  192. if (compareMonth(month, targetDate) === 0) {
  193. var _this2$$refs = _this2.$refs,
  194. body = _this2$$refs.body,
  195. months = _this2$$refs.months;
  196. months[index].scrollIntoView(body);
  197. return true;
  198. }
  199. return false;
  200. });
  201. _this2.onScroll();
  202. });
  203. },
  204. // scroll to current month
  205. scrollIntoView: function scrollIntoView() {
  206. var currentDate = this.currentDate;
  207. if (currentDate) {
  208. var targetDate = this.type === 'single' ? currentDate : currentDate[0];
  209. this.scrollToDate(targetDate);
  210. }
  211. },
  212. getInitialDate: function getInitialDate() {
  213. var type = this.type,
  214. minDate = this.minDate,
  215. maxDate = this.maxDate,
  216. defaultDate = this.defaultDate;
  217. if (defaultDate === null) {
  218. return defaultDate;
  219. }
  220. var defaultVal = new Date();
  221. if (compareDay(defaultVal, minDate) === -1) {
  222. defaultVal = minDate;
  223. } else if (compareDay(defaultVal, maxDate) === 1) {
  224. defaultVal = maxDate;
  225. }
  226. if (type === 'range') {
  227. var _ref = defaultDate || [],
  228. startDay = _ref[0],
  229. endDay = _ref[1];
  230. return [startDay || defaultVal, endDay || getNextDay(defaultVal)];
  231. }
  232. if (type === 'multiple') {
  233. return defaultDate || [defaultVal];
  234. }
  235. return defaultDate || defaultVal;
  236. },
  237. // calculate the position of the elements
  238. // and find the elements that needs to be rendered
  239. onScroll: function onScroll() {
  240. var _this$$refs = this.$refs,
  241. body = _this$$refs.body,
  242. months = _this$$refs.months;
  243. var top = getScrollTop(body);
  244. var bottom = top + this.bodyHeight;
  245. var heights = months.map(function (item) {
  246. return item.getHeight();
  247. });
  248. var heightSum = heights.reduce(function (a, b) {
  249. return a + b;
  250. }, 0); // iOS scroll bounce may exceed the range
  251. if (bottom > heightSum && top > 0) {
  252. return;
  253. }
  254. var height = 0;
  255. var currentMonth;
  256. var visibleRange = [-1, -1];
  257. for (var i = 0; i < months.length; i++) {
  258. var visible = height <= bottom && height + heights[i] >= top;
  259. if (visible) {
  260. visibleRange[1] = i;
  261. if (!currentMonth) {
  262. currentMonth = months[i];
  263. visibleRange[0] = i;
  264. }
  265. if (!months[i].showed) {
  266. months[i].showed = true;
  267. this.$emit('month-show', {
  268. date: months[i].date,
  269. title: months[i].title
  270. });
  271. }
  272. }
  273. height += heights[i];
  274. }
  275. months.forEach(function (month, index) {
  276. month.visible = index >= visibleRange[0] - 1 && index <= visibleRange[1] + 1;
  277. });
  278. /* istanbul ignore else */
  279. if (currentMonth) {
  280. this.subtitle = currentMonth.title;
  281. }
  282. },
  283. onClickDay: function onClickDay(item) {
  284. if (this.readonly) {
  285. return;
  286. }
  287. var date = item.date;
  288. var type = this.type,
  289. currentDate = this.currentDate;
  290. if (type === 'range') {
  291. if (!currentDate) {
  292. this.select([date, null]);
  293. return;
  294. }
  295. var startDay = currentDate[0],
  296. endDay = currentDate[1];
  297. if (startDay && !endDay) {
  298. var compareToStart = compareDay(date, startDay);
  299. if (compareToStart === 1) {
  300. this.select([startDay, date], true);
  301. } else if (compareToStart === -1) {
  302. this.select([date, null]);
  303. } else if (this.allowSameDay) {
  304. this.select([date, date], true);
  305. }
  306. } else {
  307. this.select([date, null]);
  308. }
  309. } else if (type === 'multiple') {
  310. if (!currentDate) {
  311. this.select([date]);
  312. return;
  313. }
  314. var selectedIndex;
  315. var selected = this.currentDate.some(function (dateItem, index) {
  316. var equal = compareDay(dateItem, date) === 0;
  317. if (equal) {
  318. selectedIndex = index;
  319. }
  320. return equal;
  321. });
  322. if (selected) {
  323. var _currentDate$splice = currentDate.splice(selectedIndex, 1),
  324. unselectedDate = _currentDate$splice[0];
  325. this.$emit('unselect', copyDate(unselectedDate));
  326. } else if (this.maxRange && currentDate.length >= this.maxRange) {
  327. Toast(this.rangePrompt || t('rangePrompt', this.maxRange));
  328. } else {
  329. this.select([].concat(currentDate, [date]));
  330. }
  331. } else {
  332. this.select(date, true);
  333. }
  334. },
  335. togglePopup: function togglePopup(val) {
  336. this.$emit('input', val);
  337. },
  338. select: function select(date, complete) {
  339. var _this3 = this;
  340. var emit = function emit(date) {
  341. _this3.currentDate = date;
  342. _this3.$emit('select', copyDates(_this3.currentDate));
  343. };
  344. if (complete && this.type === 'range') {
  345. var valid = this.checkRange(date);
  346. if (!valid) {
  347. // auto selected to max range if showConfirm
  348. if (this.showConfirm) {
  349. emit([date[0], getDayByOffset(date[0], this.maxRange - 1)]);
  350. } else {
  351. emit(date);
  352. }
  353. return;
  354. }
  355. }
  356. emit(date);
  357. if (complete && !this.showConfirm) {
  358. this.onConfirm();
  359. }
  360. },
  361. checkRange: function checkRange(date) {
  362. var maxRange = this.maxRange,
  363. rangePrompt = this.rangePrompt;
  364. if (maxRange && calcDateNum(date) > maxRange) {
  365. Toast(rangePrompt || t('rangePrompt', maxRange));
  366. return false;
  367. }
  368. return true;
  369. },
  370. onConfirm: function onConfirm() {
  371. this.$emit('confirm', copyDates(this.currentDate));
  372. },
  373. genMonth: function genMonth(date, index) {
  374. var h = this.$createElement;
  375. var showMonthTitle = index !== 0 || !this.showSubtitle;
  376. return h(Month, {
  377. "ref": "months",
  378. "refInFor": true,
  379. "attrs": {
  380. "date": date,
  381. "type": this.type,
  382. "color": this.color,
  383. "minDate": this.minDate,
  384. "maxDate": this.maxDate,
  385. "showMark": this.showMark,
  386. "formatter": this.formatter,
  387. "rowHeight": this.rowHeight,
  388. "lazyRender": this.lazyRender,
  389. "currentDate": this.currentDate,
  390. "showSubtitle": this.showSubtitle,
  391. "allowSameDay": this.allowSameDay,
  392. "showMonthTitle": showMonthTitle,
  393. "firstDayOfWeek": this.dayOffset
  394. },
  395. "scopedSlots": {
  396. 'top-info': this.$scopedSlots['top-info'],
  397. 'bottom-info': this.$scopedSlots['bottom-info']
  398. },
  399. "on": {
  400. "click": this.onClickDay
  401. }
  402. });
  403. },
  404. genFooterContent: function genFooterContent() {
  405. var h = this.$createElement;
  406. var slot = this.slots('footer');
  407. if (slot) {
  408. return slot;
  409. }
  410. if (this.showConfirm) {
  411. var text = this.buttonDisabled ? this.confirmDisabledText : this.confirmText;
  412. return h(Button, {
  413. "attrs": {
  414. "round": true,
  415. "block": true,
  416. "type": "danger",
  417. "color": this.color,
  418. "disabled": this.buttonDisabled,
  419. "nativeType": "button"
  420. },
  421. "class": bem('confirm'),
  422. "on": {
  423. "click": this.onConfirm
  424. }
  425. }, [text || t('confirm')]);
  426. }
  427. },
  428. genFooter: function genFooter() {
  429. var h = this.$createElement;
  430. return h("div", {
  431. "class": bem('footer', {
  432. unfit: !this.safeAreaInsetBottom
  433. })
  434. }, [this.genFooterContent()]);
  435. },
  436. genCalendar: function genCalendar() {
  437. var _this4 = this;
  438. var h = this.$createElement;
  439. return h("div", {
  440. "class": bem()
  441. }, [h(Header, {
  442. "attrs": {
  443. "title": this.title,
  444. "showTitle": this.showTitle,
  445. "subtitle": this.subtitle,
  446. "showSubtitle": this.showSubtitle,
  447. "firstDayOfWeek": this.dayOffset
  448. },
  449. "scopedSlots": {
  450. title: function title() {
  451. return _this4.slots('title');
  452. }
  453. }
  454. }), h("div", {
  455. "ref": "body",
  456. "class": bem('body'),
  457. "on": {
  458. "scroll": this.onScroll
  459. }
  460. }, [this.months.map(this.genMonth)]), this.genFooter()]);
  461. }
  462. },
  463. render: function render() {
  464. var _this5 = this;
  465. var h = arguments[0];
  466. if (this.poppable) {
  467. var _attrs;
  468. var createListener = function createListener(name) {
  469. return function () {
  470. return _this5.$emit(name);
  471. };
  472. };
  473. return h(Popup, {
  474. "attrs": (_attrs = {
  475. "round": true,
  476. "value": this.value
  477. }, _attrs["round"] = this.round, _attrs["position"] = this.position, _attrs["closeable"] = this.showTitle || this.showSubtitle, _attrs["getContainer"] = this.getContainer, _attrs["closeOnPopstate"] = this.closeOnPopstate, _attrs["closeOnClickOverlay"] = this.closeOnClickOverlay, _attrs),
  478. "class": bem('popup'),
  479. "on": {
  480. "input": this.togglePopup,
  481. "open": createListener('open'),
  482. "opened": createListener('opened'),
  483. "close": createListener('close'),
  484. "closed": createListener('closed')
  485. }
  486. }, [this.genCalendar()]);
  487. }
  488. return this.genCalendar();
  489. }
  490. });