picker.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <template>
  2. <el-input
  3. class="el-date-editor"
  4. :class="'el-date-editor--' + type"
  5. :readonly="!editable || readonly"
  6. :disabled="disabled"
  7. :size="size"
  8. :name="name"
  9. v-clickoutside="handleClose"
  10. :placeholder="placeholder"
  11. @focus="handleFocus"
  12. @blur="handleBlur"
  13. @keydown.native="handleKeydown"
  14. :value="displayValue"
  15. @change.native="displayValue = $event.target.value"
  16. :validateEvent="false"
  17. ref="reference">
  18. <i slot="icon"
  19. class="el-input__icon"
  20. @click="handleClickIcon"
  21. :class="[showClose ? 'el-icon-close' : triggerClass]"
  22. @mouseenter="handleMouseEnterIcon"
  23. @mouseleave="showClose = false"
  24. v-if="haveTrigger">
  25. </i>
  26. </el-input>
  27. </template>
  28. <script>
  29. import Vue from 'vue';
  30. import Clickoutside from 'element-ui/src/utils/clickoutside';
  31. import { formatDate, parseDate, getWeekNumber, equalDate, isDate } from './util';
  32. import Popper from 'element-ui/src/utils/vue-popper';
  33. import Emitter from 'element-ui/src/mixins/emitter';
  34. import Focus from 'element-ui/src/mixins/focus';
  35. import ElInput from 'element-ui/packages/input';
  36. import merge from 'element-ui/src/utils/merge';
  37. const NewPopper = {
  38. props: {
  39. appendToBody: Popper.props.appendToBody,
  40. offset: Popper.props.offset,
  41. boundariesPadding: Popper.props.boundariesPadding
  42. },
  43. methods: Popper.methods,
  44. data: Popper.data,
  45. beforeDestroy: Popper.beforeDestroy
  46. };
  47. const DEFAULT_FORMATS = {
  48. date: 'yyyy-MM-dd',
  49. month: 'yyyy-MM',
  50. datetime: 'yyyy-MM-dd HH:mm:ss',
  51. time: 'HH:mm:ss',
  52. week: 'yyyywWW',
  53. timerange: 'HH:mm:ss',
  54. daterange: 'yyyy-MM-dd',
  55. datetimerange: 'yyyy-MM-dd HH:mm:ss',
  56. year: 'yyyy'
  57. };
  58. const HAVE_TRIGGER_TYPES = [
  59. 'date',
  60. 'datetime',
  61. 'time',
  62. 'time-select',
  63. 'week',
  64. 'month',
  65. 'year',
  66. 'daterange',
  67. 'timerange',
  68. 'datetimerange'
  69. ];
  70. const DATE_FORMATTER = function(value, format) {
  71. return formatDate(value, format);
  72. };
  73. const DATE_PARSER = function(text, format) {
  74. return parseDate(text, format);
  75. };
  76. const RANGE_FORMATTER = function(value, format, separator) {
  77. if (Array.isArray(value) && value.length === 2) {
  78. const start = value[0];
  79. const end = value[1];
  80. if (start && end) {
  81. return formatDate(start, format) + separator + formatDate(end, format);
  82. }
  83. }
  84. return '';
  85. };
  86. const RANGE_PARSER = function(text, format, separator) {
  87. const array = text.split(separator);
  88. if (array.length === 2) {
  89. const range1 = array[0];
  90. const range2 = array[1];
  91. return [parseDate(range1, format), parseDate(range2, format)];
  92. }
  93. return [];
  94. };
  95. const TYPE_VALUE_RESOLVER_MAP = {
  96. default: {
  97. formatter(value) {
  98. if (!value) return '';
  99. return '' + value;
  100. },
  101. parser(text) {
  102. if (text === undefined || text === '') return null;
  103. return text;
  104. }
  105. },
  106. week: {
  107. formatter(value, format) {
  108. let date = formatDate(value, format);
  109. const week = getWeekNumber(value);
  110. date = /WW/.test(date)
  111. ? date.replace(/WW/, week < 10 ? '0' + week : week)
  112. : date.replace(/W/, week);
  113. return date;
  114. },
  115. parser(text) {
  116. const array = (text || '').split('w');
  117. if (array.length === 2) {
  118. const year = Number(array[0]);
  119. const month = Number(array[1]);
  120. if (!isNaN(year) && !isNaN(month) && month < 54) {
  121. return text;
  122. }
  123. }
  124. return null;
  125. }
  126. },
  127. date: {
  128. formatter: DATE_FORMATTER,
  129. parser: DATE_PARSER
  130. },
  131. datetime: {
  132. formatter: DATE_FORMATTER,
  133. parser: DATE_PARSER
  134. },
  135. daterange: {
  136. formatter: RANGE_FORMATTER,
  137. parser: RANGE_PARSER
  138. },
  139. datetimerange: {
  140. formatter: RANGE_FORMATTER,
  141. parser: RANGE_PARSER
  142. },
  143. timerange: {
  144. formatter: RANGE_FORMATTER,
  145. parser: RANGE_PARSER
  146. },
  147. time: {
  148. formatter: DATE_FORMATTER,
  149. parser: DATE_PARSER
  150. },
  151. month: {
  152. formatter: DATE_FORMATTER,
  153. parser: DATE_PARSER
  154. },
  155. year: {
  156. formatter: DATE_FORMATTER,
  157. parser: DATE_PARSER
  158. },
  159. number: {
  160. formatter(value) {
  161. if (!value) return '';
  162. return '' + value;
  163. },
  164. parser(text) {
  165. let result = Number(text);
  166. if (!isNaN(text)) {
  167. return result;
  168. } else {
  169. return null;
  170. }
  171. }
  172. }
  173. };
  174. const PLACEMENT_MAP = {
  175. left: 'bottom-start',
  176. center: 'bottom',
  177. right: 'bottom-end'
  178. };
  179. // only considers date-picker's value: Date or [Date, Date]
  180. const valueEquals = function(a, b) {
  181. const aIsArray = a instanceof Array;
  182. const bIsArray = b instanceof Array;
  183. if (aIsArray && bIsArray) {
  184. return new Date(a[0]).getTime() === new Date(b[0]).getTime() &&
  185. new Date(a[1]).getTime() === new Date(b[1]).getTime();
  186. }
  187. if (!aIsArray && !bIsArray) {
  188. return new Date(a).getTime() === new Date(b).getTime();
  189. }
  190. return false;
  191. };
  192. export default {
  193. mixins: [Emitter, NewPopper, Focus('reference')],
  194. props: {
  195. size: String,
  196. format: String,
  197. readonly: Boolean,
  198. placeholder: String,
  199. name: String,
  200. disabled: Boolean,
  201. clearable: {
  202. type: Boolean,
  203. default: true
  204. },
  205. popperClass: String,
  206. editable: {
  207. type: Boolean,
  208. default: true
  209. },
  210. align: {
  211. type: String,
  212. default: 'left'
  213. },
  214. value: {},
  215. defaultValue: {},
  216. rangeSeparator: {
  217. default: ' - '
  218. },
  219. pickerOptions: {}
  220. },
  221. components: { ElInput },
  222. directives: { Clickoutside },
  223. data() {
  224. return {
  225. pickerVisible: false,
  226. showClose: false,
  227. currentValue: '',
  228. unwatchPickerOptions: null
  229. };
  230. },
  231. watch: {
  232. pickerVisible(val) {
  233. if (!val) this.dispatch('ElFormItem', 'el.form.blur');
  234. if (this.readonly || this.disabled) return;
  235. val ? this.showPicker() : this.hidePicker();
  236. },
  237. currentValue(val) {
  238. if (val) return;
  239. if (this.picker && typeof this.picker.handleClear === 'function') {
  240. this.picker.handleClear();
  241. } else {
  242. this.$emit('input');
  243. }
  244. },
  245. value: {
  246. immediate: true,
  247. handler(val) {
  248. this.currentValue = isDate(val) ? new Date(val) : val;
  249. }
  250. },
  251. displayValue(val) {
  252. this.$emit('change', val);
  253. this.dispatch('ElFormItem', 'el.form.change');
  254. }
  255. },
  256. computed: {
  257. reference() {
  258. return this.$refs.reference.$el;
  259. },
  260. refInput() {
  261. if (this.reference) return this.reference.querySelector('input');
  262. return {};
  263. },
  264. valueIsEmpty() {
  265. const val = this.currentValue;
  266. if (Array.isArray(val)) {
  267. for (let i = 0, len = val.length; i < len; i++) {
  268. if (val[i]) {
  269. return false;
  270. }
  271. }
  272. } else {
  273. if (val) {
  274. return false;
  275. }
  276. }
  277. return true;
  278. },
  279. triggerClass() {
  280. return this.type.indexOf('time') !== -1 ? 'el-icon-time' : 'el-icon-date';
  281. },
  282. selectionMode() {
  283. if (this.type === 'week') {
  284. return 'week';
  285. } else if (this.type === 'month') {
  286. return 'month';
  287. } else if (this.type === 'year') {
  288. return 'year';
  289. }
  290. return 'day';
  291. },
  292. haveTrigger() {
  293. if (typeof this.showTrigger !== 'undefined') {
  294. return this.showTrigger;
  295. }
  296. return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
  297. },
  298. displayValue: {
  299. get() {
  300. const value = this.currentValue;
  301. if (!value) return;
  302. const formatter = (
  303. TYPE_VALUE_RESOLVER_MAP[this.type] ||
  304. TYPE_VALUE_RESOLVER_MAP['default']
  305. ).formatter;
  306. const format = DEFAULT_FORMATS[this.type];
  307. return formatter(value, this.format || format, this.rangeSeparator);
  308. },
  309. set(value) {
  310. if (value) {
  311. const type = this.type;
  312. const parser = (
  313. TYPE_VALUE_RESOLVER_MAP[type] ||
  314. TYPE_VALUE_RESOLVER_MAP['default']
  315. ).parser;
  316. const parsedValue = parser(value, this.format || DEFAULT_FORMATS[type], this.rangeSeparator);
  317. if (parsedValue && this.picker) {
  318. this.picker.value = parsedValue;
  319. }
  320. } else {
  321. this.$emit('input', value);
  322. this.picker.value = value;
  323. }
  324. this.$forceUpdate();
  325. }
  326. }
  327. },
  328. created() {
  329. // vue-popper
  330. this.popperOptions = {
  331. boundariesPadding: 0,
  332. gpuAcceleration: false
  333. };
  334. this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left;
  335. },
  336. methods: {
  337. handleMouseEnterIcon() {
  338. if (this.readonly || this.disabled) return;
  339. if (!this.valueIsEmpty && this.clearable) {
  340. this.showClose = true;
  341. }
  342. },
  343. handleClickIcon() {
  344. if (this.readonly || this.disabled) return;
  345. if (this.showClose) {
  346. this.currentValue = this.$options.defaultValue || '';
  347. this.showClose = false;
  348. } else {
  349. this.pickerVisible = !this.pickerVisible;
  350. }
  351. },
  352. dateChanged(dateA, dateB) {
  353. if (Array.isArray(dateA)) {
  354. let len = dateA.length;
  355. if (!dateB) return true;
  356. while (len--) {
  357. if (!equalDate(dateA[len], dateB[len])) return true;
  358. }
  359. } else {
  360. if (!equalDate(dateA, dateB)) return true;
  361. }
  362. return false;
  363. },
  364. handleClose() {
  365. this.pickerVisible = false;
  366. },
  367. handleFocus() {
  368. const type = this.type;
  369. if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
  370. this.pickerVisible = true;
  371. }
  372. this.$emit('focus', this);
  373. },
  374. handleBlur() {
  375. this.$emit('blur', this);
  376. },
  377. handleKeydown(event) {
  378. const keyCode = event.keyCode;
  379. // TAB or ESC
  380. if (keyCode === 9 || keyCode === 27) {
  381. this.pickerVisible = false;
  382. event.stopPropagation();
  383. }
  384. },
  385. hidePicker() {
  386. if (this.picker) {
  387. this.picker.resetView && this.picker.resetView();
  388. this.pickerVisible = this.picker.visible = false;
  389. this.destroyPopper();
  390. }
  391. },
  392. showPicker() {
  393. if (this.$isServer) return;
  394. if (!this.picker) {
  395. this.mountPicker();
  396. }
  397. this.pickerVisible = this.picker.visible = true;
  398. this.updatePopper();
  399. if (this.currentValue instanceof Date) {
  400. this.picker.date = new Date(this.currentValue.getTime());
  401. } else {
  402. this.picker.value = this.currentValue;
  403. }
  404. this.picker.resetView && this.picker.resetView();
  405. this.$nextTick(() => {
  406. this.picker.ajustScrollTop && this.picker.ajustScrollTop();
  407. });
  408. },
  409. mountPicker() {
  410. const defaultValue = this.defaultValue || this.currentValue;
  411. const panel = merge({}, this.panel, { defaultValue });
  412. this.picker = new Vue(panel).$mount();
  413. this.picker.popperClass = this.popperClass;
  414. this.popperElm = this.picker.$el;
  415. this.picker.width = this.reference.getBoundingClientRect().width;
  416. this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
  417. this.picker.selectionMode = this.selectionMode;
  418. if (this.format) {
  419. this.picker.format = this.format;
  420. }
  421. const updateOptions = () => {
  422. const options = this.pickerOptions;
  423. if (options && options.selectableRange) {
  424. let ranges = options.selectableRange;
  425. const parser = TYPE_VALUE_RESOLVER_MAP.datetimerange.parser;
  426. const format = DEFAULT_FORMATS.timerange;
  427. ranges = Array.isArray(ranges) ? ranges : [ranges];
  428. this.picker.selectableRange = ranges.map(range => parser(range, format, this.rangeSeparator));
  429. }
  430. for (const option in options) {
  431. if (options.hasOwnProperty(option) &&
  432. // 忽略 time-picker 的该配置项
  433. option !== 'selectableRange') {
  434. this.picker[option] = options[option];
  435. }
  436. }
  437. };
  438. updateOptions();
  439. this.unwatchPickerOptions = this.$watch('pickerOptions', () => updateOptions(), { deep: true });
  440. this.$el.appendChild(this.picker.$el);
  441. this.picker.resetView && this.picker.resetView();
  442. this.picker.$on('dodestroy', this.doDestroy);
  443. this.picker.$on('pick', (date = '', visible = false) => {
  444. // do not emit if values are same
  445. if (!valueEquals(this.value, date)) {
  446. this.$emit('input', date);
  447. }
  448. this.pickerVisible = this.picker.visible = visible;
  449. this.picker.resetView && this.picker.resetView();
  450. });
  451. this.picker.$on('select-range', (start, end) => {
  452. this.refInput.setSelectionRange(start, end);
  453. this.refInput.focus();
  454. });
  455. },
  456. unmountPicker() {
  457. if (this.picker) {
  458. this.picker.$destroy();
  459. this.picker.$off();
  460. if (typeof this.unwatchPickerOptions === 'function') {
  461. this.unwatchPickerOptions();
  462. }
  463. this.picker.$el.parentNode.removeChild(this.picker.$el);
  464. }
  465. }
  466. }
  467. };
  468. </script>