# 一.Cascader

  • element-plus
    • packages
      • components
        • cascader 【Cascader 级联选择器】
          • __tests__
            • cascader.spec.ts
              测试用例
              import { nextTick } from "vue"
              import { mount } from "@vue/test-utils"
              import Cascader from "../src/index.vue"
              
              const OPTIONS = [
                {
                  value: "zhejiang",
                  label: "Zhejiang",
                  children: [
                    {
                      value: "hangzhou",
                      label: "Hangzhou",
                    },
                    {
                      value: "ningbo",
                      label: "Ningbo",
                    },
                  ],
                },
              ]
              
              const AXIOM = "Rem is the best girl"
              
              const TRIGGER = ".el-cascader"
              const DROPDOWN = ".el-cascader__dropdown"
              const NODE = ".el-cascader-node"
              const ARROW = ".el-icon-arrow-down"
              const CLEAR_BTN = ".el-icon-circle-close"
              const TAG = ".el-tag"
              const SUGGESTION_ITEM = ".el-cascader__suggestion-item"
              const CHECK_ICON = ".el-icon-check"
              
              const _mount: typeof mount = (options) =>
                mount(
                  {
                    components: {
                      Cascader,
                    },
                    ...options,
                  },
                  {
                    attachTo: "body",
                  }
                )
              
              afterEach(() => {
                document.body.innerHTML = ""
              })
              
              describe("Cascader.vue", () => {
                test("toggle popper visible", async () => {
                  const handleVisibleChange = jest.fn()
                  const wrapper = _mount({
                    template: `
                     <cascader @visible-change="handleVisibleChange" />
                   `,
                    methods: {
                      handleVisibleChange,
                    },
                  })
                  const trigger = wrapper.find(TRIGGER)
                  const dropdown = document.querySelector(
                    DROPDOWN
                  ) as HTMLDivElement
              
                  await trigger.trigger("click")
                  expect(dropdown.style.display).not.toBe("none")
                  expect(handleVisibleChange).toBeCalledWith(true)
                  await trigger.trigger("click")
                  expect(handleVisibleChange).toBeCalledWith(false)
                  await trigger.trigger("click")
                  document.body.click()
                  expect(handleVisibleChange).toBeCalledWith(false)
                })
              
                test("expand and check", async () => {
                  const handleChange = jest.fn()
                  const handleExpandChange = jest.fn()
                  const wrapper = _mount({
                    template: `
                     <cascader
                       v-model="value"
                       :options="options"
                       @change="handleChange"
                       @expand-change="handleExpandChange"
                     />
                   `,
                    data() {
                      return {
                        value: [],
                        options: OPTIONS,
                      }
                    },
                    methods: {
                      handleChange,
                      handleExpandChange,
                    },
                  })
                  const trigger = wrapper.find(TRIGGER)
                  const dropdown = document.querySelector(
                    DROPDOWN
                  ) as HTMLDivElement
                  const vm = wrapper.vm as any
              
                  await trigger.trigger("click")
                  ;(dropdown.querySelector(
                    NODE
                  ) as HTMLElement).click()
                  await nextTick()
                  expect(handleExpandChange).toBeCalledWith([
                    "zhejiang",
                  ])
                  ;(dropdown.querySelectorAll(
                    NODE
                  )[1] as HTMLElement).click()
                  await nextTick()
                  expect(handleChange).toBeCalledWith([
                    "zhejiang",
                    "hangzhou",
                  ])
                  expect(vm.value).toEqual(["zhejiang", "hangzhou"])
                  expect(wrapper.find("input").element.value).toBe(
                    "Zhejiang / Hangzhou"
                  )
                })
              
                test("with default value", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      modelValue: ["zhejiang", "hangzhou"],
                    },
                  })
                  await nextTick()
                  expect(wrapper.find("input").element.value).toBe(
                    "Zhejiang / Hangzhou"
                  )
                  await wrapper.setProps({
                    modelValue: ["zhejiang", "ningbo"],
                  })
                  expect(wrapper.find("input").element.value).toBe(
                    "Zhejiang / Ningbo"
                  )
                })
              
                test("options change", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      modelValue: ["zhejiang", "hangzhou"],
                    },
                  })
                  await wrapper.setProps({ options: [] })
                  expect(wrapper.find("input").element.value).toBe("")
                })
              
                test("disabled", async () => {
                  const handleVisibleChange = jest.fn()
                  const wrapper = _mount({
                    template: `
                     <cascader disabled @visible-change="handleVisibleChange" />
                   `,
                    methods: {
                      handleVisibleChange,
                    },
                  })
                  await wrapper.find(TRIGGER).trigger("click")
                  expect(handleVisibleChange).not.toBeCalled()
                  expect(
                    wrapper.find("input[disabled]").exists()
                  ).toBe(true)
                })
              
                test("custom placeholder", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      placeholder: AXIOM,
                    },
                  })
                  expect(
                    wrapper.find("input").attributes().placeholder
                  ).toBe(AXIOM)
                })
              
                test("clearable", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      clearable: true,
                      modelValue: ["zhejiang", "hangzhou"],
                    },
                  })
                  const trigger = wrapper.find(TRIGGER)
                  expect(wrapper.find(ARROW).exists()).toBe(true)
                  await trigger.trigger("mouseenter")
                  expect(wrapper.find(ARROW).exists()).toBe(false)
                  await wrapper.find(CLEAR_BTN).trigger("click")
                  expect(wrapper.find("input").element.value).toBe("")
                  expect(
                    (wrapper.vm as any).getCheckedNodes().length
                  ).toBe(0)
                  await trigger.trigger("mouseleave")
                  await trigger.trigger("mouseenter")
                  await expect(wrapper.find(CLEAR_BTN).exists()).toBe(
                    false
                  )
                })
              
                test("show last level label", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      showAllLevels: false,
                      modelValue: ["zhejiang", "hangzhou"],
                    },
                  })
                  await nextTick()
                  expect(wrapper.find("input").element.value).toBe(
                    "Hangzhou"
                  )
                })
              
                test("multiple mode", async () => {
                  const wrapper = _mount({
                    template: `
                     <cascader
                       v-model="value"
                       :options="options"
                       :props="props"
                     />
                   `,
                    data() {
                      return {
                        options: OPTIONS,
                        props: { multiple: true },
                        value: [
                          ["zhejiang", "hangzhou"],
                          ["zhejiang", "ningbo"],
                        ],
                      }
                    },
                  })
                  await nextTick()
                  const tags = wrapper.findAll(TAG)
                  const [firstTag, secondTag] = tags
                  expect(tags.length).toBe(2)
                  expect(firstTag.text()).toBe("Zhejiang / Hangzhou")
                  expect(secondTag.text()).toBe("Zhejiang / Ningbo")
                  await firstTag
                    .find(".el-tag__close")
                    .trigger("click")
                  expect(wrapper.findAll(TAG).length).toBe(1)
                  expect(wrapper.vm.value).toEqual([
                    ["zhejiang", "ningbo"],
                  ])
                })
              
                test("collapse tags", async () => {
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      props: { multiple: true },
                      collapseTags: true,
                      modelValue: [
                        ["zhejiang", "hangzhou"],
                        ["zhejiang", "ningbo"],
                      ],
                    },
                  })
                  await nextTick()
                  const [firstTag, secondTag] = wrapper.findAll(TAG)
                  expect(firstTag.text()).toBe("Zhejiang / Hangzhou")
                  expect(secondTag.text()).toBe("+ 1")
                })
              
                test("filterable", async () => {
                  const wrapper = _mount({
                    template: `
                     <cascader
                       v-model="value"
                       :options="options"
                       filterable
                     />
                   `,
                    data() {
                      return {
                        options: OPTIONS,
                        value: [],
                      }
                    },
                  })
              
                  const input = wrapper.find("input")
                  const dropdown = document.querySelector(DROPDOWN)
                  input.element.value = "Ha"
                  await input.trigger("input")
                  const suggestions = dropdown.querySelectorAll(
                    SUGGESTION_ITEM
                  ) as NodeListOf<HTMLElement>
                  const hzSuggestion = suggestions[0]
                  expect(suggestions.length).toBe(1)
                  expect(hzSuggestion.textContent).toBe(
                    "Zhejiang / Hangzhou"
                  )
                  hzSuggestion.click()
                  await nextTick()
                  expect(
                    hzSuggestion.querySelector(CHECK_ICON)
                  ).toBeTruthy()
                  expect(wrapper.vm.value).toEqual([
                    "zhejiang",
                    "hangzhou",
                  ])
                  hzSuggestion.click()
                  await nextTick()
                  expect(wrapper.vm.value).toEqual([
                    "zhejiang",
                    "hangzhou",
                  ])
                })
              
                test("filterable in multiple mode", async () => {
                  const wrapper = _mount({
                    template: `
                     <cascader
                       v-model="value"
                       :options="options"
                       :props="props"
                       filterable
                     />
                   `,
                    data() {
                      return {
                        options: OPTIONS,
                        props: { multiple: true },
                        value: [],
                      }
                    },
                  })
              
                  const input = wrapper.find(
                    ".el-cascader__search-input"
                  )
                  const dropdown = document.querySelector(DROPDOWN)
                  ;(input.element as HTMLInputElement).value = "Ha"
                  await input.trigger("input")
                  await nextTick()
                  const hzSuggestion = dropdown.querySelector(
                    SUGGESTION_ITEM
                  ) as HTMLElement
                  hzSuggestion.click()
                  await nextTick()
                  expect(wrapper.vm.value).toEqual([
                    ["zhejiang", "hangzhou"],
                  ])
                  hzSuggestion.click()
                  await nextTick()
                  expect(wrapper.vm.value).toEqual([])
                })
              
                test("filter method", async () => {
                  const filterMethod = jest.fn((node, keyword) => {
                    const { text, value } = node
                    return (
                      text.includes(keyword) ||
                      value.includes(keyword)
                    )
                  })
                  const wrapper = mount(Cascader, {
                    props: {
                      options: OPTIONS,
                      filterable: true,
                      filterMethod,
                    },
                  })
              
                  const input = wrapper.find("input")
                  const dropdown = document.querySelector(DROPDOWN)
                  input.element.value = "ha"
                  await input.trigger("input")
                  const hzSuggestion = dropdown.querySelector(
                    SUGGESTION_ITEM
                  ) as HTMLElement
                  expect(filterMethod).toBeCalled()
                  expect(hzSuggestion.textContent).toBe(
                    "Zhejiang / Hangzhou"
                  )
                })
              })
              
              1
              2
              3
              4
              5
              6
              7
              8
              9
              10
              11
              12
              13
              14
              15
              16
              17
              18
              19
              20
              21
              22
              23
              24
              25
              26
              27
              28
              29
              30
              31
              32
              33
              34
              35
              36
              37
              38
              39
              40
              41
              42
              43
              44
              45
              46
              47
              48
              49
              50
              51
              52
              53
              54
              55
              56
              57
              58
              59
              60
              61
              62
              63
              64
              65
              66
              67
              68
              69
              70
              71
              72
              73
              74
              75
              76
              77
              78
              79
              80
              81
              82
              83
              84
              85
              86
              87
              88
              89
              90
              91
              92
              93
              94
              95
              96
              97
              98
              99
              100
              101
              102
              103
              104
              105
              106
              107
              108
              109
              110
              111
              112
              113
              114
              115
              116
              117
              118
              119
              120
              121
              122
              123
              124
              125
              126
              127
              128
              129
              130
              131
              132
              133
              134
              135
              136
              137
              138
              139
              140
              141
              142
              143
              144
              145
              146
              147
              148
              149
              150
              151
              152
              153
              154
              155
              156
              157
              158
              159
              160
              161
              162
              163
              164
              165
              166
              167
              168
              169
              170
              171
              172
              173
              174
              175
              176
              177
              178
              179
              180
              181
              182
              183
              184
              185
              186
              187
              188
              189
              190
              191
              192
              193
              194
              195
              196
              197
              198
              199
              200
              201
              202
              203
              204
              205
              206
              207
              208
              209
              210
              211
              212
              213
              214
              215
              216
              217
              218
              219
              220
              221
              222
              223
              224
              225
              226
              227
              228
              229
              230
              231
              232
              233
              234
              235
              236
              237
              238
              239
              240
              241
              242
              243
              244
              245
              246
              247
              248
              249
              250
              251
              252
              253
              254
              255
              256
              257
              258
              259
              260
              261
              262
              263
              264
              265
              266
              267
              268
              269
              270
              271
              272
              273
              274
              275
              276
              277
              278
              279
              280
              281
              282
              283
              284
              285
              286
              287
              288
              289
              290
              291
              292
              293
              294
              295
              296
              297
              298
              299
              300
              301
              302
              303
              304
              305
              306
              307
              308
              309
              310
              311
              312
              313
              314
              315
              316
              317
              318
              319
              320
              321
              322
              323
              324
              325
              326
              327
              328
              329
              330
              331
              332
              333
              334
              335
              336
              337
              338
              339
              340
              341
              342
              343
              344
              345
              346
              347
              348
              349
              350
              351
              352
              353
              354
              355
              356
              357
              358
              359
              360
              361
              362
              363
              364
              365
              366
              367
              368
              369
              370
              371
              372
              373
              374
              375
              376
              377
              378
              379
              380
              381
              382
              383
              384
              385
              386
              387
              388
              389
          • src
            • index.vue
              代码
              <template>
                <el-popper
                  ref="popper"
                  v-model:visible="popperVisible"
                  manual-mode
                  :append-to-body="popperAppendToBody"
                  placement="bottom-start"
                  :popper-class="
                    `el-cascader__dropdown ${popperClass}`
                  "
                  :popper-options="popperOptions"
                  :fallback-placements="[
                    'bottom-start',
                    'top-start',
                    'right',
                    'left',
                  ]"
                  :stop-popper-mouse-event="false"
                  transition="el-zoom-in-top"
                  :gpu-acceleration="false"
                  :effect="Effect.LIGHT"
                  pure
                  @after-leave="hideSuggestionPanel"
                >
                  <template #trigger>
                    <div
                      v-clickoutside:[popperPaneRef]="
                        () => togglePopperVisible(false)
                      "
                      :class="[
                        'el-cascader',
                        realSize && `el-cascader--${realSize}`,
                        { 'is-disabled': isDisabled },
                      ]"
                      @click="
                        () =>
                          togglePopperVisible(
                            readonly ? undefined : true
                          )
                      "
                      @keydown="handleKeyDown"
                      @mouseenter="inputHover = true"
                      @mouseleave="inputHover = false"
                    >
                      <el-input
                        ref="input"
                        v-model.trim="inputValue"
                        :placeholder="inputPlaceholder"
                        :readonly="readonly"
                        :disabled="isDisabled"
                        :validate-event="false"
                        :size="realSize"
                        :class="{ 'is-focus': popperVisible }"
                        @focus="(e) => $emit('focus', e)"
                        @blur="(e) => $emit('blur', e)"
                        @input="handleInput"
                      >
                        <template #suffix>
                          <i
                            v-if="clearBtnVisible"
                            key="clear"
                            class="el-input__icon el-icon-circle-close"
                            @click.stop="handleClear"
                          ></i>
                          <i
                            v-else
                            key="arrow-down"
                            :class="[
                              'el-input__icon',
                              'el-icon-arrow-down',
                              popperVisible && 'is-reverse',
                            ]"
                            @click.stop="togglePopperVisible()"
                          ></i>
                        </template>
                      </el-input>
              
                      <div
                        v-if="multiple"
                        ref="tagWrapper"
                        class="el-cascader__tags"
                      >
                        <el-tag
                          v-for="tag in presentTags"
                          :key="tag.key"
                          type="info"
                          :size="tagSize"
                          :hit="tag.hitState"
                          :closable="tag.closable"
                          disable-transitions
                          @close="deleteTag(tag)"
                        >
                          <span>{{ tag.text }}</span>
                        </el-tag>
                        <input
                          v-if="filterable && !isDisabled"
                          v-model.trim="searchInputValue"
                          type="text"
                          class="el-cascader__search-input"
                          :placeholder="
                            presentText ? '' : inputPlaceholder
                          "
                          @input="
                            (e) => handleInput(searchInputValue, e)
                          "
                          @click.stop="togglePopperVisible(true)"
                          @keydown.delete="handleDelete"
                        />
                      </div>
                    </div>
                  </template>
              
                  <template #default>
                    <el-cascader-panel
                      v-show="!filtering"
                      ref="panel"
                      v-model="checkedValue"
                      :options="options"
                      :props="props"
                      :border="false"
                      :render-label="$slots.default"
                      @expand-change="handleExpandChange"
                      @close="togglePopperVisible(false)"
                    />
                    <el-scrollbar
                      v-if="filterable"
                      v-show="filtering"
                      ref="suggestionPanel"
                      tag="ul"
                      class="el-cascader__suggestion-panel"
                      view-class="el-cascader__suggestion-list"
                    >
                      <template v-if="suggestions.length">
                        <li
                          v-for="item in suggestions"
                          :key="item.uid"
                          :class="[
                            'el-cascader__suggestion-item',
                            item.checked && 'is-checked',
                          ]"
                          :tabindex="-1"
                          @click="handleSuggestionClick(item)"
                        >
                          <span>{{ item.text }}</span>
                          <i
                            v-if="item.checked"
                            class="el-icon-check"
                          ></i>
                        </li>
                      </template>
                      <slot v-else name="empty">
                        <li class="el-cascader__empty-text">
                          {{ t("el.cascader.noMatch") }}
                        </li>
                      </slot>
                    </el-scrollbar>
                  </template>
                </el-popper>
              </template>
              
              <script lang="ts">
              import {
                computed,
                defineComponent,
                inject,
                nextTick,
                onMounted,
                onBeforeUnmount,
                Ref,
                ref,
                watch,
              } from 'vue'
              import { isPromise } from '@vue/shared'
              import debounce from 'lodash/debounce'
              
              import ElCascaderPanel, {
                CommonProps,
              } from '@element-plus/components/cascader-panel'
              import ElInput from '@element-plus/components/input'
              import ElPopper from '@element-plus/components/popper'
              import ElScrollbar from '@element-plus/components/scrollbar'
              import ElTag from '@element-plus/components/tag'
              import { elFormKey, elFormItemKey } from '@element-plus/tokens'
              
              import { ClickOutside as Clickoutside } from '@element-plus/directives'
              import { useLocaleInject } from '@element-plus/hooks'
              
              import { EVENT_CODE } from '@element-plus/utils/aria'
              import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
              import isServer from '@element-plus/utils/isServer'
              import { useGlobalConfig } from '@element-plus/utils/util'
              import {
                addResizeListener,
                removeResizeListener,
              } from '@element-plus/utils/resize-event'
              import { isValidComponentSize } from '@element-plus/utils/validators'
              import { Effect, Options } from '@element-plus/components/popper'
              
              import type { ComputedRef, PropType } from 'vue'
              import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
              import type {
                CascaderValue,
                CascaderNode,
                Tag,
              } from '@element-plus/components/cascader-panel'
              import type { ComponentSize } from '@element-plus/utils/types'
              
              const DEFAULT_INPUT_HEIGHT = 40
              
              const INPUT_HEIGHT_MAP = {
                medium: 36,
                small: 32,
                mini: 28,
              }
              
              const popperOptions: Partial<Options> = {
                modifiers: [
                  {
                    name: 'arrowPosition',
                    enabled: true,
                    phase: 'main',
                    fn: ({ state }) => {
                      const { modifiersData, placement } = state
                      if (['right', 'left'].includes(placement)) return
                      modifiersData.arrow.x = 35
                    },
                    requires: ['arrow'],
                  },
                ],
              }
              
              export default defineComponent({
                name: 'ElCascader',
              
                components: {
                  ElCascaderPanel,
                  ElInput,
                  ElPopper,
                  ElScrollbar,
                  ElTag,
                },
              
                directives: {
                  Clickoutside,
                },
              
                props: {
                  ...CommonProps,
                  size: {
                    type: String as PropType<ComponentSize>,
                    validator: isValidComponentSize,
                  },
                  placeholder: {
                    type: String,
                  },
                  disabled: Boolean,
                  clearable: Boolean,
                  filterable: Boolean,
                  filterMethod: {
                    type: Function as PropType<
                      (node: CascaderNode, keyword: string) => boolean
                    >,
                    default: (node: CascaderNode, keyword: string) =>
                      node.text.includes(keyword),
                  },
                  separator: {
                    type: String,
                    default: ' / ',
                  },
                  showAllLevels: {
                    type: Boolean,
                    default: true,
                  },
                  collapseTags: Boolean,
                  debounce: {
                    type: Number,
                    default: 300,
                  },
                  beforeFilter: {
                    type: Function as PropType<(value: string) => boolean | Promise<any>>,
                    default: () => true,
                  },
                  popperClass: {
                    type: String,
                    default: '',
                  },
                  popperAppendToBody: {
                    type: Boolean,
                    default: true,
                  },
                },
              
                emits: [
                  UPDATE_MODEL_EVENT,
                  CHANGE_EVENT,
                  'focus',
                  'blur',
                  'visible-change',
                  'expand-change',
                  'remove-tag',
                ],
              
                setup(props, { emit }) {
                  let inputInitialHeight = 0
                  let pressDeleteCount = 0
              
                  const { t } = useLocaleInject()
                  const $ELEMENT = useGlobalConfig()
                  const elForm = inject(elFormKey, {} as ElFormContext)
                  const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
              
                  const popper = ref(null)
                  const input = ref(null)
                  const tagWrapper = ref(null)
                  const panel = ref(null)
                  const suggestionPanel = ref(null)
                  const popperVisible = ref(false)
                  const inputHover = ref(false)
                  const filtering = ref(false)
                  const inputValue = ref('')
                  const searchInputValue = ref('')
                  const presentTags: Ref<Tag[]> = ref([])
                  const suggestions: Ref<CascaderNode[]> = ref([])
              
                  const isDisabled = computed(() => props.disabled || elForm.disabled)
                  const inputPlaceholder = computed(
                    () => props.placeholder || t('el.cascader.placeholder')
                  )
                  const realSize: ComputedRef<ComponentSize> = computed(
                    () => props.size || elFormItem.size || $ELEMENT.size
                  )
                  const tagSize = computed(() =>
                    ['small', 'mini'].includes(realSize.value) ? 'mini' : 'small'
                  )
                  const multiple = computed(() => !!props.props.multiple)
                  const readonly = computed(() => !props.filterable || multiple.value)
                  const searchKeyword = computed(() =>
                    multiple.value ? searchInputValue.value : inputValue.value
                  )
                  const checkedNodes: ComputedRef<CascaderNode[]> = computed(
                    () => panel.value?.checkedNodes || []
                  )
                  const clearBtnVisible = computed(() => {
                    if (
                      !props.clearable ||
                      isDisabled.value ||
                      filtering.value ||
                      !inputHover.value
                    )
                      return false
              
                    return !!checkedNodes.value.length
                  })
                  const presentText = computed(() => {
                    const { showAllLevels, separator } = props
                    const nodes = checkedNodes.value
                    return nodes.length
                      ? multiple.value
                        ? ' '
                        : nodes[0].calcText(showAllLevels, separator)
                      : ''
                  })
              
                  const checkedValue = computed<CascaderValue>({
                    get() {
                      return props.modelValue
                    },
                    set(val) {
                      emit(UPDATE_MODEL_EVENT, val)
                      emit(CHANGE_EVENT, val)
                      elFormItem.formItemMitt?.emit('el.form.change', [val])
                    },
                  })
              
                  const popperPaneRef = computed(() => {
                    return popper.value?.popperRef
                  })
              
                  const togglePopperVisible = (visible?: boolean) => {
                    if (isDisabled.value) return
              
                    visible = visible ?? !popperVisible.value
              
                    if (visible !== popperVisible.value) {
                      popperVisible.value = visible
                      input.value.input.setAttribute('aria-expanded', visible)
              
                      if (visible) {
                        updatePopperPosition()
                        nextTick(panel.value.scrollToExpandingNode)
                      } else if (props.filterable) {
                        const { value } = presentText
                        inputValue.value = value
                        searchInputValue.value = value
                      }
              
                      emit('visible-change', visible)
                    }
                  }
              
                  const updatePopperPosition = () => {
                    nextTick(popper.value.update)
                  }
              
                  const hideSuggestionPanel = () => {
                    filtering.value = false
                  }
              
                  const genTag = (node: CascaderNode): Tag => {
                    const { showAllLevels, separator } = props
                    return {
                      node,
                      key: node.uid,
                      text: node.calcText(showAllLevels, separator),
                      hitState: false,
                      closable: !isDisabled.value && !node.isDisabled,
                    }
                  }
              
                  const deleteTag = (tag: Tag) => {
                    const { node } = tag
                    node.doCheck(false)
                    panel.value.calculateCheckedValue()
                    emit('remove-tag', node.valueByOption)
                  }
              
                  const calculatePresentTags = () => {
                    if (!multiple.value) return
              
                    const nodes = checkedNodes.value
                    const tags: Tag[] = []
              
                    if (nodes.length) {
                      const [first, ...rest] = nodes
                      const restCount = rest.length
              
                      tags.push(genTag(first))
              
                      if (restCount) {
                        if (props.collapseTags) {
                          tags.push({
                            key: -1,
                            text: `+ ${restCount}`,
                            closable: false,
                          })
                        } else {
                          rest.forEach((node) => tags.push(genTag(node)))
                        }
                      }
                    }
              
                    presentTags.value = tags
                  }
              
                  const calculateSuggestions = () => {
                    const { filterMethod, showAllLevels, separator } = props
                    const res = panel.value
                      .getFlattedNodes(!props.props.checkStrictly)
                      .filter((node) => {
                        if (node.isDisabled) return false
                        node.calcText(showAllLevels, separator)
                        return filterMethod(node, searchKeyword.value)
                      })
              
                    if (multiple.value) {
                      presentTags.value.forEach((tag) => {
                        tag.hitState = false
                      })
                    }
              
                    filtering.value = true
                    suggestions.value = res
                    updatePopperPosition()
                  }
              
                  const focusFirstNode = () => {
                    let firstNode = null
              
                    if (filtering.value && suggestionPanel.value) {
                      firstNode = suggestionPanel.value.$el.querySelector(
                        '.el-cascader__suggestion-item'
                      )
                    } else {
                      firstNode = panel.value?.$el.querySelector(
                        '.el-cascader-node[tabindex="-1"]'
                      )
                    }
              
                    if (firstNode) {
                      firstNode.focus()
                      !filtering.value && firstNode.click()
                    }
                  }
              
                  const updateStyle = () => {
                    const inputInner = input.value.input
                    const tagWrapperEl = tagWrapper.value
                    const suggestionPanelEl = suggestionPanel.value?.$el
              
                    if (isServer || !inputInner) return
              
                    if (suggestionPanelEl) {
                      const suggestionList = suggestionPanelEl.querySelector(
                        '.el-cascader__suggestion-list'
                      )
                      suggestionList.style.minWidth = inputInner.offsetWidth + 'px'
                    }
              
                    if (tagWrapperEl) {
                      const { offsetHeight } = tagWrapperEl
                      const height =
                        presentTags.value.length > 0
                          ? Math.max(offsetHeight + 6, inputInitialHeight) + 'px'
                          : `${inputInitialHeight}px`
                      inputInner.style.height = height
                      updatePopperPosition()
                    }
                  }
              
                  const getCheckedNodes = (leafOnly: boolean) => {
                    return panel.value.getCheckedNodes(leafOnly)
                  }
              
                  const handleExpandChange = (value: CascaderValue) => {
                    updatePopperPosition()
                    emit('expand-change', value)
                  }
              
                  const handleKeyDown = (e: KeyboardEvent) => {
                    switch (e.code) {
                      case EVENT_CODE.enter:
                        togglePopperVisible()
                        break
                      case EVENT_CODE.down:
                        togglePopperVisible(true)
                        nextTick(focusFirstNode)
                        e.preventDefault()
                        break
                      case EVENT_CODE.esc:
                      case EVENT_CODE.tab:
                        togglePopperVisible(false)
                        break
                    }
                  }
              
                  const handleClear = () => {
                    panel.value.clearCheckedNodes()
                    togglePopperVisible(false)
                  }
              
                  const handleSuggestionClick = (node: CascaderNode) => {
                    const { checked } = node
              
                    if (multiple.value) {
                      panel.value.handleCheckChange(node, !checked, false)
                    } else {
                      !checked && panel.value.handleCheckChange(node, true, false)
                      togglePopperVisible(false)
                    }
                  }
              
                  const handleDelete = () => {
                    const tags = presentTags.value
                    const lastTag = tags[tags.length - 1]
                    pressDeleteCount = searchInputValue.value ? 0 : pressDeleteCount + 1
              
                    if (!lastTag || !pressDeleteCount) return
              
                    if (lastTag.hitState) {
                      deleteTag(lastTag)
                    } else {
                      lastTag.hitState = true
                    }
                  }
              
                  const handleFilter = debounce(() => {
                    const { value } = searchKeyword
              
                    if (!value) return
              
                    const passed = props.beforeFilter(value)
              
                    if (isPromise(passed)) {
                      passed.then(calculateSuggestions).catch(() => {
                        /* prevent log error */
                      })
                    } else if (passed !== false) {
                      calculateSuggestions()
                    } else {
                      hideSuggestionPanel()
                    }
                  }, props.debounce)
              
                  const handleInput = (val: string, e: KeyboardEvent) => {
                    !popperVisible.value && togglePopperVisible(true)
              
                    if (e?.isComposing) return
              
                    val ? handleFilter() : hideSuggestionPanel()
                  }
              
                  watch(filtering, updatePopperPosition)
              
                  watch([checkedNodes, isDisabled], calculatePresentTags)
              
                  watch(presentTags, () => nextTick(updateStyle))
              
                  watch(presentText, (val) => (inputValue.value = val), { immediate: true })
              
                  onMounted(() => {
                    const inputEl = input.value.$el
                    inputInitialHeight =
                      inputEl?.offsetHeight ||
                      INPUT_HEIGHT_MAP[realSize.value] ||
                      DEFAULT_INPUT_HEIGHT
                    addResizeListener(inputEl, updateStyle)
                  })
              
                  onBeforeUnmount(() => {
                    removeResizeListener(input.value.$el, updateStyle)
                  })
              
                  return {
                    Effect,
                    popperOptions,
                    popper,
                    popperPaneRef,
                    input,
                    tagWrapper,
                    panel,
                    suggestionPanel,
                    popperVisible,
                    inputHover,
                    inputPlaceholder,
                    filtering,
                    presentText,
                    checkedValue,
                    inputValue,
                    searchInputValue,
                    presentTags,
                    suggestions,
                    isDisabled,
                    realSize,
                    tagSize,
                    multiple,
                    readonly,
                    clearBtnVisible,
                    t,
                    togglePopperVisible,
                    hideSuggestionPanel,
                    deleteTag,
                    focusFirstNode,
                    getCheckedNodes,
                    handleExpandChange,
                    handleKeyDown,
                    handleClear,
                    handleSuggestionClick,
                    handleDelete,
                    handleInput,
                  }
                },
              })
              </script>
              
              1
              2
              3
              4
              5
              6
              7
              8
              9
              10
              11
              12
              13
              14
              15
              16
              17
              18
              19
              20
              21
              22
              23
              24
              25
              26
              27
              28
              29
              30
              31
              32
              33
              34
              35
              36
              37
              38
              39
              40
              41
              42
              43
              44
              45
              46
              47
              48
              49
              50
              51
              52
              53
              54
              55
              56
              57
              58
              59
              60
              61
              62
              63
              64
              65
              66
              67
              68
              69
              70
              71
              72
              73
              74
              75
              76
              77
              78
              79
              80
              81
              82
              83
              84
              85
              86
              87
              88
              89
              90
              91
              92
              93
              94
              95
              96
              97
              98
              99
              100
              101
              102
              103
              104
              105
              106
              107
              108
              109
              110
              111
              112
              113
              114
              115
              116
              117
              118
              119
              120
              121
              122
              123
              124
              125
              126
              127
              128
              129
              130
              131
              132
              133
              134
              135
              136
              137
              138
              139
              140
              141
              142
              143
              144
              145
              146
              147
              148
              149
              150
              151
              152
              153
              154
              155
              156
              157
              158
              159
              160
              161
              162
              163
              164
              165
              166
              167
              168
              169
              170
              171
              172
              173
              174
              175
              176
              177
              178
              179
              180
              181
              182
              183
              184
              185
              186
              187
              188
              189
              190
              191
              192
              193
              194
              195
              196
              197
              198
              199
              200
              201
              202
              203
              204
              205
              206
              207
              208
              209
              210
              211
              212
              213
              214
              215
              216
              217
              218
              219
              220
              221
              222
              223
              224
              225
              226
              227
              228
              229
              230
              231
              232
              233
              234
              235
              236
              237
              238
              239
              240
              241
              242
              243
              244
              245
              246
              247
              248
              249
              250
              251
              252
              253
              254
              255
              256
              257
              258
              259
              260
              261
              262
              263
              264
              265
              266
              267
              268
              269
              270
              271
              272
              273
              274
              275
              276
              277
              278
              279
              280
              281
              282
              283
              284
              285
              286
              287
              288
              289
              290
              291
              292
              293
              294
              295
              296
              297
              298
              299
              300
              301
              302
              303
              304
              305
              306
              307
              308
              309
              310
              311
              312
              313
              314
              315
              316
              317
              318
              319
              320
              321
              322
              323
              324
              325
              326
              327
              328
              329
              330
              331
              332
              333
              334
              335
              336
              337
              338
              339
              340
              341
              342
              343
              344
              345
              346
              347
              348
              349
              350
              351
              352
              353
              354
              355
              356
              357
              358
              359
              360
              361
              362
              363
              364
              365
              366
              367
              368
              369
              370
              371
              372
              373
              374
              375
              376
              377
              378
              379
              380
              381
              382
              383
              384
              385
              386
              387
              388
              389
              390
              391
              392
              393
              394
              395
              396
              397
              398
              399
              400
              401
              402
              403
              404
              405
              406
              407
              408
              409
              410
              411
              412
              413
              414
              415
              416
              417
              418
              419
              420
              421
              422
              423
              424
              425
              426
              427
              428
              429
              430
              431
              432
              433
              434
              435
              436
              437
              438
              439
              440
              441
              442
              443
              444
              445
              446
              447
              448
              449
              450
              451
              452
              453
              454
              455
              456
              457
              458
              459
              460
              461
              462
              463
              464
              465
              466
              467
              468
              469
              470
              471
              472
              473
              474
              475
              476
              477
              478
              479
              480
              481
              482
              483
              484
              485
              486
              487
              488
              489
              490
              491
              492
              493
              494
              495
              496
              497
              498
              499
              500
              501
              502
              503
              504
              505
              506
              507
              508
              509
              510
              511
              512
              513
              514
              515
              516
              517
              518
              519
              520
              521
              522
              523
              524
              525
              526
              527
              528
              529
              530
              531
              532
              533
              534
              535
              536
              537
              538
              539
              540
              541
              542
              543
              544
              545
              546
              547
              548
              549
              550
              551
              552
              553
              554
              555
              556
              557
              558
              559
              560
              561
              562
              563
              564
              565
              566
              567
              568
              569
              570
              571
              572
              573
              574
              575
              576
              577
              578
              579
              580
              581
              582
              583
              584
              585
              586
              587
              588
              589
              590
              591
              592
              593
              594
              595
              596
              597
              598
              599
              600
              601
              602
              603
              604
              605
              606
              607
              608
              609
              610
              611
              612
              613
              614
              615
              616
              617
              618
              619
              620
              621
              622
              623
              624
              625
              626
              627
              628
              629
              630
              631
              632
              633
              634
              635
              636
              637
              638
              639
              640
              641
              642
              643
              644
              645
              646
              647
              648
              649
              650
              651
              652
              653
              654
              655
              656
              657
              658
              659
              660
              661
              662
              663
          • style
            • css.ts
              代码
              import "@element-plus/components/base/style/css"
              import "@element-plus/theme-chalk/el-cascader.css"
              import "@element-plus/components/input/style/css"
              import "@element-plus/components/popper/style/css"
              import "@element-plus/components/tag/style/css"
              import "@element-plus/components/cascader-panel/style/css"
              
              1
              2
              3
              4
              5
              6
            • index.ts
              代码
              import "@element-plus/components/base/style"
              import "@element-plus/theme-chalk/src/cascader.scss"
              import "@element-plus/components/input/style/index"
              import "@element-plus/components/popper/style/index"
              import "@element-plus/components/tag/style/index"
              import "@element-plus/components/cascader-panel/style/index"
              
              1
              2
              3
              4
              5
              6
          • index.ts
            组件入口


             

             
             
             




             

            import { App } from 'vue'
            import type { SFCWithInstall } from '@element-plus/utils/types'
            import Cascader from './src/index.vue'
            
            Cascader.install = (app: App): void => {
              app.component(Cascader.name, Cascader)
            }
            
            const _Cascader = Cascader as SFCWithInstall<typeof Cascader>
            
            export default _Cascader
            export const ElCascader = _Cascader
            
            1
            2
            3
            4
            5
            6
            7
            8
            9
            10
            11
            12