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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  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 = {activeKey: {key: ''}};
  110. export default {
  111. props: {
  112. multiple: {
  113. type: Boolean,
  114. default: false,
  115. }, // If open multiple choice
  116. sPlaceholder: {
  117. type: String,
  118. default: '',
  119. }, // The placeholder of selector
  120. iPlaceholder: {
  121. type: String,
  122. default: '',
  123. }, // The placeholder of filter input
  124. overflow: {
  125. type: String,
  126. default: 'none',
  127. }, // The display way of overflow selected label('none' or 'ellipsis')
  128. cancel: {
  129. type: Boolean,
  130. default: true,
  131. }, // If the tag can be cancel by icon, only effective when multiple is true
  132. plain: {
  133. type: Boolean,
  134. default: false,
  135. }, // Represents whether the type of the incoming array, 'true' stands for 'String', 'false' stands for 'Object'
  136. source: {
  137. type: Array,
  138. default: () => {
  139. return [];
  140. },
  141. }, // The total data source of component
  142. labelName: {
  143. type: String,
  144. default: 'label',
  145. }, // The name of the property representing label in the object
  146. valueName: {
  147. type: String,
  148. default: 'value',
  149. }, // The name of the property representing value in the object
  150. disabledName: {
  151. type: String,
  152. default: 'disabled',
  153. }, // The name of the property representing disabled in the object
  154. selectedName: {
  155. type: String,
  156. default: 'selected',
  157. }, // The name of the property representing selected in the object
  158. },
  159. data() {
  160. return {
  161. ifFocus: false, // When the selector is focused, the options area may not display
  162. ifActive: false, // When the selector is active, the options area must display
  163. options: [], // The option list after processing original data
  164. containerTop: 0, // The top to set container position
  165. containerLeft: 0, // The left to set container position
  166. containerWidth: 0, // The min-width of container
  167. containerToSelector: 12, // The gap between options-container and selector
  168. functionAble: true, // The effective of button after input
  169. // The key of component to make sure it can be distinguished from PublicStore
  170. key: new Date().getTime().toString(),
  171. filter: '', // The value to filter the option
  172. ifEmpty: true, // If no option match the filter
  173. activeKey: undefined, // In order to add wacter to the PublicStore.activeKey.key
  174. latestIndex: undefined, // The index of the last click option, only effective when multiple is false
  175. indexes: [], // The list of index of selected options
  176. filterDebounce: 150, // The filter watcher debounce time in ms
  177. };
  178. },
  179. methods: {
  180. /**
  181. * The logic of calculate values or value
  182. * @return {Array | String | Number}
  183. */
  184. calValues() {
  185. if (this.multiple) {
  186. const values = [];
  187. for (let i = 0; i < this.indexes.length; i++) {
  188. values.push(this.options[this.indexes[i]].value);
  189. }
  190. return values;
  191. } else {
  192. return this.options[this.indexes[0]].value;
  193. }
  194. },
  195. /**
  196. * The logic of enter down
  197. */
  198. enter() {
  199. this.ifFocus = false;
  200. this.ifActive = false;
  201. this.$emit('selectEnter');
  202. },
  203. /**
  204. * The logic of click the display area of selector, excluding the options area
  205. */
  206. click() {
  207. this.ifActive = !this.ifActive;
  208. this.ifFocus = true;
  209. },
  210. /**
  211. * The logic of click event that add to window, which can make response to defocus
  212. */
  213. clickHandler() {
  214. this.ifFocus = false;
  215. this.ifActive = false;
  216. },
  217. /**
  218. * The logic of click the selector, including everywhere
  219. * @param {Object} event
  220. */
  221. mouseClick(event) {
  222. PublicStore.activeKey.key = this.key;
  223. event.stopPropagation();
  224. event.preventDefault();
  225. },
  226. /**
  227. * The logic of click selectAll button
  228. */
  229. selectAll() {
  230. if (!this.functionAble) {
  231. return;
  232. }
  233. this.indexes = [];
  234. for (let i = 0; i < this.options.length; i++) {
  235. if (this.options[i].disabled) {
  236. if (this.options[i].selected) {
  237. this.indexes.push(i);
  238. }
  239. } else {
  240. this.mulSelectOption(this.options[i], i);
  241. }
  242. }
  243. },
  244. /**
  245. * The logic of click clearAll button
  246. */
  247. clearAll() {
  248. if (!this.functionAble) {
  249. return;
  250. }
  251. const indexes = [];
  252. for (let i = 0; i < this.indexes.length; i++) {
  253. const option = this.options[this.indexes[i]];
  254. if (option.disabled) {
  255. indexes.push(i);
  256. } else {
  257. option.selected = false;
  258. }
  259. }
  260. this.indexes = indexes;
  261. },
  262. /**
  263. * The logic of process original data when group is false
  264. * @param {Array} data
  265. * @return {Array}
  266. */
  267. processDefault(data) {
  268. const options = [];
  269. if (!this.plain) {
  270. for (let i = 0; i < data.length; i++) {
  271. options.push({
  272. label: data[i][this.labelName],
  273. value: data[i][this.valueName],
  274. disabled: data[i][this.disabledName] === true ? true : false,
  275. selected: data[i][this.selectedName] === true ? true : false,
  276. show: true,
  277. });
  278. if (options[i].selected) {
  279. this.indexes.push(i);
  280. }
  281. }
  282. } else {
  283. for (let i = 0; i < data.length; i++) {
  284. options.push({
  285. label: data[i],
  286. value: data[i],
  287. disabled: false,
  288. selected: false,
  289. show: true,
  290. });
  291. }
  292. }
  293. return options;
  294. },
  295. /**
  296. * The logic of process original data
  297. * @param {Array} data
  298. * @return {Array}
  299. */
  300. processData(data) {
  301. return this.processDefault(data);
  302. },
  303. /**
  304. * The logic of deselect option when multiple is true
  305. * @param {Object} option
  306. * @param {Number} index
  307. */
  308. mulDeselectOption(option, index) {
  309. const indexTemp = this.indexes.indexOf(index);
  310. this.indexes.splice(indexTemp, 1);
  311. option.selected = false;
  312. },
  313. /**
  314. * The logic of select option when multiple is true
  315. * @param {Object} option
  316. * @param {Number} index
  317. */
  318. mulSelectOption(option, index) {
  319. this.indexes.push(index);
  320. option.selected = true;
  321. },
  322. /**
  323. * The logic of click option
  324. * @param {Object} option
  325. * @param {Number} index
  326. */
  327. optionClick(option, index) {
  328. if (option.disabled) {
  329. return;
  330. }
  331. if (option.selected) {
  332. if (this.multiple) {
  333. this.mulDeselectOption(option, index);
  334. } else {
  335. // Single choice not allowed to deselect directly, do this by selecting other options
  336. return;
  337. }
  338. } else {
  339. if (this.multiple) {
  340. this.mulSelectOption(option, index);
  341. } else {
  342. this.indexes[0] = index;
  343. // When multiple is false, there is at most one option can have 'selected' true
  344. if (typeof this.latestIndex === 'number') {
  345. this.options[this.latestIndex].selected = false;
  346. }
  347. this.latestIndex = index;
  348. return;
  349. }
  350. }
  351. },
  352. /**
  353. * The logic of click cancel icon
  354. * @param {Object} event
  355. * @param {Number} index
  356. */
  357. cancelLabel(event, index) {
  358. event.stopPropagation();
  359. event.preventDefault();
  360. this.mulDeselectOption(this.options[index], index);
  361. },
  362. },
  363. created() {
  364. if (this.source.length > 0) {
  365. this.options = this.processData(this.source);
  366. this.ifEmpty = false;
  367. }
  368. this.activeKey = PublicStore.activeKey;
  369. window.addEventListener('click', this.clickHandler);
  370. },
  371. mounted() {
  372. // Calculate position and width of options container
  373. if (this.$refs.selector) {
  374. const styleList = getComputedStyle(this.$refs.selector);
  375. const height = styleList['height'].replace('px', '');
  376. const minWidth = styleList['width'];
  377. this.containerTop = `${parseInt(height) + this.containerToSelector}px`;
  378. this.containerWidth = minWidth;
  379. }
  380. },
  381. watch: {
  382. // The watcher of filter,to filter options and control ifEmpty after filtered
  383. filter(newVal) {
  384. if (newVal !== '') {
  385. this.functionAble = false;
  386. } else {
  387. this.functionAble = true;
  388. }
  389. clearTimeout(this.filterTimer);
  390. this.filterTimer = setTimeout(() => {
  391. for (let i = 0; i < this.options.length; i++) {
  392. if (this.options[i].label.indexOf(newVal) < 0) {
  393. this.options[i].show = false;
  394. } else {
  395. this.options[i].show = true;
  396. }
  397. }
  398. this.$nextTick(() => {
  399. if (this.$refs.options) {
  400. if (getComputedStyle(this.$refs.options)['height'] === '0px') {
  401. this.ifEmpty = true;
  402. } else {
  403. this.ifEmpty = false;
  404. }
  405. }
  406. });
  407. }, this.filterDebounce);
  408. },
  409. // The watcher of source can process asynchronous data input, or make response when original data changed
  410. 'source': {
  411. handler(newVal) {
  412. this.indexes = [];
  413. this.options = this.processData(newVal);
  414. this.ifEmpty = false;
  415. },
  416. },
  417. // The watcher of activeKey to keep the selector in right state
  418. 'activeKey.key': {
  419. handler(newVal) {
  420. if (newVal !== this.key) {
  421. this.ifFocus = false;
  422. this.ifActive = false;
  423. }
  424. },
  425. },
  426. 'indexes': {
  427. handler() {
  428. this.$nextTick(() => {
  429. this.$emit('selectedUpdate', this.calValues());
  430. });
  431. },
  432. },
  433. },
  434. beforeDestroy() {
  435. window.removeEventListener('click', this.clickHandler);
  436. },
  437. };
  438. </script>
  439. <style>
  440. .cl-search-select .filter-container .el-input {
  441. width: 0;
  442. flex-grow: 1;
  443. }
  444. .cl-search-select .filter-container .el-input .el-input__inner {
  445. padding: 0 9px;
  446. }
  447. </style>
  448. <style scoped>
  449. .cl-search-select {
  450. height: 100%;
  451. width: 100%;
  452. position: relative;
  453. }
  454. .cl-search-select .cl-search-select-inner {
  455. height: 100%;
  456. border: 1px solid #dcdfe6;
  457. border-radius: 1px;
  458. background-color: #fff;
  459. color: #606266;
  460. padding: 0 15px;
  461. cursor: pointer;
  462. display: flex;
  463. align-items: center;
  464. }
  465. .cl-search-select .is-focus {
  466. border-color: #00a5a7;
  467. }
  468. .cl-search-select .mul-tag {
  469. height: 24px;
  470. padding: 0 4px 0 8px;
  471. border: 1px solid #d9ecff;
  472. border-radius: 4px;
  473. background-color: #f4f4f5;
  474. border-color: #e9e9eb;
  475. margin-right: 6px;
  476. max-width: 70%;
  477. display: flex;
  478. align-items: center;
  479. }
  480. .cl-search-select .mul-tag .mul-tag-content {
  481. font-size: 12px;
  482. white-space: nowrap;
  483. overflow: hidden;
  484. color: #909399;
  485. margin-right: 4px;
  486. }
  487. .cl-search-select .mul-tag .mul-tag-del {
  488. font-size: 12px;
  489. background-color: #c0c4cc;
  490. min-height: 12.8px;
  491. min-width: 12.8px;
  492. display: flex;
  493. align-items: center;
  494. justify-content: center;
  495. border-radius: 50%;
  496. }
  497. .cl-search-select .mul-tag .mul-tag-del:hover {
  498. background-color: #909399;
  499. }
  500. .cl-search-select .mul-tag .mul-tag-del .el-icon-close {
  501. color: #909399;
  502. }
  503. .cl-search-select .mul-tag .mul-tag-del .el-icon-close:hover {
  504. color: #fff;
  505. }
  506. .cl-search-select .single-tag {
  507. flex-wrap: nowrap;
  508. overflow: hidden;
  509. }
  510. .cl-search-select .select-container {
  511. position: absolute;
  512. border: 1px solid #e4e7ed;
  513. border-radius: 4px;
  514. background-color: #fff;
  515. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  516. box-sizing: border-box;
  517. margin: 5px 0;
  518. display: flex;
  519. flex-direction: column;
  520. max-height: 244px;
  521. z-index: 9998;
  522. }
  523. .cl-search-select .select-container .filter-container {
  524. margin-top: 4px;
  525. flex-shrink: 0;
  526. height: 40px;
  527. display: flex;
  528. align-items: center;
  529. padding: 0 10px;
  530. }
  531. .cl-search-select .select-container .filter-container .has-gap {
  532. margin-right: 6px;
  533. }
  534. .cl-search-select .select-container .filter-container .able {
  535. color: #00a5a7;
  536. cursor: pointer;
  537. }
  538. .cl-search-select .select-container .filter-container .disable {
  539. color: #c3c3c3;
  540. cursor: not-allowed;
  541. }
  542. .cl-search-select .select-container .option-container {
  543. overflow-x: hidden;
  544. overflow-y: scroll;
  545. }
  546. .cl-search-select .select-container .option-container .select-option {
  547. padding: 0 20px;
  548. height: 34px;
  549. display: flex;
  550. align-items: center;
  551. justify-content: space-between;
  552. cursor: pointer;
  553. }
  554. .cl-search-select .select-container .option-container .select-option:hover {
  555. background-color: #f5f7fa;
  556. }
  557. .cl-search-select .select-container .option-container .select-option .label-container {
  558. white-space: nowrap;
  559. max-width: 320px;
  560. text-overflow: ellipsis;
  561. overflow: hidden;
  562. }
  563. .cl-search-select .select-container .option-container .select-option .icon-container {
  564. width: 14px;
  565. }
  566. .cl-search-select .select-container .option-container .select-option .icon-container .icon-no-selected {
  567. display: none;
  568. }
  569. .cl-search-select .select-container .option-container .is-selected {
  570. color: #00a5a7;
  571. }
  572. .cl-search-select .select-container .option-container .is-disabled {
  573. color: #c0c4cc;
  574. cursor: not-allowed !important;
  575. }
  576. .cl-search-select .select-container .option-container::-webkit-scrollbar {
  577. cursor: pointer;
  578. width: 6px;
  579. }
  580. .cl-search-select .select-container .option-container::-webkit-scrollbar-track {
  581. -webkit-box-shadow: inset 0 0 6px #fff;
  582. background-color: #fff;
  583. border-radius: 3px;
  584. }
  585. .cl-search-select .select-container .option-container::-webkit-scrollbar-thumb {
  586. border-radius: 7px;
  587. -webkit-box-shadow: inset 0 0 6px rgba(144, 147, 153, 0.3);
  588. background-color: #e8e8e8;
  589. }
  590. .cl-search-select .select-container .option-container::-webkit-scrollbar-thumb:hover {
  591. -webkit-box-shadow: inset 0 0 6px rgba(144, 147, 153, 0.3);
  592. background-color: #cacaca;
  593. border-radius: 3px;
  594. }
  595. .cl-search-select .select-container .option-empty {
  596. height: 34px;
  597. display: flex;
  598. align-items: center;
  599. justify-content: center;
  600. }
  601. </style>