index.js 14 KB

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