You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

search-select.vue 18 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <!--
  2. Copyright 2020-2021 Huawei Technologies Co., Ltd.All Rights Reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. -->
  13. <template>
  14. <div class="cl-search-select"
  15. ref="selector"
  16. @click="mouseClick($event)"
  17. tabindex="0"
  18. @keyup.enter="enter">
  19. <div class="cl-search-select-inner"
  20. :class="{'is-focus': ifFocus}"
  21. @click="click">
  22. <!-- Multiple Choice -->
  23. <template v-if="multiple">
  24. <div class="mul-tag"
  25. v-if="indexes.length > 0">
  26. <div class="mul-tag-content"
  27. :title="options[indexes[0]].label"
  28. :style="{'text-overflow': overflow}">
  29. {{options[indexes[0]].label}}
  30. </div>
  31. <div class="mul-tag-del"
  32. @click="cancelLabel($event, indexes[0])"
  33. v-if="cancel && !options[indexes[0]].disabled">
  34. <i class="el-icon-close"></i>
  35. </div>
  36. </div>
  37. <div v-else>
  38. {{sPlaceholder}}
  39. </div>
  40. <div v-if="indexes.length > 1"
  41. class="mul-tag">
  42. <div class="mul-tag-content">
  43. +{{indexes.length - 1}}
  44. </div>
  45. </div>
  46. </template>
  47. <!-- Single choice -->
  48. <template v-else>
  49. <div class="single-tag"
  50. :style="{'text-overflow': overflow}">
  51. {{indexes.length > 0 ? options[indexes[0]].label : sPlaceholder}}
  52. </div>
  53. </template>
  54. </div>
  55. <div class="select-container"
  56. :style="{
  57. 'top': containerTop,
  58. 'left': containerLeft,
  59. 'min-width': containerWidth,
  60. }"
  61. v-show="ifActive">
  62. <!-- Filter Line -->
  63. <div class="filter-container">
  64. <el-input v-model="filter"
  65. clearable
  66. class="has-gap">
  67. </el-input>
  68. <span class="has-gap"
  69. :class="{'able': functionAble, 'disable': !functionAble}"
  70. @click="selectAll"
  71. v-if="multiple">{{$t('public.selectAll')}}</span>
  72. <span @click="clearAll"
  73. :class="{'able': functionAble, 'disable': !functionAble}"
  74. v-if="multiple">
  75. {{$t('public.clear')}}</span>
  76. </div>
  77. <div class="option-container"
  78. ref="options">
  79. <div v-for="(option, index) in options"
  80. :key="option.label"
  81. :value="option.value"
  82. v-show="option.show"
  83. class="select-option"
  84. :class="{
  85. 'is-selected': option.selected,
  86. 'is-disabled': option.disabled,
  87. }"
  88. @click="optionClick(option, index)">
  89. <div class="label-container"
  90. :title="option.label">{{option.label}}</div>
  91. <div class="icon-container">
  92. <i class="el-icon-check"
  93. :class="{'icon-no-selected': !option.selected}"></i>
  94. </div>
  95. </div>
  96. </div>
  97. <div class="option-empty"
  98. v-show="ifEmpty">
  99. {{$t('public.emptyData')}}
  100. </div>
  101. </div>
  102. </div>
  103. </template>
  104. <script>
  105. /**
  106. * The publicStore holds the key of focused selector
  107. * When there is more than two component in same page, help selector to keep correct display
  108. */
  109. const publicStore = {
  110. activeKey: {
  111. key: '',
  112. },
  113. };
  114. export default {
  115. props: {
  116. multiple: {
  117. type: Boolean,
  118. default: false,
  119. }, // If open multiple choice
  120. sPlaceholder: {
  121. type: String,
  122. default: '',
  123. }, // The placeholder of selector
  124. iPlaceholder: {
  125. type: String,
  126. default: '',
  127. }, // The placeholder of filter input
  128. overflow: {
  129. type: String,
  130. default: 'none',
  131. }, // The display way of overflow selected label('none' or 'ellipsis')
  132. cancel: {
  133. type: Boolean,
  134. default: true,
  135. }, // If the tag can be cancel by icon, only effective when multiple is true
  136. plain: {
  137. type: Boolean,
  138. default: false,
  139. }, // Represents whether the type of the incoming array, 'true' stands for 'String', 'false' stands for 'Object'
  140. source: {
  141. type: Array,
  142. default: () => {
  143. return [];
  144. },
  145. }, // The total data source of component
  146. labelName: {
  147. type: String,
  148. default: 'label',
  149. }, // The name of the property representing label in the object
  150. valueName: {
  151. type: String,
  152. default: 'value',
  153. }, // The name of the property representing value in the object
  154. disabledName: {
  155. type: String,
  156. default: 'disabled',
  157. }, // The name of the property representing disabled in the object
  158. selectedName: {
  159. type: String,
  160. default: 'selected',
  161. }, // The name of the property representing selected in the object
  162. },
  163. data() {
  164. return {
  165. ifFocus: false, // When the selector is focused, the options area may not display
  166. ifActive: false, // When the selector is active, the options area must display
  167. options: [], // The option list after processing original data
  168. containerTop: 0, // The top to set container position
  169. containerLeft: 0, // The left to set container position
  170. containerWidth: 0, // The min-width of container
  171. containerToSelector: 12, // The gap between options-container and selector
  172. functionAble: true, // The effective of button after input
  173. // The key of component to make sure it can be distinguished from publicStore
  174. key: new Date().getTime().toString(),
  175. filter: '', // The value to filter the option
  176. ifEmpty: true, // If no option match the filter
  177. activeKey: undefined, // In order to add wacter to the publicStore.activeKey.key
  178. latestIndex: undefined, // The index of the last click option, only effective when multiple is false
  179. indexes: [], // The list of index of selected options
  180. filterDebounce: 150, // The filter watcher debounce time in ms
  181. };
  182. },
  183. methods: {
  184. /**
  185. * The logic of calculate values or value
  186. * @return {Array | String | Number}
  187. */
  188. calValues() {
  189. if (this.multiple) {
  190. const values = [];
  191. for (let i = 0; i < this.indexes.length; i++) {
  192. values.push(this.options[this.indexes[i]].value);
  193. }
  194. return values;
  195. } else {
  196. return this.options[this.indexes[0]].value;
  197. }
  198. },
  199. /**
  200. * The logic of enter down
  201. */
  202. enter() {
  203. this.ifFocus = false;
  204. this.ifActive = false;
  205. this.$emit('selectEnter');
  206. },
  207. /**
  208. * The logic of click the display area of selector, excluding the options area
  209. */
  210. click() {
  211. this.ifActive = !this.ifActive;
  212. this.ifFocus = true;
  213. },
  214. /**
  215. * The logic of click event that add to window, which can make response to defocus
  216. */
  217. clickHandler() {
  218. if (this.ifFocus) {
  219. this.ifFocus = false;
  220. this.ifActive = false;
  221. this.$emit('selectBlur');
  222. }
  223. },
  224. /**
  225. * The logic of click the selector, including everywhere
  226. * @param {Object} event
  227. */
  228. mouseClick(event) {
  229. publicStore.activeKey.key = this.key;
  230. event.stopPropagation();
  231. event.preventDefault();
  232. },
  233. /**
  234. * The logic of click selectAll button
  235. */
  236. selectAll() {
  237. if (!this.functionAble) {
  238. return;
  239. }
  240. this.indexes = [];
  241. for (let i = 0; i < this.options.length; i++) {
  242. if (this.options[i].disabled) {
  243. if (this.options[i].selected) {
  244. this.indexes.push(i);
  245. }
  246. } else {
  247. this.mulSelectOption(this.options[i], i);
  248. }
  249. }
  250. },
  251. /**
  252. * The logic of click clearAll button
  253. */
  254. clearAll() {
  255. if (!this.functionAble) {
  256. return;
  257. }
  258. const indexes = [];
  259. for (let i = 0; i < this.indexes.length; i++) {
  260. const option = this.options[this.indexes[i]];
  261. if (option.disabled) {
  262. indexes.push(i);
  263. } else {
  264. option.selected = false;
  265. }
  266. }
  267. this.indexes = indexes;
  268. },
  269. /**
  270. * The logic of process original data when group is false
  271. * @param {Array} data
  272. * @return {Array}
  273. */
  274. processDefault(data) {
  275. const options = [];
  276. if (!this.plain) {
  277. for (let i = 0; i < data.length; i++) {
  278. options.push({
  279. label: data[i][this.labelName],
  280. value: data[i][this.valueName],
  281. disabled: data[i][this.disabledName] === true ? true : false,
  282. selected: data[i][this.selectedName] === true ? true : false,
  283. show: true,
  284. });
  285. if (options[i].selected) {
  286. this.indexes.push(i);
  287. }
  288. }
  289. } else {
  290. for (let i = 0; i < data.length; i++) {
  291. options.push({
  292. label: data[i],
  293. value: data[i],
  294. disabled: false,
  295. selected: false,
  296. show: true,
  297. });
  298. }
  299. }
  300. return options;
  301. },
  302. /**
  303. * The logic of process original data
  304. * @param {Array} data
  305. * @return {Array}
  306. */
  307. processData(data) {
  308. return this.processDefault(data);
  309. },
  310. /**
  311. * The logic of deselect option when multiple is true
  312. * @param {Object} option
  313. * @param {Number} index
  314. */
  315. mulDeselectOption(option, index) {
  316. return new Promise((resolve) => {
  317. const indexTemp = this.indexes.indexOf(index);
  318. this.indexes.splice(indexTemp, 1);
  319. option.selected = false;
  320. this.$nextTick(() => {
  321. resolve(true);
  322. });
  323. });
  324. },
  325. /**
  326. * The logic of select option when multiple is true
  327. * @param {Object} option
  328. * @param {Number} index
  329. */
  330. mulSelectOption(option, index) {
  331. this.indexes.push(index);
  332. option.selected = true;
  333. },
  334. /**
  335. * The logic of click option
  336. * @param {Object} option
  337. * @param {Number} index
  338. */
  339. optionClick(option, index) {
  340. if (option.disabled) {
  341. return;
  342. }
  343. if (option.selected) {
  344. if (this.multiple) {
  345. this.mulDeselectOption(option, index);
  346. } else {
  347. // Single choice not allowed to deselect directly, do this by selecting other options
  348. return;
  349. }
  350. } else {
  351. if (this.multiple) {
  352. this.mulSelectOption(option, index);
  353. } else {
  354. this.indexes[0] = index;
  355. // When multiple is false, there is at most one option can have 'selected' true
  356. if (typeof this.latestIndex === 'number') {
  357. this.options[this.latestIndex].selected = false;
  358. }
  359. this.latestIndex = index;
  360. return;
  361. }
  362. }
  363. },
  364. /**
  365. * The logic of click cancel icon
  366. * @param {Object} event
  367. * @param {Number} index
  368. */
  369. cancelLabel(event, index) {
  370. event.stopPropagation();
  371. event.preventDefault();
  372. this.mulDeselectOption(this.options[index], index).then(() => {
  373. if (!this.ifActive) {
  374. this.$emit('cancelLabel');
  375. }
  376. });
  377. },
  378. },
  379. created() {
  380. if (this.source.length > 0) {
  381. this.options = this.processData(this.source);
  382. this.ifEmpty = false;
  383. }
  384. this.activeKey = publicStore.activeKey;
  385. window.addEventListener('click', this.clickHandler);
  386. },
  387. mounted() {
  388. // Calculate position and width of options container
  389. if (this.$refs.selector) {
  390. const styleList = getComputedStyle(this.$refs.selector);
  391. const height = styleList['height'].replace('px', '');
  392. const minWidth = styleList['width'];
  393. this.containerTop = `${parseInt(height) + this.containerToSelector}px`;
  394. this.containerWidth = minWidth;
  395. }
  396. },
  397. watch: {
  398. // The watcher of filter,to filter options and control ifEmpty after filtered
  399. filter(newVal) {
  400. if (newVal !== '') {
  401. this.functionAble = false;
  402. } else {
  403. this.functionAble = true;
  404. }
  405. clearTimeout(this.filterTimer);
  406. this.filterTimer = setTimeout(() => {
  407. for (let i = 0; i < this.options.length; i++) {
  408. if (this.options[i].label.indexOf(newVal) < 0) {
  409. this.options[i].show = false;
  410. } else {
  411. this.options[i].show = true;
  412. }
  413. }
  414. this.$nextTick(() => {
  415. if (this.$refs.options) {
  416. if (getComputedStyle(this.$refs.options)['height'] === '0px') {
  417. this.ifEmpty = true;
  418. } else {
  419. this.ifEmpty = false;
  420. }
  421. }
  422. });
  423. }, this.filterDebounce);
  424. },
  425. // The watcher of source can process asynchronous data input, or make response when original data changed
  426. 'source': {
  427. handler(newVal) {
  428. this.indexes = [];
  429. this.options = this.processData(newVal);
  430. this.ifEmpty = false;
  431. },
  432. },
  433. // The watcher of activeKey to keep the selector in right state
  434. 'activeKey.key': {
  435. handler(newVal) {
  436. if (newVal !== this.key) {
  437. this.ifFocus = false;
  438. this.ifActive = false;
  439. }
  440. },
  441. },
  442. 'indexes': {
  443. handler() {
  444. this.$nextTick(() => {
  445. this.$emit('selectedUpdate', this.calValues());
  446. });
  447. },
  448. },
  449. },
  450. beforeDestroy() {
  451. window.removeEventListener('click', this.clickHandler);
  452. },
  453. };
  454. </script>
  455. <style>
  456. .cl-search-select .filter-container .el-input {
  457. width: 0;
  458. flex-grow: 1;
  459. }
  460. .cl-search-select .filter-container .el-input .el-input__inner {
  461. padding: 0 9px;
  462. }
  463. </style>
  464. <style scoped>
  465. .cl-search-select {
  466. height: 100%;
  467. width: 100%;
  468. position: relative;
  469. }
  470. .cl-search-select .cl-search-select-inner {
  471. height: 100%;
  472. border: 1px solid #dcdfe6;
  473. border-radius: 1px;
  474. background-color: #fff;
  475. color: #606266;
  476. padding: 0 15px;
  477. cursor: pointer;
  478. display: flex;
  479. align-items: center;
  480. }
  481. .cl-search-select .is-focus {
  482. border-color: #00a5a7;
  483. }
  484. .cl-search-select .mul-tag {
  485. height: 24px;
  486. padding: 0 4px 0 8px;
  487. border: 1px solid #d9ecff;
  488. border-radius: 4px;
  489. background-color: #f4f4f5;
  490. border-color: #e9e9eb;
  491. margin-right: 6px;
  492. max-width: 70%;
  493. display: flex;
  494. align-items: center;
  495. }
  496. .cl-search-select .mul-tag .mul-tag-content {
  497. font-size: 12px;
  498. white-space: nowrap;
  499. overflow: hidden;
  500. color: #909399;
  501. margin-right: 4px;
  502. }
  503. .cl-search-select .mul-tag .mul-tag-del {
  504. font-size: 12px;
  505. background-color: #c0c4cc;
  506. min-height: 12.8px;
  507. min-width: 12.8px;
  508. display: flex;
  509. align-items: center;
  510. justify-content: center;
  511. border-radius: 50%;
  512. }
  513. .cl-search-select .mul-tag .mul-tag-del:hover {
  514. background-color: #909399;
  515. }
  516. .cl-search-select .mul-tag .mul-tag-del .el-icon-close {
  517. color: #909399;
  518. }
  519. .cl-search-select .mul-tag .mul-tag-del .el-icon-close:hover {
  520. color: #fff;
  521. }
  522. .cl-search-select .single-tag {
  523. flex-wrap: nowrap;
  524. overflow: hidden;
  525. }
  526. .cl-search-select .select-container {
  527. position: absolute;
  528. border: 1px solid #e4e7ed;
  529. border-radius: 4px;
  530. background-color: #fff;
  531. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  532. box-sizing: border-box;
  533. margin: 5px 0;
  534. display: flex;
  535. flex-direction: column;
  536. max-height: 244px;
  537. z-index: 9998;
  538. }
  539. .cl-search-select .select-container .filter-container {
  540. margin-top: 4px;
  541. flex-shrink: 0;
  542. height: 40px;
  543. display: flex;
  544. align-items: center;
  545. padding: 0 10px;
  546. }
  547. .cl-search-select .select-container .filter-container .has-gap {
  548. margin-right: 6px;
  549. }
  550. .cl-search-select .select-container .filter-container .able {
  551. color: #00a5a7;
  552. cursor: pointer;
  553. }
  554. .cl-search-select .select-container .filter-container .disable {
  555. color: #c3c3c3;
  556. cursor: not-allowed;
  557. }
  558. .cl-search-select .select-container .option-container {
  559. overflow-x: hidden;
  560. overflow-y: scroll;
  561. }
  562. .cl-search-select .select-container .option-container .select-option {
  563. padding: 0 20px;
  564. height: 34px;
  565. display: flex;
  566. align-items: center;
  567. justify-content: space-between;
  568. cursor: pointer;
  569. }
  570. .cl-search-select .select-container .option-container .select-option:hover {
  571. background-color: #f5f7fa;
  572. }
  573. .cl-search-select
  574. .select-container
  575. .option-container
  576. .select-option
  577. .label-container {
  578. white-space: nowrap;
  579. max-width: 320px;
  580. text-overflow: ellipsis;
  581. overflow: hidden;
  582. }
  583. .cl-search-select
  584. .select-container
  585. .option-container
  586. .select-option
  587. .icon-container {
  588. width: 14px;
  589. }
  590. .cl-search-select
  591. .select-container
  592. .option-container
  593. .select-option
  594. .icon-container
  595. .icon-no-selected {
  596. display: none;
  597. }
  598. .cl-search-select .select-container .option-container .is-selected {
  599. color: #00a5a7;
  600. }
  601. .cl-search-select .select-container .option-container .is-disabled {
  602. color: #c0c4cc;
  603. cursor: not-allowed !important;
  604. }
  605. .cl-search-select .select-container .option-container::-webkit-scrollbar {
  606. cursor: pointer;
  607. width: 6px;
  608. }
  609. .cl-search-select .select-container .option-container::-webkit-scrollbar-track {
  610. -webkit-box-shadow: inset 0 0 6px #fff;
  611. background-color: #fff;
  612. border-radius: 3px;
  613. }
  614. .cl-search-select .select-container .option-container::-webkit-scrollbar-thumb {
  615. border-radius: 7px;
  616. -webkit-box-shadow: inset 0 0 6px rgba(144, 147, 153, 0.3);
  617. background-color: #e8e8e8;
  618. }
  619. .cl-search-select
  620. .select-container
  621. .option-container::-webkit-scrollbar-thumb:hover {
  622. -webkit-box-shadow: inset 0 0 6px rgba(144, 147, 153, 0.3);
  623. background-color: #cacaca;
  624. border-radius: 3px;
  625. }
  626. .cl-search-select .select-container .option-empty {
  627. height: 34px;
  628. display: flex;
  629. align-items: center;
  630. justify-content: center;
  631. }
  632. </style>