index.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import { VantComponent } from '../common/component';
  2. import { ROW_HEIGHT, getPrevDay, getNextDay, getToday, compareDay, copyDates, calcDateNum, formatMonthTitle, compareMonth, getMonths, getDayByOffset, } from './utils';
  3. import Toast from '../toast/toast';
  4. import { requestAnimationFrame } from '../common/utils';
  5. const initialMinDate = getToday().getTime();
  6. const initialMaxDate = (() => {
  7. const now = getToday();
  8. return new Date(now.getFullYear(), now.getMonth() + 6, now.getDate()).getTime();
  9. })();
  10. VantComponent({
  11. props: {
  12. title: {
  13. type: String,
  14. value: '日期选择',
  15. },
  16. color: String,
  17. show: {
  18. type: Boolean,
  19. observer(val) {
  20. if (val) {
  21. this.initRect();
  22. this.scrollIntoView();
  23. }
  24. },
  25. },
  26. formatter: null,
  27. confirmText: {
  28. type: String,
  29. value: '确定',
  30. },
  31. confirmDisabledText: {
  32. type: String,
  33. value: '确定',
  34. },
  35. rangePrompt: String,
  36. showRangePrompt: {
  37. type: Boolean,
  38. value: true,
  39. },
  40. defaultDate: {
  41. type: null,
  42. observer(val) {
  43. this.setData({ currentDate: val });
  44. this.scrollIntoView();
  45. },
  46. },
  47. allowSameDay: Boolean,
  48. type: {
  49. type: String,
  50. value: 'single',
  51. observer: 'reset',
  52. },
  53. minDate: {
  54. type: Number,
  55. value: initialMinDate,
  56. },
  57. maxDate: {
  58. type: Number,
  59. value: initialMaxDate,
  60. },
  61. position: {
  62. type: String,
  63. value: 'bottom',
  64. },
  65. rowHeight: {
  66. type: null,
  67. value: ROW_HEIGHT,
  68. },
  69. round: {
  70. type: Boolean,
  71. value: true,
  72. },
  73. poppable: {
  74. type: Boolean,
  75. value: true,
  76. },
  77. showMark: {
  78. type: Boolean,
  79. value: true,
  80. },
  81. showTitle: {
  82. type: Boolean,
  83. value: true,
  84. },
  85. showConfirm: {
  86. type: Boolean,
  87. value: true,
  88. },
  89. showSubtitle: {
  90. type: Boolean,
  91. value: true,
  92. },
  93. safeAreaInsetBottom: {
  94. type: Boolean,
  95. value: true,
  96. },
  97. closeOnClickOverlay: {
  98. type: Boolean,
  99. value: true,
  100. },
  101. maxRange: {
  102. type: null,
  103. value: null,
  104. },
  105. firstDayOfWeek: {
  106. type: Number,
  107. value: 0,
  108. },
  109. },
  110. data: {
  111. subtitle: '',
  112. currentDate: null,
  113. scrollIntoView: '',
  114. },
  115. created() {
  116. this.setData({
  117. currentDate: this.getInitialDate(this.data.defaultDate),
  118. });
  119. },
  120. mounted() {
  121. if (this.data.show || !this.data.poppable) {
  122. this.initRect();
  123. this.scrollIntoView();
  124. }
  125. },
  126. methods: {
  127. reset() {
  128. this.setData({ currentDate: this.getInitialDate() });
  129. this.scrollIntoView();
  130. },
  131. initRect() {
  132. if (this.contentObserver != null) {
  133. this.contentObserver.disconnect();
  134. }
  135. const contentObserver = this.createIntersectionObserver({
  136. thresholds: [0, 0.1, 0.9, 1],
  137. observeAll: true,
  138. });
  139. this.contentObserver = contentObserver;
  140. contentObserver.relativeTo('.van-calendar__body');
  141. contentObserver.observe('.month', (res) => {
  142. if (res.boundingClientRect.top <= res.relativeRect.top) {
  143. // @ts-ignore
  144. this.setData({ subtitle: formatMonthTitle(res.dataset.date) });
  145. }
  146. });
  147. },
  148. limitDateRange(date, minDate = null, maxDate = null) {
  149. minDate = minDate || this.data.minDate;
  150. maxDate = maxDate || this.data.maxDate;
  151. if (compareDay(date, minDate) === -1) {
  152. return minDate;
  153. }
  154. if (compareDay(date, maxDate) === 1) {
  155. return maxDate;
  156. }
  157. return date;
  158. },
  159. getInitialDate(defaultDate = null) {
  160. const { type, minDate, maxDate } = this.data;
  161. const now = getToday().getTime();
  162. if (type === 'range') {
  163. if (!Array.isArray(defaultDate)) {
  164. defaultDate = [];
  165. }
  166. const [startDay, endDay] = defaultDate || [];
  167. const start = this.limitDateRange(startDay || now, minDate, getPrevDay(new Date(maxDate)).getTime());
  168. const end = this.limitDateRange(endDay || now, getNextDay(new Date(minDate)).getTime());
  169. return [start, end];
  170. }
  171. if (type === 'multiple') {
  172. if (Array.isArray(defaultDate)) {
  173. return defaultDate.map((date) => this.limitDateRange(date));
  174. }
  175. return [this.limitDateRange(now)];
  176. }
  177. if (!defaultDate || Array.isArray(defaultDate)) {
  178. defaultDate = now;
  179. }
  180. return this.limitDateRange(defaultDate);
  181. },
  182. scrollIntoView() {
  183. requestAnimationFrame(() => {
  184. const { currentDate, type, show, poppable, minDate, maxDate, } = this.data;
  185. // @ts-ignore
  186. const targetDate = type === 'single' ? currentDate : currentDate[0];
  187. const displayed = show || !poppable;
  188. if (!targetDate || !displayed) {
  189. return;
  190. }
  191. const months = getMonths(minDate, maxDate);
  192. months.some((month, index) => {
  193. if (compareMonth(month, targetDate) === 0) {
  194. this.setData({ scrollIntoView: `month${index}` });
  195. return true;
  196. }
  197. return false;
  198. });
  199. });
  200. },
  201. onOpen() {
  202. this.$emit('open');
  203. },
  204. onOpened() {
  205. this.$emit('opened');
  206. },
  207. onClose() {
  208. this.$emit('close');
  209. },
  210. onClosed() {
  211. this.$emit('closed');
  212. },
  213. onClickDay(event) {
  214. const { date } = event.detail;
  215. const { type, currentDate, allowSameDay } = this.data;
  216. if (type === 'range') {
  217. // @ts-ignore
  218. const [startDay, endDay] = currentDate;
  219. if (startDay && !endDay) {
  220. const compareToStart = compareDay(date, startDay);
  221. if (compareToStart === 1) {
  222. this.select([startDay, date], true);
  223. }
  224. else if (compareToStart === -1) {
  225. this.select([date, null]);
  226. }
  227. else if (allowSameDay) {
  228. this.select([date, date]);
  229. }
  230. }
  231. else {
  232. this.select([date, null]);
  233. }
  234. }
  235. else if (type === 'multiple') {
  236. let selectedIndex;
  237. // @ts-ignore
  238. const selected = currentDate.some((dateItem, index) => {
  239. const equal = compareDay(dateItem, date) === 0;
  240. if (equal) {
  241. selectedIndex = index;
  242. }
  243. return equal;
  244. });
  245. if (selected) {
  246. // @ts-ignore
  247. const cancelDate = currentDate.splice(selectedIndex, 1);
  248. this.setData({ currentDate });
  249. this.unselect(cancelDate);
  250. }
  251. else {
  252. // @ts-ignore
  253. this.select([...currentDate, date]);
  254. }
  255. }
  256. else {
  257. this.select(date, true);
  258. }
  259. },
  260. unselect(dateArray) {
  261. const date = dateArray[0];
  262. if (date) {
  263. this.$emit('unselect', copyDates(date));
  264. }
  265. },
  266. select(date, complete) {
  267. if (complete && this.data.type === 'range') {
  268. const valid = this.checkRange(date);
  269. if (!valid) {
  270. // auto selected to max range if showConfirm
  271. if (this.data.showConfirm) {
  272. this.emit([
  273. date[0],
  274. getDayByOffset(date[0], this.data.maxRange - 1),
  275. ]);
  276. }
  277. else {
  278. this.emit(date);
  279. }
  280. return;
  281. }
  282. }
  283. this.emit(date);
  284. if (complete && !this.data.showConfirm) {
  285. this.onConfirm();
  286. }
  287. },
  288. emit(date) {
  289. const getTime = (date) => date instanceof Date ? date.getTime() : date;
  290. this.setData({
  291. currentDate: Array.isArray(date) ? date.map(getTime) : getTime(date),
  292. });
  293. this.$emit('select', copyDates(date));
  294. },
  295. checkRange(date) {
  296. const { maxRange, rangePrompt, showRangePrompt } = this.data;
  297. if (maxRange && calcDateNum(date) > maxRange) {
  298. if (showRangePrompt) {
  299. Toast({
  300. context: this,
  301. message: rangePrompt || `选择天数不能超过 ${maxRange} 天`,
  302. });
  303. }
  304. this.$emit('over-range');
  305. return false;
  306. }
  307. return true;
  308. },
  309. onConfirm() {
  310. if (this.data.type === 'range' &&
  311. !this.checkRange(this.data.currentDate)) {
  312. return;
  313. }
  314. wx.nextTick(() => {
  315. // @ts-ignore
  316. this.$emit('confirm', copyDates(this.data.currentDate));
  317. });
  318. },
  319. onClickSubtitle(event) {
  320. this.$emit('click-subtitle', event);
  321. },
  322. },
  323. });