select.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. <template>
  2. <div
  3. class="el-select"
  4. v-clickoutside="handleClose"
  5. :class="{ 'is-multiple': multiple, 'is-small': size === 'small' }">
  6. <div class="el-select__tags" v-if="multiple" @click.stop="toggleMenu" ref="tags" :style="{ 'max-width': inputWidth - 32 + 'px' }">
  7. <transition-group @after-leave="resetInputHeight">
  8. <el-tag
  9. v-for="item in selected"
  10. :key="item"
  11. closable
  12. :hit="item.hitState"
  13. type="primary"
  14. @close="deleteTag($event, item)"
  15. close-transition>{{ item.currentLabel }}</el-tag>
  16. </transition-group>
  17. <input
  18. type="text"
  19. class="el-select__input"
  20. @focus="visible = true"
  21. @keyup="managePlaceholder"
  22. @keydown="resetInputState"
  23. @keydown.down.prevent="navigateOptions('next')"
  24. @keydown.up.prevent="navigateOptions('prev')"
  25. @keydown.enter.prevent="selectOption"
  26. @keydown.esc.prevent="visible = false"
  27. @keydown.delete="deletePrevTag"
  28. v-model="query"
  29. :debounce="remote ? 300 : 0"
  30. v-if="filterable"
  31. :style="{ width: inputLength + 'px', 'max-width': inputWidth - 42 + 'px' }"
  32. ref="input">
  33. </div>
  34. <el-input
  35. ref="reference"
  36. v-model="selectedLabel"
  37. type="text"
  38. :placeholder="currentPlaceholder"
  39. :name="name"
  40. :disabled="disabled"
  41. :readonly="!filterable || multiple"
  42. @click.native="toggleMenu"
  43. @keyup.native="debouncedOnInputChange"
  44. @keydown.native.down.prevent="navigateOptions('next')"
  45. @keydown.native.up.prevent="navigateOptions('prev')"
  46. @keydown.native.enter.prevent="selectOption"
  47. @keydown.native.esc.prevent="visible = false"
  48. @keydown.native.tab="visible = false"
  49. @mouseenter.native="inputHovering = true"
  50. @mouseleave.native="inputHovering = false"
  51. :icon="iconClass">
  52. </el-input>
  53. <transition name="md-fade-bottom" @after-leave="doDestroy">
  54. <el-select-menu
  55. ref="popper"
  56. v-show="visible && emptyText !== false">
  57. <ul class="el-select-dropdown__list" v-show="options.length > 0 && filteredOptionsCount > 0 && !loading">
  58. <slot></slot>
  59. </ul>
  60. <p class="el-select-dropdown__empty" v-if="emptyText">{{ emptyText }}</p>
  61. </el-select-menu>
  62. </transition>
  63. </div>
  64. </template>
  65. <script type="text/babel">
  66. import Emitter from 'element-ui/src/mixins/emitter';
  67. import Locale from 'element-ui/src/mixins/locale';
  68. import ElInput from 'element-ui/packages/input';
  69. import ElSelectMenu from './select-dropdown.vue';
  70. import ElTag from 'element-ui/packages/tag';
  71. import debounce from 'throttle-debounce/debounce';
  72. import Clickoutside from 'element-ui/src/utils/clickoutside';
  73. import { addClass, removeClass, hasClass } from 'wind-dom/src/class';
  74. import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
  75. import { t } from 'element-ui/src/locale';
  76. export default {
  77. mixins: [Emitter, Locale],
  78. name: 'ElSelect',
  79. componentName: 'select',
  80. computed: {
  81. iconClass() {
  82. return this.showCloseIcon ? 'circle-close' : (this.remote && this.filterable ? '' : 'caret-top');
  83. },
  84. debounce() {
  85. return this.remote ? 300 : 0;
  86. },
  87. showCloseIcon() {
  88. let criteria = this.clearable && this.inputHovering && !this.multiple && this.options.indexOf(this.selected) > -1;
  89. if (!this.$el) return false;
  90. let icon = this.$el.querySelector('.el-input__icon');
  91. if (icon) {
  92. if (criteria) {
  93. icon.addEventListener('click', this.deleteSelected);
  94. addClass(icon, 'is-show-close');
  95. } else {
  96. icon.removeEventListener('click', this.deleteSelected);
  97. removeClass(icon, 'is-show-close');
  98. }
  99. }
  100. return criteria;
  101. },
  102. emptyText() {
  103. if (this.loading) {
  104. return this.t('el.select.loading');
  105. } else {
  106. if (this.voidRemoteQuery) {
  107. this.voidRemoteQuery = false;
  108. return false;
  109. }
  110. if (this.filterable && this.filteredOptionsCount === 0) {
  111. return this.t('el.select.noMatch');
  112. }
  113. if (this.options.length === 0) {
  114. return this.t('el.select.noData');
  115. }
  116. }
  117. return null;
  118. }
  119. },
  120. components: {
  121. ElInput,
  122. ElSelectMenu,
  123. ElTag
  124. },
  125. directives: { Clickoutside },
  126. props: {
  127. name: String,
  128. value: {},
  129. size: String,
  130. disabled: Boolean,
  131. clearable: Boolean,
  132. filterable: Boolean,
  133. loading: Boolean,
  134. remote: Boolean,
  135. remoteMethod: Function,
  136. filterMethod: Function,
  137. multiple: Boolean,
  138. placeholder: {
  139. type: String,
  140. default() {
  141. return t('el.select.placeholder');
  142. }
  143. }
  144. },
  145. data() {
  146. return {
  147. options: [],
  148. selected: {},
  149. isSelect: true,
  150. inputLength: 20,
  151. inputWidth: 0,
  152. valueChangeBySelected: false,
  153. cachedPlaceHolder: '',
  154. optionsCount: 0,
  155. filteredOptionsCount: 0,
  156. dropdownUl: null,
  157. visible: false,
  158. selectedLabel: '',
  159. selectInit: false,
  160. hoverIndex: -1,
  161. query: '',
  162. voidRemoteQuery: false,
  163. bottomOverflowBeforeHidden: 0,
  164. optionsAllDisabled: false,
  165. inputHovering: false,
  166. currentPlaceholder: ''
  167. };
  168. },
  169. watch: {
  170. placeholder(val) {
  171. this.currentPlaceholder = val;
  172. },
  173. value(val) {
  174. if (this.valueChangeBySelected) {
  175. this.valueChangeBySelected = false;
  176. return;
  177. }
  178. this.$nextTick(() => {
  179. if (this.multiple && Array.isArray(val)) {
  180. this.$nextTick(() => {
  181. this.resetInputHeight();
  182. });
  183. this.selectedInit = true;
  184. this.selected = [];
  185. this.currentPlaceholder = this.cachedPlaceHolder;
  186. val.forEach(item => {
  187. let option = this.options.filter(option => option.value === item)[0];
  188. if (option) {
  189. this.$emit('addOptionToValue', option);
  190. }
  191. });
  192. }
  193. if (!this.multiple) {
  194. let option = this.options.filter(option => option.value === val)[0];
  195. if (option) {
  196. this.$emit('addOptionToValue', option);
  197. } else {
  198. this.selected = {};
  199. this.selectedLabel = '';
  200. }
  201. }
  202. this.resetHoverIndex();
  203. });
  204. },
  205. selected(val, oldVal) {
  206. if (this.multiple) {
  207. if (this.selected.length > 0) {
  208. this.currentPlaceholder = '';
  209. } else {
  210. this.currentPlaceholder = this.cachedPlaceHolder;
  211. }
  212. this.$nextTick(() => {
  213. this.resetInputHeight();
  214. });
  215. if (this.selectedInit) {
  216. this.selectedInit = false;
  217. return;
  218. }
  219. this.valueChangeBySelected = true;
  220. const result = val.map(item => item.value);
  221. this.$emit('input', result);
  222. this.$emit('change', result);
  223. this.dispatch('form-item', 'el.form.change', val);
  224. if (this.filterable) {
  225. this.query = '';
  226. this.hoverIndex = -1;
  227. this.$refs.input.focus();
  228. this.inputLength = 20;
  229. }
  230. } else {
  231. if (this.selectedInit) {
  232. this.selectedInit = false;
  233. return;
  234. }
  235. if (val.value === oldVal.value) return;
  236. this.$emit('input', val.value);
  237. this.$emit('change', val.value);
  238. }
  239. },
  240. query(val) {
  241. this.$nextTick(() => {
  242. this.broadcast('select-dropdown', 'updatePopper');
  243. });
  244. if (this.multiple && this.filterable) {
  245. this.resetInputHeight();
  246. }
  247. if (this.remote && typeof this.remoteMethod === 'function') {
  248. this.hoverIndex = -1;
  249. this.remoteMethod(val);
  250. this.voidRemoteQuery = val === '';
  251. this.broadcast('option', 'resetIndex');
  252. } else if (typeof this.filterMethod === 'function') {
  253. this.filterMethod(val);
  254. } else {
  255. this.filteredOptionsCount = this.optionsCount;
  256. this.broadcast('option', 'queryChange', val);
  257. }
  258. },
  259. visible(val) {
  260. if (!val) {
  261. this.$refs.reference.$el.querySelector('input').blur();
  262. if (this.$el.querySelector('.el-input__icon')) {
  263. removeClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
  264. }
  265. this.broadcast('select-dropdown', 'destroyPopper');
  266. if (this.$refs.input) {
  267. this.$refs.input.blur();
  268. }
  269. this.resetHoverIndex();
  270. if (!this.multiple) {
  271. if (this.dropdownUl && this.selected.$el) {
  272. this.bottomOverflowBeforeHidden = this.selected.$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
  273. }
  274. if (this.selected && this.selected.value) {
  275. this.selectedLabel = this.selected.currentLabel;
  276. }
  277. }
  278. } else {
  279. let icon = this.$el.querySelector('.el-input__icon');
  280. if (icon && !hasClass(icon, 'el-icon-circle-close')) {
  281. addClass(this.$el.querySelector('.el-input__icon'), 'is-reverse');
  282. }
  283. this.broadcast('select-dropdown', 'updatePopper');
  284. if (this.filterable) {
  285. this.query = this.selectedLabel;
  286. if (this.multiple) {
  287. this.$refs.input.focus();
  288. } else {
  289. this.broadcast('input', 'inputSelect');
  290. }
  291. }
  292. if (!this.dropdownUl) {
  293. let dropdownChildNodes = this.$refs.popper.$el.childNodes;
  294. this.dropdownUl = [].filter.call(dropdownChildNodes, item => item.tagName === 'UL')[0];
  295. }
  296. if (!this.multiple && this.dropdownUl) {
  297. if (this.bottomOverflowBeforeHidden > 0) {
  298. this.dropdownUl.scrollTop += this.bottomOverflowBeforeHidden;
  299. }
  300. }
  301. }
  302. },
  303. options(val) {
  304. this.optionsAllDisabled = val.length === val.filter(item => item.disabled === true).length;
  305. }
  306. },
  307. methods: {
  308. doDestroy() {
  309. this.$refs.popper.doDestroy();
  310. },
  311. handleClose() {
  312. this.visible = false;
  313. },
  314. toggleLastOptionHitState(hit) {
  315. if (!Array.isArray(this.selected)) return;
  316. const option = this.selected[this.selected.length - 1];
  317. if (!option) return;
  318. if (hit === true || hit === false) {
  319. option.hitState = hit;
  320. return hit;
  321. }
  322. option.hitState = !option.hitState;
  323. return option.hitState;
  324. },
  325. deletePrevTag(e) {
  326. if (e.target.value.length <= 0 && !this.toggleLastOptionHitState()) {
  327. this.selected.pop();
  328. }
  329. },
  330. addOptionToValue(option, init) {
  331. if (this.multiple) {
  332. if (this.selected.indexOf(option) === -1 && (this.remote ? this.value.indexOf(option.value) === -1 : true)) {
  333. this.selectedInit = !!init;
  334. this.selected.push(option);
  335. this.resetHoverIndex();
  336. }
  337. } else {
  338. this.selectedInit = !!init;
  339. this.selected = option;
  340. this.selectedLabel = option.currentLabel;
  341. this.hoverIndex = option.index;
  342. }
  343. },
  344. managePlaceholder() {
  345. if (this.currentPlaceholder !== '') {
  346. this.currentPlaceholder = this.$refs.input.value ? '' : this.cachedPlaceHolder;
  347. }
  348. },
  349. resetInputState(e) {
  350. if (e.keyCode !== 8) this.toggleLastOptionHitState(false);
  351. this.inputLength = this.$refs.input.value.length * 15 + 20;
  352. },
  353. resetInputHeight() {
  354. this.$nextTick(() => {
  355. let inputChildNodes = this.$refs.reference.$el.childNodes;
  356. let input = [].filter.call(inputChildNodes, item => item.tagName === 'INPUT')[0];
  357. input.style.height = Math.max(this.$refs.tags.clientHeight + 6, this.size === 'small' ? 28 : 36) + 'px';
  358. this.broadcast('select-dropdown', 'updatePopper');
  359. });
  360. },
  361. resetHoverIndex() {
  362. setTimeout(() => {
  363. if (!this.multiple) {
  364. this.hoverIndex = this.options.indexOf(this.selected);
  365. } else {
  366. if (this.selected.length > 0) {
  367. this.hoverIndex = Math.min.apply(null, this.selected.map(item => this.options.indexOf(item)));
  368. } else {
  369. this.hoverIndex = -1;
  370. }
  371. }
  372. }, 300);
  373. },
  374. handleOptionSelect(option) {
  375. if (!this.multiple) {
  376. this.selected = option;
  377. this.selectedLabel = option.currentLabel;
  378. this.visible = false;
  379. } else {
  380. let optionIndex = -1;
  381. this.selected.forEach((item, index) => {
  382. if (item === option || item.currentLabel === option.currentLabel) {
  383. optionIndex = index;
  384. }
  385. });
  386. if (optionIndex > -1) {
  387. this.selected.splice(optionIndex, 1);
  388. } else {
  389. this.selected.push(option);
  390. }
  391. }
  392. },
  393. toggleMenu() {
  394. if (this.filterable && this.query === '' && this.visible) {
  395. return;
  396. }
  397. if (!this.disabled) {
  398. this.visible = !this.visible;
  399. }
  400. },
  401. navigateOptions(direction) {
  402. if (!this.visible) {
  403. this.visible = true;
  404. return;
  405. }
  406. if (!this.optionsAllDisabled) {
  407. if (direction === 'next') {
  408. this.hoverIndex++;
  409. if (this.hoverIndex === this.options.length) {
  410. this.hoverIndex = 0;
  411. }
  412. this.resetScrollTop();
  413. if (this.options[this.hoverIndex].disabled === true ||
  414. this.options[this.hoverIndex].groupDisabled === true ||
  415. !this.options[this.hoverIndex].visible) {
  416. this.navigateOptions('next');
  417. }
  418. }
  419. if (direction === 'prev') {
  420. this.hoverIndex--;
  421. if (this.hoverIndex < 0) {
  422. this.hoverIndex = this.options.length - 1;
  423. }
  424. this.resetScrollTop();
  425. if (this.options[this.hoverIndex].disabled === true ||
  426. this.options[this.hoverIndex].groupDisabled === true ||
  427. !this.options[this.hoverIndex].visible) {
  428. this.navigateOptions('prev');
  429. }
  430. }
  431. }
  432. },
  433. resetScrollTop() {
  434. let bottomOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().bottom - this.$refs.popper.$el.getBoundingClientRect().bottom;
  435. let topOverflowDistance = this.options[this.hoverIndex].$el.getBoundingClientRect().top - this.$refs.popper.$el.getBoundingClientRect().top;
  436. if (bottomOverflowDistance > 0) {
  437. this.dropdownUl.scrollTop += bottomOverflowDistance;
  438. }
  439. if (topOverflowDistance < 0) {
  440. this.dropdownUl.scrollTop += topOverflowDistance;
  441. }
  442. },
  443. selectOption() {
  444. if (this.options[this.hoverIndex]) {
  445. this.handleOptionSelect(this.options[this.hoverIndex]);
  446. }
  447. },
  448. deleteSelected(event) {
  449. event.stopPropagation();
  450. this.selected = {};
  451. this.selectedLabel = '';
  452. this.$emit('input', '');
  453. this.$emit('change', '');
  454. this.visible = false;
  455. },
  456. deleteTag(event, tag) {
  457. let index = this.selected.indexOf(tag);
  458. if (index > -1) {
  459. this.selected.splice(index, 1);
  460. }
  461. event.stopPropagation();
  462. },
  463. onInputChange() {
  464. if (this.filterable && this.selectedLabel !== this.value) {
  465. this.query = this.selectedLabel;
  466. }
  467. },
  468. onOptionDestroy(option) {
  469. this.optionsCount--;
  470. this.filteredOptionsCount--;
  471. let index = this.options.indexOf(option);
  472. if (index > -1) {
  473. this.options.splice(index, 1);
  474. }
  475. this.broadcast('option', 'resetIndex');
  476. },
  477. resetInputWidth() {
  478. this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width;
  479. }
  480. },
  481. created() {
  482. this.cachedPlaceHolder = this.currentPlaceholder = this.placeholder;
  483. if (this.multiple) {
  484. this.selectedInit = true;
  485. this.selected = [];
  486. }
  487. if (this.remote) {
  488. this.voidRemoteQuery = true;
  489. }
  490. this.debouncedOnInputChange = debounce(this.debounce, () => {
  491. this.onInputChange();
  492. });
  493. this.$on('addOptionToValue', this.addOptionToValue);
  494. this.$on('handleOptionClick', this.handleOptionSelect);
  495. this.$on('onOptionDestroy', this.onOptionDestroy);
  496. },
  497. mounted() {
  498. addResizeListener(this.$el, this.resetInputWidth);
  499. if (this.remote && this.multiple && Array.isArray(this.value)) {
  500. this.selected = this.options.reduce((prev, curr) => {
  501. return this.value.indexOf(curr.value) > -1 ? prev.concat(curr) : prev;
  502. }, []);
  503. this.$nextTick(() => {
  504. this.resetInputHeight();
  505. });
  506. }
  507. this.$nextTick(() => {
  508. if (this.$refs.reference.$el) {
  509. this.inputWidth = this.$refs.reference.$el.getBoundingClientRect().width;
  510. }
  511. });
  512. },
  513. destroyed() {
  514. if (this.resetInputWidth) removeResizeListener(this.$el, this.resetInputWidth);
  515. }
  516. };
  517. </script>