mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-29 17:58:08 +08:00
feat: add mention component (#17586)
* feat: add mention component * fix: build error * fix: build error * feat: delete as a whole * fix: update docs * fix: update global.d.ts * fix: update * fix: update code * fix: update code * fix: update code * fix: rename * fix: update code * fix: upload code * fix: update code * fix: fixed cursor position abnormality * fix: update code * fix: docs add avatar * fix: tooltip position is wrong when placing on top * feat: add overview icon * fix: overview icon color
This commit is contained in:
parent
76ee10ab69
commit
0456c790db
@ -106,6 +106,11 @@
|
||||
"link": "/input-number",
|
||||
"text": "Input Number"
|
||||
},
|
||||
{
|
||||
"link": "/mention",
|
||||
"text": "Mention",
|
||||
"promotion": "2.8.0"
|
||||
},
|
||||
{
|
||||
"link": "/radio",
|
||||
"text": "Radio"
|
||||
|
@ -71,6 +71,7 @@ import OvPopover from './ov-popover.vue'
|
||||
import OvTooltip from './ov-tooltip.vue'
|
||||
import OvDivider from './ov-divider.vue'
|
||||
import OvWatermark from './ov-watermark.vue'
|
||||
import OvMention from './ov-mention.vue'
|
||||
|
||||
export default {
|
||||
button: OvButton,
|
||||
@ -149,4 +150,5 @@ export default {
|
||||
tooltip: OvTooltip,
|
||||
divider: OvDivider,
|
||||
watermark: OvWatermark,
|
||||
mention: OvMention,
|
||||
}
|
||||
|
@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<svg
|
||||
width="280"
|
||||
height="180"
|
||||
viewBox="0 0 280 180"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="280" height="180" fill="var(--el-fill-color-light)" />
|
||||
<rect
|
||||
x="85.5"
|
||||
y="40.5"
|
||||
width="99"
|
||||
height="19"
|
||||
rx="1.5"
|
||||
fill="var(--el-fill-color-blank)"
|
||||
stroke="var(--el-color-primary)"
|
||||
/>
|
||||
<g filter="url(#filter0_d_1544_8231)">
|
||||
<rect
|
||||
x="85"
|
||||
y="62"
|
||||
width="100"
|
||||
height="68"
|
||||
rx="2"
|
||||
fill="var(--el-bg-color-overlay)"
|
||||
/>
|
||||
<rect
|
||||
x="84.7"
|
||||
y="61.7"
|
||||
width="100.6"
|
||||
height="68.6"
|
||||
rx="2.3"
|
||||
stroke="var(--el-border-color-dark)"
|
||||
stroke-width="0.6"
|
||||
/>
|
||||
</g>
|
||||
<rect
|
||||
x="93"
|
||||
y="74"
|
||||
width="25"
|
||||
height="4"
|
||||
rx="2"
|
||||
fill="var(--el-border-color-dark)"
|
||||
/>
|
||||
<path
|
||||
d="M93.47 46.72C94.55 46.72 95.4 47.0433 96.02 47.69C96.5533 48.2567 96.82 48.9467 96.82 49.76C96.82 50.4533 96.5967 51.1033 96.15 51.71C95.6967 52.3033 95.1833 52.6 94.61 52.6C94.1967 52.6 93.9533 52.3933 93.88 51.98C93.5933 52.4 93.2067 52.61 92.72 52.61C92.2733 52.61 91.91 52.46 91.63 52.16C91.35 51.86 91.21 51.4833 91.21 51.03C91.21 50.33 91.4267 49.7133 91.86 49.18C92.2867 48.6333 92.8033 48.36 93.41 48.36C93.8367 48.36 94.14 48.59 94.32 49.05L94.46 48.55H95.35L94.67 50.99C94.57 51.35 94.52 51.5733 94.52 51.66C94.52 51.7933 94.5933 51.86 94.74 51.86C95.0133 51.86 95.2933 51.6767 95.58 51.31C95.9 50.89 96.06 50.3767 96.06 49.77C96.06 49.15 95.84 48.6233 95.4 48.19C94.92 47.7167 94.2567 47.48 93.41 47.48C92.5167 47.48 91.8 47.7633 91.26 48.33C90.7267 48.8833 90.46 49.61 90.46 50.51C90.46 51.4967 90.7433 52.24 91.31 52.74C91.83 53.2 92.5767 53.43 93.55 53.43C94.1833 53.43 94.7367 53.3267 95.21 53.12C95.63 52.92 95.9667 52.6433 96.22 52.29H97.16C96.8067 52.9233 96.29 53.4 95.61 53.72C94.99 54 94.3033 54.14 93.55 54.14C92.2633 54.14 91.2667 53.81 90.56 53.15C89.8533 52.49 89.5 51.5967 89.5 50.47C89.5 49.4233 89.8433 48.5533 90.53 47.86C91.27 47.1 92.25 46.72 93.47 46.72ZM93.59 49.23C93.21 49.23 92.88 49.42 92.6 49.8C92.32 50.18 92.18 50.6033 92.18 51.07C92.18 51.31 92.23 51.49 92.33 51.61C92.43 51.7433 92.5867 51.81 92.8 51.81C92.9933 51.81 93.18 51.74 93.36 51.6C93.56 51.44 93.7 51.2333 93.78 50.98L93.98 50.26C94.06 49.9733 94.1 49.7967 94.1 49.73C94.1 49.5833 94.0467 49.4633 93.94 49.37C93.8467 49.2767 93.73 49.23 93.59 49.23Z"
|
||||
fill="var(--el-color-primary)"
|
||||
/>
|
||||
<rect
|
||||
x="93"
|
||||
y="94"
|
||||
width="25"
|
||||
height="4"
|
||||
rx="2"
|
||||
fill="var(--el-color-primary)"
|
||||
/>
|
||||
<rect
|
||||
x="93"
|
||||
y="114"
|
||||
width="25"
|
||||
height="4"
|
||||
rx="2"
|
||||
fill="var(--el-border-color-dark)"
|
||||
/>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_d_1544_8231"
|
||||
x="81"
|
||||
y="59"
|
||||
width="108"
|
||||
height="76"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB"
|
||||
>
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="2" />
|
||||
<feComposite in2="hardAlpha" operator="out" />
|
||||
<feColorMatrix
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in2="BackgroundImageFix"
|
||||
result="effect1_dropShadow_1544_8231"
|
||||
/>
|
||||
<feBlend
|
||||
mode="normal"
|
||||
in="SourceGraphic"
|
||||
in2="effect1_dropShadow_1544_8231"
|
||||
result="shape"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
1
docs/components.d.ts
vendored
1
docs/components.d.ts
vendored
@ -82,6 +82,7 @@ declare module 'vue' {
|
||||
OvLayout: typeof import('./.vitepress/vitepress/components/overview-icons/ov-layout.vue')['default']
|
||||
OvLink: typeof import('./.vitepress/vitepress/components/overview-icons/ov-link.vue')['default']
|
||||
OvLoading: typeof import('./.vitepress/vitepress/components/overview-icons/ov-loading.vue')['default']
|
||||
OvMention: typeof import('./.vitepress/vitepress/components/overview-icons/ov-mention.vue')['default']
|
||||
OvMenu: typeof import('./.vitepress/vitepress/components/overview-icons/ov-menu.vue')['default']
|
||||
OvMessage: typeof import('./.vitepress/vitepress/components/overview-icons/ov-message.vue')['default']
|
||||
OvMessageBox: typeof import('./.vitepress/vitepress/components/overview-icons/ov-message-box.vue')['default']
|
||||
|
@ -173,17 +173,18 @@ input/length-limiting
|
||||
|
||||
### Exposes
|
||||
|
||||
| Name | Description | Type |
|
||||
| -------------- | -------------------------------- | ------------------------------------------------------- |
|
||||
| blur | blur the input element | ^[Function]`() => void` |
|
||||
| clear | clear input value | ^[Function]`() => void` |
|
||||
| focus | focus the input element | ^[Function]`() => void` |
|
||||
| input | HTML input element | ^[object]`Ref<HTMLInputElement>` |
|
||||
| ref | HTML element, input or textarea | ^[object]`Ref<HTMLInputElement \| HTMLTextAreaElement>` |
|
||||
| resizeTextarea | resize textarea | ^[Function]`() => void` |
|
||||
| select | select the text in input element | ^[Function]`() => void` |
|
||||
| textarea | HTML textarea element | ^[object]`Ref<HTMLTextAreaElement>` |
|
||||
| textareaStyle | style of textarea | ^[object]`Ref<StyleValue>` |
|
||||
| Name | Description | Type |
|
||||
| -------------------- | -------------------------------- | ------------------------------------------------------- |
|
||||
| blur | blur the input element | ^[Function]`() => void` |
|
||||
| clear | clear input value | ^[Function]`() => void` |
|
||||
| focus | focus the input element | ^[Function]`() => void` |
|
||||
| input | HTML input element | ^[object]`Ref<HTMLInputElement>` |
|
||||
| ref | HTML element, input or textarea | ^[object]`Ref<HTMLInputElement \| HTMLTextAreaElement>` |
|
||||
| resizeTextarea | resize textarea | ^[Function]`() => void` |
|
||||
| select | select the text in input element | ^[Function]`() => void` |
|
||||
| textarea | HTML textarea element | ^[object]`Ref<HTMLTextAreaElement>` |
|
||||
| textareaStyle | style of textarea | ^[object]`Ref<StyleValue>` |
|
||||
| isComposing ^(2.8.0) | is input composing | ^[object]`Ref<boolean>` |
|
||||
|
||||
## FAQ
|
||||
|
||||
|
145
docs/en-US/component/mention.md
Normal file
145
docs/en-US/component/mention.md
Normal file
@ -0,0 +1,145 @@
|
||||
---
|
||||
title: Mention
|
||||
lang: en-US
|
||||
---
|
||||
|
||||
# Mention
|
||||
|
||||
Used to mention someone or something in an input.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The most basic usage.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/basic
|
||||
|
||||
:::
|
||||
|
||||
## Textarea
|
||||
|
||||
The input type can be set to `textarea`.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/textarea
|
||||
|
||||
:::
|
||||
|
||||
## Customize label
|
||||
|
||||
Customize label by `label` slot.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/label
|
||||
|
||||
:::
|
||||
|
||||
## Load remote options
|
||||
|
||||
Load options asynchronously.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/loading
|
||||
|
||||
:::
|
||||
|
||||
## Customize trigger token
|
||||
|
||||
Customize trigger token by `prefix` props. Default to `@`, `Array<string>` also supported.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/prefix
|
||||
|
||||
:::
|
||||
|
||||
## Delete as a whole
|
||||
|
||||
Set the `whole` attribute to `true`, and when you press the backspace, the mention will be deleted as a whole.
|
||||
Set the `check-is-whole` attribute to customize the checking logic.
|
||||
:::demo
|
||||
|
||||
mention/whole
|
||||
|
||||
:::
|
||||
|
||||
## Work with form
|
||||
|
||||
to work with `el-form`.
|
||||
|
||||
:::demo
|
||||
|
||||
mention/form
|
||||
|
||||
:::
|
||||
|
||||
::: tip
|
||||
Since this component is developed based on the component [`el-input`](./input.md#attributes) , the original properties have not changed, so no repetition here,
|
||||
and please go to the original component to view the documentation.
|
||||
:::
|
||||
|
||||
## API
|
||||
|
||||
### Attributes
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| ------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------- |
|
||||
| options | mention options list | ^[array]`MentionOption[]` | [] |
|
||||
| prefix | prefix character to trigger mentions. The string length must be exactly 1 | ^[string] \| ^[array]`string[]` | `'@'` |
|
||||
| split | character to split mentions. The string length must be exactly 1 | ^[string] | `' '` |
|
||||
| filter-option | customize filter option logic | ^[false] \| ^[Function]`(pattern: string, option: MentionOption) => boolean` | - |
|
||||
| placement | set popup placement | ^[string]`'bottom' \| 'top'` | `'bottom'` |
|
||||
| show-arrow | whether the dropdown panel has an arrow | ^[boolean] | `false` |
|
||||
| offset | offset of the dropdown panel | ^[number] | `0` |
|
||||
| whole | when backspace is pressed to delete, whether the mention content is deleted as a whole | ^[boolean] | `false` |
|
||||
| check-is-whole | when backspace is pressed to delete, check if the mention is a whole | ^[Function]`(pattern: string, prefix: string) => boolean` | - |
|
||||
| loading | whether the dropdown panel of mentions is in a loading state | ^[boolean] | `false` |
|
||||
| model-value / v-model | input value | ^[string] | - |
|
||||
| popper-class | custom class name for dropdown panel | ^[string] | - |
|
||||
| popper-options | [popper.js](https://popper.js.org/docs/v2/) parameters | ^[object] refer to [popper.js doc](https://popper.js.org/docs/v2/) | - |
|
||||
| [input props](./input.md#attributes) | - | - | - |
|
||||
|
||||
### Events
|
||||
|
||||
| Name | Description | Type |
|
||||
| --------------------------------- | ----------------------------------- | ------------------------------------------------------------ |
|
||||
| search | trigger when prefix hit | ^[Function]`(pattern: string, prefix: string) => void` |
|
||||
| select | trigger when user select the option | ^[Function]`(option: MentionOption, prefix: string) => void` |
|
||||
| [input events](./input.md#events) | - | - |
|
||||
|
||||
### Slots
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------------------------------- | ------------------------------------- | ------------------------------------------------- |
|
||||
| label | content as option label | ^[object]`{ item: MentionOption, index: number }` |
|
||||
| loading | content as option loading | - |
|
||||
| header | content at the top of the dropdown | - |
|
||||
| footer | content at the bottom of the dropdown | - |
|
||||
| [input slots](./input.md#slots) | - | - |
|
||||
|
||||
### Exposes
|
||||
|
||||
| Name | Description | Type |
|
||||
| ------- | ----------------------------- | --------------------------------------- |
|
||||
| input | el-input component instance | ^[object]`Ref<InputInstance \| null>` |
|
||||
| tooltip | el-tooltip component instance | ^[object]`Ref<TooltipInstance \| null>` |
|
||||
|
||||
## Type Declarations
|
||||
|
||||
<details>
|
||||
<summary>Show declarations</summary>
|
||||
|
||||
```ts
|
||||
type MentionOption = {
|
||||
value: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
32
docs/examples/mention/basic.vue
Normal file
32
docs/examples/mention/basic.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value"
|
||||
:options="options"
|
||||
style="width: 320px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const value = ref('@')
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
label: 'Fuphoenixes',
|
||||
value: 'Fuphoenixes',
|
||||
},
|
||||
{
|
||||
label: 'kooriookami',
|
||||
value: 'kooriookami',
|
||||
},
|
||||
{
|
||||
label: 'Jeremy',
|
||||
value: 'Jeremy',
|
||||
},
|
||||
{
|
||||
label: 'btea',
|
||||
value: 'btea',
|
||||
},
|
||||
])
|
||||
</script>
|
76
docs/examples/mention/form.vue
Normal file
76
docs/examples/mention/form.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
style="max-width: 600px"
|
||||
:model="ruleForm"
|
||||
:rules="rules"
|
||||
>
|
||||
<el-form-item label="name" prop="name">
|
||||
<el-mention v-model="ruleForm.name" :options="options" />
|
||||
</el-form-item>
|
||||
<el-form-item label="desc" prop="desc">
|
||||
<el-mention v-model="ruleForm.desc" type="textarea" :options="options" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm(ruleFormRef)">
|
||||
Submit
|
||||
</el-button>
|
||||
<el-button @click="resetForm(ruleFormRef)">Reset</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface RuleForm {
|
||||
name: string
|
||||
desc: string
|
||||
}
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
const ruleForm = reactive<RuleForm>({
|
||||
name: '',
|
||||
desc: '',
|
||||
})
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
label: 'Fuphoenixes',
|
||||
value: 'Fuphoenixes',
|
||||
},
|
||||
{
|
||||
label: 'kooriookami',
|
||||
value: 'kooriookami',
|
||||
},
|
||||
{
|
||||
label: 'Jeremy',
|
||||
value: 'Jeremy',
|
||||
},
|
||||
{
|
||||
label: 'btea',
|
||||
value: 'btea',
|
||||
},
|
||||
])
|
||||
|
||||
const rules = reactive<FormRules<RuleForm>>({
|
||||
name: [{ required: true, message: 'Please input name', trigger: 'blur' }],
|
||||
desc: [{ required: true, message: 'Please input desc', trigger: 'blur' }],
|
||||
})
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
console.log('submit!')
|
||||
} else {
|
||||
console.log('error submit!', fields)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
}
|
||||
</script>
|
39
docs/examples/mention/label.vue
Normal file
39
docs/examples/mention/label.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value"
|
||||
:options="options"
|
||||
style="width: 320px"
|
||||
placeholder="Please input"
|
||||
>
|
||||
<template #label="{ item }">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-avatar :size="24" :src="item.avatar" />
|
||||
<span style="margin-left: 6px">{{ item.value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-mention>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const value = ref('')
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
value: 'Fuphoenixes',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/27912232',
|
||||
},
|
||||
{
|
||||
value: 'kooriookami',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/38392315',
|
||||
},
|
||||
{
|
||||
value: 'Jeremy',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/15975785',
|
||||
},
|
||||
{
|
||||
value: 'btea',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/24516654',
|
||||
},
|
||||
])
|
||||
</script>
|
39
docs/examples/mention/loading.vue
Normal file
39
docs/examples/mention/loading.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value"
|
||||
:options="options"
|
||||
:loading="loading"
|
||||
style="width: 320px"
|
||||
placeholder="Please input"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref } from 'vue'
|
||||
import type { MentionOption } from 'element-plus'
|
||||
|
||||
const value = ref('')
|
||||
const loading = ref(false)
|
||||
const options = ref<MentionOption[]>([])
|
||||
|
||||
let timer: ReturnType<typeof setTimeout>
|
||||
const handleSearch = (pattern: string) => {
|
||||
if (timer) clearTimeout(timer)
|
||||
|
||||
loading.value = true
|
||||
timer = setTimeout(() => {
|
||||
options.value = ['Fuphoenixes', 'kooriookami', 'Jeremy', 'btea'].map(
|
||||
(item) => ({
|
||||
label: pattern + item,
|
||||
value: pattern + item,
|
||||
})
|
||||
)
|
||||
loading.value = false
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
</script>
|
28
docs/examples/mention/prefix.vue
Normal file
28
docs/examples/mention/prefix.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value"
|
||||
:options="options"
|
||||
:prefix="['@', '#']"
|
||||
style="width: 320px"
|
||||
placeholder="input @ to mention people, # to mention tag"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { MentionOption } from 'element-plus'
|
||||
|
||||
const MOCK_DATA: Record<string, string[]> = {
|
||||
'@': ['Fuphoenixes', 'kooriookami', 'Jeremy', 'btea'],
|
||||
'#': ['1.0', '2.0', '3.0'],
|
||||
}
|
||||
const value = ref('')
|
||||
const options = ref<MentionOption[]>([])
|
||||
|
||||
const handleSearch = (_: string, prefix: string) => {
|
||||
options.value = (MOCK_DATA[prefix] || []).map((value) => ({
|
||||
value,
|
||||
}))
|
||||
}
|
||||
</script>
|
33
docs/examples/mention/textarea.vue
Normal file
33
docs/examples/mention/textarea.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value"
|
||||
type="textarea"
|
||||
:options="options"
|
||||
style="width: 320px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
const value = ref('')
|
||||
|
||||
const options = ref([
|
||||
{
|
||||
label: 'Fuphoenixes',
|
||||
value: 'Fuphoenixes',
|
||||
},
|
||||
{
|
||||
label: 'kooriookami',
|
||||
value: 'kooriookami',
|
||||
},
|
||||
{
|
||||
label: 'Jeremy',
|
||||
value: 'Jeremy',
|
||||
},
|
||||
{
|
||||
label: 'btea',
|
||||
value: 'btea',
|
||||
},
|
||||
])
|
||||
</script>
|
46
docs/examples/mention/whole.vue
Normal file
46
docs/examples/mention/whole.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<el-mention
|
||||
v-model="value1"
|
||||
whole
|
||||
:options="options1"
|
||||
style="width: 320px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
<el-divider />
|
||||
<el-mention
|
||||
v-model="value2"
|
||||
:options="options2"
|
||||
:prefix="['@', '#']"
|
||||
whole
|
||||
:check-is-whole="checkIsWhole"
|
||||
style="width: 320px"
|
||||
placeholder="input @ to mention people, # to mention tag"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { MentionOption } from 'element-plus'
|
||||
|
||||
const MOCK_DATA: Record<string, string[]> = {
|
||||
'@': ['Fuphoenixes', 'kooriookami', 'Jeremy', 'btea'],
|
||||
'#': ['1.0', '2.0', '3.0'],
|
||||
}
|
||||
const value1 = ref('')
|
||||
const value2 = ref('')
|
||||
const options1 = ref<MentionOption[]>(
|
||||
MOCK_DATA['@'].map((value) => ({ value }))
|
||||
)
|
||||
const options2 = ref<MentionOption[]>([])
|
||||
|
||||
const handleSearch = (_: string, prefix: string) => {
|
||||
options2.value = (MOCK_DATA[prefix] || []).map((value) => ({
|
||||
value,
|
||||
}))
|
||||
}
|
||||
|
||||
const checkIsWhole = (pattern: string, prefix: string) => {
|
||||
return (MOCK_DATA[prefix] || []).includes(pattern)
|
||||
}
|
||||
</script>
|
1
global.d.ts
vendored
1
global.d.ts
vendored
@ -100,6 +100,7 @@ declare module 'vue' {
|
||||
ElWatermark: typeof import('element-plus')['ElWatermark']
|
||||
ElTour: typeof import('element-plus')['ElTour']
|
||||
ElTourStep: typeof import('element-plus')['ElTourStep']
|
||||
ElMention: typeof import('element-plus')['ElMention']
|
||||
}
|
||||
|
||||
interface ComponentCustomProperties {
|
||||
|
@ -73,6 +73,7 @@ export * from './watermark'
|
||||
export * from './tour'
|
||||
export * from './anchor'
|
||||
export * from './segmented'
|
||||
export * from './mention'
|
||||
|
||||
// plugins
|
||||
export * from './infinite-scroll'
|
||||
|
@ -541,6 +541,9 @@ defineExpose({
|
||||
/** @description from props (used on unit test) */
|
||||
autosize: toRef(props, 'autosize'),
|
||||
|
||||
/** @description is input composing */
|
||||
isComposing,
|
||||
|
||||
/** @description HTML input element native method */
|
||||
focus,
|
||||
/** @description HTML input element native method */
|
||||
|
89
packages/components/mention/__tests__/mention.test.tsx
Normal file
89
packages/components/mention/__tests__/mention.test.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { afterEach, describe, expect, test } from 'vitest'
|
||||
import sleep from '@element-plus/test-utils/sleep'
|
||||
import Mention from '../src/mention.vue'
|
||||
|
||||
describe('Mention.vue', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
})
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: 'Fuphoenixes',
|
||||
value: 'Fuphoenixes',
|
||||
},
|
||||
{
|
||||
label: 'kooriookami',
|
||||
value: 'kooriookami',
|
||||
},
|
||||
{
|
||||
label: 'Jeremy',
|
||||
value: 'Jeremy',
|
||||
},
|
||||
{
|
||||
label: 'btea',
|
||||
value: 'btea',
|
||||
},
|
||||
]
|
||||
|
||||
test('should work with `options` prop', async () => {
|
||||
const wrapper = mount(Mention, {
|
||||
attachTo: document.body,
|
||||
props: { options },
|
||||
})
|
||||
|
||||
wrapper.find('input').element.focus()
|
||||
wrapper.find('input').setValue('@')
|
||||
|
||||
await sleep(150)
|
||||
expect(document.querySelector('.el-mention-dropdown')).not.toEqual(null)
|
||||
expect(document.querySelectorAll('.el-mention-dropdown__item').length).toBe(
|
||||
4
|
||||
)
|
||||
})
|
||||
|
||||
test('should work with `type` prop', async () => {
|
||||
const wrapper = mount(Mention, {
|
||||
attachTo: document.body,
|
||||
props: { options },
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
|
||||
await wrapper.setProps({ type: 'text' })
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
|
||||
await wrapper.setProps({ type: 'textarea' })
|
||||
expect(wrapper.find('textarea').exists()).toBe(true)
|
||||
})
|
||||
|
||||
test('should work with `loading` prop', async () => {
|
||||
const wrapper = mount(Mention, {
|
||||
attachTo: document.body,
|
||||
props: { options, loading: true },
|
||||
})
|
||||
|
||||
wrapper.find('input').element.focus()
|
||||
await wrapper.find('input').setValue('@')
|
||||
await sleep(150)
|
||||
expect(document.querySelector('.el-mention-dropdown__loading')).not.toEqual(
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
test('should work with `prefix` prop', async () => {
|
||||
const wrapper = mount(Mention, {
|
||||
attachTo: document.body,
|
||||
props: { options, prefix: '#' },
|
||||
})
|
||||
|
||||
wrapper.find('input').element.focus()
|
||||
await wrapper.find('input').setValue('#')
|
||||
await sleep(150)
|
||||
expect(document.querySelector('.el-mention-dropdown')).not.toEqual(null)
|
||||
expect(document.querySelectorAll('.el-mention-dropdown__item').length).toBe(
|
||||
4
|
||||
)
|
||||
})
|
||||
})
|
8
packages/components/mention/index.ts
Normal file
8
packages/components/mention/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { withInstall } from '@element-plus/utils'
|
||||
import Mention from './src/mention.vue'
|
||||
import type { SFCWithInstall } from '@element-plus/utils'
|
||||
|
||||
export const ElMention: SFCWithInstall<typeof Mention> = withInstall(Mention)
|
||||
export default ElMention
|
||||
|
||||
export * from './src/mention'
|
219
packages/components/mention/src/helper.ts
Normal file
219
packages/components/mention/src/helper.ts
Normal file
@ -0,0 +1,219 @@
|
||||
import { ensureArray, isFirefox } from '@element-plus/utils'
|
||||
|
||||
import type { MentionCtx, MentionOption } from './types'
|
||||
|
||||
export const filterOption = (
|
||||
pattern: string,
|
||||
option: MentionOption
|
||||
): boolean => {
|
||||
const lowerCase = pattern.toLowerCase()
|
||||
const label = option.label || option.value
|
||||
return label.toLowerCase().includes(lowerCase)
|
||||
}
|
||||
|
||||
export const getMentionCtx = (
|
||||
inputEl: HTMLInputElement | HTMLTextAreaElement,
|
||||
prefix: string | string[],
|
||||
split: string
|
||||
) => {
|
||||
const { selectionEnd } = inputEl
|
||||
if (selectionEnd === null) return
|
||||
const inputValue = inputEl.value
|
||||
const prefixArray = ensureArray(prefix)
|
||||
let splitIndex = -1
|
||||
let mentionCtx: MentionCtx | undefined
|
||||
for (let i = selectionEnd - 1; i >= 0; --i) {
|
||||
const char = inputValue[i]
|
||||
if (char === split || char === '\n' || char === '\r') {
|
||||
splitIndex = i
|
||||
continue
|
||||
}
|
||||
if (prefixArray.includes(char)) {
|
||||
const end = splitIndex === -1 ? selectionEnd : splitIndex
|
||||
const pattern = inputValue.slice(i + 1, end)
|
||||
mentionCtx = {
|
||||
pattern,
|
||||
start: i + 1,
|
||||
end,
|
||||
prefix: char,
|
||||
prefixIndex: i,
|
||||
splitIndex,
|
||||
selectionEnd,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return mentionCtx
|
||||
}
|
||||
|
||||
/**
|
||||
* fork from textarea-caret-position
|
||||
* https://github.com/component/textarea-caret-position
|
||||
* The MIT License (MIT)
|
||||
* Copyright (c) 2015 Jonathan Ong me@jongleberry.com
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
export const getCursorPosition = (
|
||||
element: HTMLInputElement | HTMLTextAreaElement,
|
||||
options = {
|
||||
debug: false,
|
||||
useSelectionEnd: false,
|
||||
}
|
||||
) => {
|
||||
const selectionStart =
|
||||
element.selectionStart !== null ? element.selectionStart : 0
|
||||
const selectionEnd = element.selectionEnd !== null ? element.selectionEnd : 0
|
||||
const position = options.useSelectionEnd ? selectionEnd : selectionStart
|
||||
// We'll copy the properties below into the mirror div.
|
||||
// Note that some browsers, such as Firefox, do not concatenate properties
|
||||
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
|
||||
// so we have to list every single property explicitly.
|
||||
const properties: string[] = [
|
||||
'direction', // RTL support
|
||||
'boxSizing',
|
||||
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
|
||||
'height',
|
||||
'overflowX',
|
||||
'overflowY', // copy the scrollbar for IE
|
||||
'borderTopWidth',
|
||||
'borderRightWidth',
|
||||
'borderBottomWidth',
|
||||
'borderLeftWidth',
|
||||
'borderStyle',
|
||||
'paddingTop',
|
||||
'paddingRight',
|
||||
'paddingBottom',
|
||||
'paddingLeft',
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
||||
'fontStyle',
|
||||
'fontVariant',
|
||||
'fontWeight',
|
||||
'fontStretch',
|
||||
'fontSize',
|
||||
'fontSizeAdjust',
|
||||
'lineHeight',
|
||||
'fontFamily',
|
||||
'textAlign',
|
||||
'textTransform',
|
||||
'textIndent',
|
||||
'textDecoration', // might not make a difference, but better be safe
|
||||
'letterSpacing',
|
||||
'wordSpacing',
|
||||
'tabSize',
|
||||
'MozTabSize',
|
||||
]
|
||||
|
||||
if (options.debug) {
|
||||
const el = document.querySelector(
|
||||
'#input-textarea-caret-position-mirror-div'
|
||||
)
|
||||
if (el?.parentNode) el.parentNode.removeChild(el)
|
||||
}
|
||||
|
||||
// The mirror div will replicate the textareas style
|
||||
const div = document.createElement('div')
|
||||
div.id = 'input-textarea-caret-position-mirror-div'
|
||||
document.body.appendChild(div)
|
||||
|
||||
const style = div.style
|
||||
const computed = window.getComputedStyle(element)
|
||||
|
||||
const isInput = element.nodeName === 'INPUT'
|
||||
|
||||
// Default textarea styles
|
||||
style.whiteSpace = isInput ? 'nowrap' : 'pre-wrap'
|
||||
if (!isInput) style.wordWrap = 'break-word' // only for textarea-s
|
||||
|
||||
// Position off-screen
|
||||
style.position = 'absolute' // required to return coordinates properly
|
||||
if (!options.debug) style.visibility = 'hidden' // not 'display: none' because we want rendering
|
||||
|
||||
// Transfer the element's properties to the div
|
||||
properties.forEach((prop) => {
|
||||
if (isInput && prop === 'lineHeight') {
|
||||
// Special case for <input>s because text is rendered centered and line height may be != height
|
||||
if (computed.boxSizing === 'border-box') {
|
||||
const height = Number.parseInt(computed.height as string)
|
||||
const outerHeight =
|
||||
Number.parseInt(computed.paddingTop as string) +
|
||||
Number.parseInt(computed.paddingBottom as string) +
|
||||
Number.parseInt(computed.borderTopWidth as string) +
|
||||
Number.parseInt(computed.borderBottomWidth as string)
|
||||
const targetHeight =
|
||||
outerHeight + Number.parseInt(computed.lineHeight as string)
|
||||
if (height > targetHeight) {
|
||||
style.lineHeight = `${height - outerHeight}px`
|
||||
} else if (height === targetHeight) {
|
||||
style.lineHeight = computed.lineHeight
|
||||
} else {
|
||||
style.lineHeight = '0'
|
||||
}
|
||||
} else {
|
||||
style.lineHeight = computed.height
|
||||
}
|
||||
} else {
|
||||
style[prop as any] = computed[prop as any]
|
||||
}
|
||||
})
|
||||
|
||||
if (isFirefox()) {
|
||||
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
|
||||
if (element.scrollHeight > Number.parseInt(computed.height as string)) {
|
||||
style.overflowY = 'scroll'
|
||||
}
|
||||
} else {
|
||||
style.overflow = 'hidden' // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
|
||||
}
|
||||
|
||||
div.textContent = element.value.slice(0, Math.max(0, position))
|
||||
// The second special handling for input type="text" vs textarea:
|
||||
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
|
||||
if (isInput && div.textContent) {
|
||||
div.textContent = div.textContent.replace(/\s/g, '\u00A0')
|
||||
}
|
||||
|
||||
const span = document.createElement('span')
|
||||
// Wrapping must be replicated *exactly*, including when a long word gets
|
||||
// onto the next line, with whitespace at the end of the line before (#7).
|
||||
// The *only* reliable way to do that is to copy the *entire* rest of the
|
||||
// textareas content into the <span> created at the caret position.
|
||||
// For inputs, just '.' would be enough, but no need to bother.
|
||||
span.textContent = element.value.slice(Math.max(0, position)) || '.' // || because a completely empty faux span doesn't render at all
|
||||
span.style.position = 'relative'
|
||||
span.style.left = `${-element.scrollLeft}px`
|
||||
span.style.top = `${-element.scrollTop}px`
|
||||
div.appendChild(span)
|
||||
|
||||
const relativePosition = {
|
||||
top: span.offsetTop + Number.parseInt(computed.borderTopWidth as string),
|
||||
left: span.offsetLeft + Number.parseInt(computed.borderLeftWidth as string),
|
||||
// We don't use line-height since it may be too large for position. Eg. 34px
|
||||
// for input
|
||||
height: Number.parseInt(computed.fontSize as string) * 1.5,
|
||||
}
|
||||
|
||||
if (options.debug) {
|
||||
span.style.backgroundColor = '#aaa'
|
||||
} else {
|
||||
document.body.removeChild(div)
|
||||
}
|
||||
|
||||
if (relativePosition.left >= element.clientWidth) {
|
||||
relativePosition.left = element.clientWidth
|
||||
}
|
||||
return relativePosition
|
||||
}
|
16
packages/components/mention/src/mention-dropdown.ts
Normal file
16
packages/components/mention/src/mention-dropdown.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { buildProps, definePropType, isString } from '@element-plus/utils'
|
||||
|
||||
import type { MentionOption } from './types'
|
||||
|
||||
export const mentionDropdownProps = buildProps({
|
||||
options: {
|
||||
type: definePropType<MentionOption[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
loading: Boolean,
|
||||
disabled: Boolean,
|
||||
})
|
||||
|
||||
export const mentionDropdownEmits = {
|
||||
select: (option: MentionOption) => isString(option.value),
|
||||
}
|
141
packages/components/mention/src/mention-dropdown.vue
Normal file
141
packages/components/mention/src/mention-dropdown.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div ref="dropdownRef" :class="ns.b('dropdown')">
|
||||
<div v-if="$slots.header" :class="ns.be('dropdown', 'header')">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<el-scrollbar
|
||||
v-show="options.length > 0 && !loading"
|
||||
ref="scrollbarRef"
|
||||
tag="ul"
|
||||
:wrap-class="ns.be('dropdown', 'wrap')"
|
||||
:view-class="ns.be('dropdown', 'list')"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in options"
|
||||
ref="optionRefs"
|
||||
:key="item.value"
|
||||
:class="optionkls(item, index)"
|
||||
@mouseenter="handleMouseEnter(index)"
|
||||
@click.stop="handleSelect(item)"
|
||||
>
|
||||
<slot name="label" :item="item" :index="index">
|
||||
<span>{{ item.label ?? item.value }}</span>
|
||||
</slot>
|
||||
</li>
|
||||
</el-scrollbar>
|
||||
<div v-if="loading" :class="ns.be('dropdown', 'loading')">
|
||||
<slot name="loading"> {{ t('el.mention.loading') }} </slot>
|
||||
</div>
|
||||
<div v-if="$slots.footer" :class="ns.be('dropdown', 'footer')">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { scrollIntoView } from '@element-plus/utils'
|
||||
import ElScrollbar from '@element-plus/components/scrollbar'
|
||||
import { mentionDropdownEmits, mentionDropdownProps } from './mention-dropdown'
|
||||
|
||||
import type { MentionOption } from './types'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElMentionDropdown',
|
||||
})
|
||||
|
||||
const props = defineProps(mentionDropdownProps)
|
||||
const emit = defineEmits(mentionDropdownEmits)
|
||||
|
||||
const ns = useNamespace('mention')
|
||||
const { t } = useLocale()
|
||||
const hoveringIndex = ref(-1)
|
||||
|
||||
const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
|
||||
const optionRefs = ref<HTMLElement[]>()
|
||||
const dropdownRef = ref<HTMLElement>()
|
||||
|
||||
const optionkls = (item: MentionOption, index: number) => [
|
||||
ns.be('dropdown', 'item'),
|
||||
ns.is('hovering', hoveringIndex.value === index),
|
||||
ns.is('disabled', item.disabled || props.disabled),
|
||||
]
|
||||
|
||||
const handleSelect = (item: MentionOption) => {
|
||||
if (item.disabled || props.disabled) return
|
||||
emit('select', item)
|
||||
}
|
||||
|
||||
const handleMouseEnter = (index: number) => {
|
||||
hoveringIndex.value = index
|
||||
}
|
||||
|
||||
const filteredAllDisabled = computed(
|
||||
() => props.disabled || props.options.every((item) => item.disabled)
|
||||
)
|
||||
|
||||
const hoverOption = computed(() => props.options[hoveringIndex.value])
|
||||
|
||||
const selectHoverOption = () => {
|
||||
if (!hoverOption.value) return
|
||||
emit('select', hoverOption.value)
|
||||
}
|
||||
|
||||
const navigateOptions = (direction: 'next' | 'prev') => {
|
||||
const { options } = props
|
||||
if (options.length === 0 || filteredAllDisabled.value) return
|
||||
|
||||
if (direction === 'next') {
|
||||
hoveringIndex.value++
|
||||
if (hoveringIndex.value === options.length) {
|
||||
hoveringIndex.value = 0
|
||||
}
|
||||
} else if (direction === 'prev') {
|
||||
hoveringIndex.value--
|
||||
if (hoveringIndex.value < 0) {
|
||||
hoveringIndex.value = options.length - 1
|
||||
}
|
||||
}
|
||||
const option = options[hoveringIndex.value]
|
||||
if (option.disabled) {
|
||||
navigateOptions(direction)
|
||||
return
|
||||
}
|
||||
nextTick(() => scrollToOption(option))
|
||||
}
|
||||
|
||||
const scrollToOption = (option: MentionOption) => {
|
||||
const { options } = props
|
||||
|
||||
const index = options.findIndex((item) => item.value === option.value)
|
||||
const target = optionRefs.value?.[index]
|
||||
|
||||
if (target) {
|
||||
const menu = dropdownRef.value?.querySelector?.(
|
||||
`.${ns.be('dropdown', 'wrap')}`
|
||||
)
|
||||
if (menu) {
|
||||
scrollIntoView(menu as HTMLElement, target)
|
||||
}
|
||||
}
|
||||
scrollbarRef.value?.handleScroll()
|
||||
}
|
||||
|
||||
const resetHoveringIndex = () => {
|
||||
if (filteredAllDisabled.value || props.options.length === 0) {
|
||||
hoveringIndex.value = -1
|
||||
} else {
|
||||
hoveringIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.options, resetHoveringIndex, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
navigateOptions,
|
||||
selectHoverOption,
|
||||
hoverOption,
|
||||
})
|
||||
</script>
|
123
packages/components/mention/src/mention.ts
Normal file
123
packages/components/mention/src/mention.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import {
|
||||
buildProps,
|
||||
definePropType,
|
||||
isFunction,
|
||||
isString,
|
||||
} from '@element-plus/utils'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { inputProps } from '@element-plus/components/input'
|
||||
import { filterOption } from './helper'
|
||||
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Mention from './mention.vue'
|
||||
import type { MentionOption } from './types'
|
||||
import type { Options } from '@element-plus/components/popper'
|
||||
|
||||
export const mentionProps = buildProps({
|
||||
...inputProps,
|
||||
/**
|
||||
* @description mention options list
|
||||
*/
|
||||
options: {
|
||||
type: definePropType<MentionOption[]>(Array),
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* @description prefix character to trigger mentions. The string length must be exactly 1.
|
||||
*/
|
||||
prefix: {
|
||||
type: definePropType<string | string[]>([String, Array]),
|
||||
default: '@',
|
||||
validator: (val: string | string[]) => {
|
||||
if (isString(val)) return val.length === 1
|
||||
return val.every((v) => isString(v) && v.length === 1)
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @description character to split mentions. The string length must be exactly 1.
|
||||
*/
|
||||
split: {
|
||||
type: String,
|
||||
default: ' ',
|
||||
validator: (val: string) => val.length === 1,
|
||||
},
|
||||
/**
|
||||
* @description customize filter option logic.
|
||||
*/
|
||||
filterOption: {
|
||||
type: definePropType<false | typeof filterOption>([Boolean, Function]),
|
||||
default: () => filterOption,
|
||||
validator: (val) => {
|
||||
if (val === false) return true
|
||||
return isFunction(val)
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @description set popup placement
|
||||
*/
|
||||
placement: {
|
||||
type: definePropType<'bottom' | 'top'>(String),
|
||||
default: 'bottom',
|
||||
},
|
||||
/**
|
||||
* @description whether the dropdown panel has an arrow
|
||||
*/
|
||||
showArrow: Boolean,
|
||||
/**
|
||||
* @description offset of the dropdown panel
|
||||
*/
|
||||
offset: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
/**
|
||||
* @description when backspace is pressed to delete, whether the mention content is deleted as a whole
|
||||
*/
|
||||
whole: Boolean,
|
||||
/**
|
||||
* @description when backspace is pressed to delete, check if the mention is a whole
|
||||
*/
|
||||
checkIsWhole: {
|
||||
type: definePropType<(pattern: string, prefix: string) => boolean>(
|
||||
Function
|
||||
),
|
||||
},
|
||||
/**
|
||||
* @description input value
|
||||
*/
|
||||
modelValue: String,
|
||||
/**
|
||||
* @description whether the dropdown panel of mentions is in a loading state.
|
||||
*/
|
||||
loading: Boolean,
|
||||
/**
|
||||
* @description custom class name for dropdown panel
|
||||
*/
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* @description [popper.js](https://popper.js.org/docs/v2/) parameters
|
||||
*/
|
||||
popperOptions: {
|
||||
type: definePropType<Partial<Options>>(Object),
|
||||
default: () => ({} as Partial<Options>),
|
||||
},
|
||||
})
|
||||
|
||||
export const mentionEmits = {
|
||||
[UPDATE_MODEL_EVENT]: (value: string) => isString(value),
|
||||
search: (pattern: string, prefix: string) =>
|
||||
isString(pattern) && isString(prefix),
|
||||
select: (option: MentionOption, prefix: string) =>
|
||||
isString(option.value) && isString(prefix),
|
||||
focus: (evt: FocusEvent) => evt instanceof FocusEvent,
|
||||
blur: (evt: FocusEvent) => evt instanceof FocusEvent,
|
||||
}
|
||||
|
||||
export type MentionEmits = typeof mentionEmits
|
||||
export type MentionProps = ExtractPropTypes<typeof mentionProps>
|
||||
export type MentionInstance = InstanceType<typeof Mention>
|
||||
|
||||
export type { MentionOption } from './types'
|
251
packages/components/mention/src/mention.vue
Normal file
251
packages/components/mention/src/mention.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div ref="wrapperRef" :class="ns.b()">
|
||||
<el-input
|
||||
v-bind="mergeProps(passInputProps, $attrs)"
|
||||
ref="elInputRef"
|
||||
:model-value="modelValue"
|
||||
@input="handleInputChange"
|
||||
@keydown="handleInputKeyDown"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@mousedown="handleInputMouseDown"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip
|
||||
ref="tooltipRef"
|
||||
:visible="visible && (!!filteredOptions.length || loading)"
|
||||
:popper-class="[ns.e('popper'), popperClass]"
|
||||
:popper-options="popperOptions"
|
||||
:placement="computedPlacement"
|
||||
:fallback-placements="computedFallbackPlacements"
|
||||
effect="light"
|
||||
pure
|
||||
:offset="offset"
|
||||
:show-arrow="showArrow"
|
||||
>
|
||||
<template #default>
|
||||
<div :style="cursorStyle" />
|
||||
</template>
|
||||
<template #content>
|
||||
<el-mention-dropdown
|
||||
ref="dropdownRef"
|
||||
:options="filteredOptions"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
@select="handleSelect"
|
||||
@click.stop="elInputRef?.focus"
|
||||
>
|
||||
<template v-for="(_, name) in $slots" #[name]="slotProps">
|
||||
<slot :name="name" v-bind="slotProps" />
|
||||
</template>
|
||||
</el-mention-dropdown>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, mergeProps, nextTick, ref } from 'vue'
|
||||
import { pick } from 'lodash-unified'
|
||||
import { useFocusController, useNamespace } from '@element-plus/hooks'
|
||||
import ElInput, { inputProps } from '@element-plus/components/input'
|
||||
import ElTooltip from '@element-plus/components/tooltip'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import { isFunction } from '@element-plus/utils'
|
||||
import { mentionEmits, mentionProps } from './mention'
|
||||
import { getCursorPosition, getMentionCtx } from './helper'
|
||||
import ElMentionDropdown from './mention-dropdown.vue'
|
||||
|
||||
import type { Placement } from '@popperjs/core'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type { InputInstance } from '@element-plus/components/input'
|
||||
import type { TooltipInstance } from '@element-plus/components/tooltip'
|
||||
import type { MentionCtx, MentionOption } from './types'
|
||||
|
||||
defineOptions({
|
||||
name: 'ElMention',
|
||||
})
|
||||
|
||||
const props = defineProps(mentionProps)
|
||||
const emit = defineEmits(mentionEmits)
|
||||
|
||||
const passInputProps = computed(() => pick(props, Object.keys(inputProps)))
|
||||
|
||||
const ns = useNamespace('mention')
|
||||
|
||||
const elInputRef = ref<InputInstance>()
|
||||
const tooltipRef = ref<TooltipInstance>()
|
||||
const dropdownRef = ref<InstanceType<typeof ElMentionDropdown>>()
|
||||
|
||||
const visible = ref(false)
|
||||
const cursorStyle = ref<CSSProperties>()
|
||||
const mentionCtx = ref<MentionCtx>()
|
||||
|
||||
const computedPlacement = computed<Placement>(() =>
|
||||
props.showArrow ? props.placement : `${props.placement}-start`
|
||||
)
|
||||
|
||||
const computedFallbackPlacements = computed<Placement[]>(() =>
|
||||
props.showArrow ? ['bottom', 'top'] : ['bottom-start', 'top-start']
|
||||
)
|
||||
|
||||
const filteredOptions = computed(() => {
|
||||
const { filterOption, options } = props
|
||||
if (!mentionCtx.value || !filterOption) return options
|
||||
return options.filter((option) =>
|
||||
filterOption(mentionCtx.value!.pattern, option)
|
||||
)
|
||||
})
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
emit('update:modelValue', value)
|
||||
syncAfterCursorMove()
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e: KeyboardEvent | Event) => {
|
||||
if (!('key' in e)) return
|
||||
if (elInputRef.value?.isComposing) return
|
||||
if (['ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
syncAfterCursorMove()
|
||||
} else if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
if (!visible.value) return
|
||||
e.preventDefault()
|
||||
const direction = e.key === 'ArrowUp' ? 'prev' : 'next'
|
||||
dropdownRef.value?.navigateOptions(direction)
|
||||
} else if (['Enter'].includes(e.key)) {
|
||||
if (!visible.value) return
|
||||
e.preventDefault()
|
||||
if (dropdownRef.value?.hoverOption) {
|
||||
dropdownRef.value?.selectHoverOption()
|
||||
} else {
|
||||
visible.value = false
|
||||
}
|
||||
} else if (['Backspace'].includes(e.key)) {
|
||||
if (props.whole && mentionCtx.value) {
|
||||
const { splitIndex, selectionEnd, pattern, prefixIndex, prefix } =
|
||||
mentionCtx.value
|
||||
const inputEl = getInputEl()
|
||||
if (!inputEl) return
|
||||
const inputValue = inputEl.value
|
||||
const matchOption = props.options.find((item) => item.value === pattern)
|
||||
const isWhole = isFunction(props.checkIsWhole)
|
||||
? props.checkIsWhole(pattern, prefix)
|
||||
: matchOption
|
||||
if (isWhole && splitIndex !== -1 && splitIndex + 1 === selectionEnd) {
|
||||
e.preventDefault()
|
||||
const newValue =
|
||||
inputValue.slice(0, prefixIndex) + inputValue.slice(splitIndex + 1)
|
||||
emit(UPDATE_MODEL_EVENT, newValue)
|
||||
|
||||
const newSelectionEnd = prefixIndex
|
||||
nextTick(() => {
|
||||
// input value is updated
|
||||
inputEl.selectionStart = newSelectionEnd
|
||||
inputEl.selectionEnd = newSelectionEnd
|
||||
syncDropdownVisible()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { wrapperRef, handleFocus, handleBlur } = useFocusController(elInputRef, {
|
||||
afterFocus() {
|
||||
syncAfterCursorMove()
|
||||
},
|
||||
beforeBlur(event) {
|
||||
return tooltipRef.value?.isFocusInsideContent(event)
|
||||
},
|
||||
afterBlur() {
|
||||
visible.value = false
|
||||
},
|
||||
})
|
||||
|
||||
const handleInputMouseDown = () => {
|
||||
syncAfterCursorMove()
|
||||
}
|
||||
|
||||
const handleSelect = (item: MentionOption) => {
|
||||
if (!mentionCtx.value) return
|
||||
const inputEl = getInputEl()
|
||||
if (!inputEl) return
|
||||
const inputValue = inputEl.value
|
||||
const { split } = props
|
||||
|
||||
const newEndPart = inputValue.slice(mentionCtx.value.end)
|
||||
const alreadySeparated = newEndPart.startsWith(split)
|
||||
const newMiddlePart = `${item.value}${alreadySeparated ? '' : split}`
|
||||
|
||||
const newValue =
|
||||
inputValue.slice(0, mentionCtx.value.start) + newMiddlePart + newEndPart
|
||||
|
||||
emit(UPDATE_MODEL_EVENT, newValue)
|
||||
emit('select', item, mentionCtx.value.prefix)
|
||||
|
||||
const newSelectionEnd =
|
||||
mentionCtx.value.start + newMiddlePart.length + (alreadySeparated ? 1 : 0)
|
||||
|
||||
nextTick(() => {
|
||||
// input value is updated
|
||||
inputEl.selectionStart = newSelectionEnd
|
||||
inputEl.selectionEnd = newSelectionEnd
|
||||
inputEl.focus()
|
||||
syncDropdownVisible()
|
||||
})
|
||||
}
|
||||
|
||||
const getInputEl = () =>
|
||||
props.type === 'textarea'
|
||||
? elInputRef.value?.textarea
|
||||
: elInputRef.value?.input
|
||||
|
||||
const syncAfterCursorMove = () => {
|
||||
// can't use nextTick(), get cursor position will be wrong
|
||||
setTimeout(() => {
|
||||
syncCursor()
|
||||
syncDropdownVisible()
|
||||
nextTick(() => tooltipRef.value?.updatePopper())
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const syncCursor = () => {
|
||||
const inputEl = getInputEl()
|
||||
if (!inputEl) return
|
||||
|
||||
const caretPosition = getCursorPosition(inputEl)
|
||||
const inputRect = inputEl.getBoundingClientRect()
|
||||
const elInputRect = elInputRef.value!.$el.getBoundingClientRect()
|
||||
|
||||
cursorStyle.value = {
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: `${caretPosition.height}px`,
|
||||
left: `${caretPosition.left + inputRect.left - elInputRect.left}px`,
|
||||
top: `${caretPosition.top + inputRect.top - elInputRect.top}px`,
|
||||
}
|
||||
}
|
||||
|
||||
const syncDropdownVisible = () => {
|
||||
const inputEl = getInputEl()
|
||||
if (document.activeElement !== inputEl) {
|
||||
visible.value = false
|
||||
return
|
||||
}
|
||||
const { prefix, split } = props
|
||||
mentionCtx.value = getMentionCtx(inputEl, prefix, split)
|
||||
if (mentionCtx.value && mentionCtx.value.splitIndex === -1) {
|
||||
visible.value = true
|
||||
emit('search', mentionCtx.value.pattern, mentionCtx.value.prefix)
|
||||
return
|
||||
}
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
input: elInputRef,
|
||||
tooltip: tooltipRef,
|
||||
})
|
||||
</script>
|
16
packages/components/mention/src/types.ts
Normal file
16
packages/components/mention/src/types.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export type MentionOption = {
|
||||
value: string
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface MentionCtx {
|
||||
pattern: string
|
||||
start: number
|
||||
end: number
|
||||
prefix: string
|
||||
prefixIndex: number
|
||||
splitIndex: number
|
||||
selectionEnd: number
|
||||
}
|
5
packages/components/mention/style/css.ts
Normal file
5
packages/components/mention/style/css.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import '@element-plus/components/base/style/css'
|
||||
import '@element-plus/components/input/style/css'
|
||||
import '@element-plus/components/tooltip/style/css'
|
||||
import '@element-plus/components/scrollbar/style/css'
|
||||
import '@element-plus/theme-chalk/el-mention.css'
|
5
packages/components/mention/style/index.ts
Normal file
5
packages/components/mention/style/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import '@element-plus/components/base/style'
|
||||
import '@element-plus/components/input/style'
|
||||
import '@element-plus/components/tooltip/style'
|
||||
import '@element-plus/components/scrollbar/style'
|
||||
import '@element-plus/theme-chalk/src/mention.scss'
|
@ -106,6 +106,7 @@ import { ElWatermark } from '@element-plus/components/watermark'
|
||||
import { ElTour, ElTourStep } from '@element-plus/components/tour'
|
||||
import { ElAnchor, ElAnchorLink } from '@element-plus/components/anchor'
|
||||
import { ElSegmented } from '@element-plus/components/segmented'
|
||||
import { ElMention } from '@element-plus/components/mention'
|
||||
|
||||
import type { Plugin } from 'vue'
|
||||
|
||||
@ -214,4 +215,5 @@ export default [
|
||||
ElAnchor,
|
||||
ElAnchorLink,
|
||||
ElSegmented,
|
||||
ElMention,
|
||||
] as Plugin[]
|
||||
|
@ -13,7 +13,7 @@ interface UseFocusControllerOptions {
|
||||
afterBlur?: () => void
|
||||
}
|
||||
|
||||
export function useFocusController<T extends HTMLElement>(
|
||||
export function useFocusController<T extends { focus: () => void }>(
|
||||
target: ShallowRef<T | undefined>,
|
||||
{ afterFocus, beforeBlur, afterBlur }: UseFocusControllerOptions = {}
|
||||
) {
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Geen data',
|
||||
placeholder: 'Kies',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Laai',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Geen toepaslike data',
|
||||
loading: 'Laai',
|
||||
|
@ -78,6 +78,9 @@ export default {
|
||||
noData: 'لايوجد بيانات',
|
||||
placeholder: 'إختر',
|
||||
},
|
||||
mention: {
|
||||
loading: 'جار التحميل',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: 'تبديل القائمة',
|
||||
},
|
||||
|
@ -78,6 +78,9 @@ export default {
|
||||
noData: 'لايوجد بيانات',
|
||||
placeholder: 'إختر',
|
||||
},
|
||||
mention: {
|
||||
loading: 'جار التحميل',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: 'تبديل القائمة',
|
||||
},
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Məlumat yoxdur',
|
||||
placeholder: 'Seç',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Yüklənir',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nəticə tapılmadı',
|
||||
loading: 'Yüklənir',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Няма данни',
|
||||
placeholder: 'Избери',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Зареждане',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Няма намерени',
|
||||
loading: 'Зареждане',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'কোন ডাটা নেই',
|
||||
placeholder: 'নির্বাচন করুন',
|
||||
},
|
||||
mention: {
|
||||
loading: 'লোড হচ্ছে',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'কোন মিল পওয়া যায়নি',
|
||||
loading: 'লোড হচ্ছে',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Sense Dades',
|
||||
placeholder: 'Seleccionar',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Carregant',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'No hi ha dades que coincideixin',
|
||||
loading: 'Carregant',
|
||||
|
@ -89,6 +89,9 @@ export default {
|
||||
noData: 'هیچ داتایەک نیە',
|
||||
placeholder: 'هەڵبژاردن',
|
||||
},
|
||||
mention: {
|
||||
loading: 'بارکردن',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: 'کردنەوەو داخستنی کشاو',
|
||||
},
|
||||
|
@ -70,6 +70,9 @@ export default {
|
||||
noData: 'Žádná data',
|
||||
placeholder: 'Vybrat',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Načítání',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Žádná shoda',
|
||||
loading: 'Načítání',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Ingen data',
|
||||
placeholder: 'Vælg',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Henter',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Ingen matchende data',
|
||||
loading: 'Henter',
|
||||
|
@ -70,6 +70,9 @@ export default {
|
||||
noData: 'Keine Daten',
|
||||
placeholder: 'Daten wählen',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Lädt.',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nichts gefunden.',
|
||||
loading: 'Lädt.',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Χωρίς δεδομένα',
|
||||
placeholder: 'Επιλογή',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Φόρτωση',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
|
||||
loading: 'Φόρτωση',
|
||||
|
@ -90,6 +90,9 @@ export default {
|
||||
noData: 'No data',
|
||||
placeholder: 'Select',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Loading',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: 'Toggle Dropdown',
|
||||
},
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Neniuj datumoj',
|
||||
placeholder: 'Bonvolu elekti',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Ŝarĝante',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Neniuj kongruaj datumoj',
|
||||
loading: 'Ŝarĝante',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Sin datos',
|
||||
placeholder: 'Seleccionar',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Cargando',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'No hay datos que coincidan',
|
||||
loading: 'Cargando',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Andmed puuduvad',
|
||||
placeholder: 'Vali',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Laadimine',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Sobivad andmed puuduvad',
|
||||
loading: 'Laadimine',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Daturik ez',
|
||||
placeholder: 'Hautatu',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Kargatzen',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Bat datorren daturik ez',
|
||||
loading: 'Kargatzen',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'اطلاعاتی وجود ندارد',
|
||||
placeholder: 'انتخاب کنید',
|
||||
},
|
||||
mention: {
|
||||
loading: 'در حال بارگذاری',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'هیچ نتیجهای پیدا نشد',
|
||||
loading: 'در حال بارگذاری',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Ei tietoja',
|
||||
placeholder: 'Valitse',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Lataa',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Ei vastaavia tietoja',
|
||||
loading: 'Lataa',
|
||||
|
@ -91,6 +91,9 @@ export default {
|
||||
noData: 'Aucune donnée',
|
||||
placeholder: 'Choisir',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Chargement',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Aucune correspondance',
|
||||
loading: 'Chargement',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'אין נתונים',
|
||||
placeholder: 'שומר מקום',
|
||||
},
|
||||
mention: {
|
||||
loading: 'טוען',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'לא נמצאה התאמה',
|
||||
loading: 'טוען',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Nema podataka',
|
||||
placeholder: 'Izaberi',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Učitavanje',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nema pronađenih podataka',
|
||||
loading: 'Učitavanje',
|
||||
|
@ -67,6 +67,9 @@ export default {
|
||||
noData: 'Nincs adat',
|
||||
placeholder: 'Válassz',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Betöltés',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nincs találat',
|
||||
loading: 'Betöltés',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Տվյալներ չկան',
|
||||
placeholder: 'Ընտրել',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Բեռնում',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Համապատասխան տուեալներ չկան',
|
||||
loading: 'Բեռնում',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Tidak ada data',
|
||||
placeholder: 'Pilih',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Memuat',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Tidak ada data yg cocok',
|
||||
loading: 'Memuat',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Nessun dato',
|
||||
placeholder: 'Seleziona',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Caricamento',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nessuna corrispondenza',
|
||||
loading: 'Caricamento',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'データなし',
|
||||
placeholder: '選択してください',
|
||||
},
|
||||
mention: {
|
||||
loading: 'ロード中',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'データなし',
|
||||
loading: 'ロード中',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Деректер жоқ',
|
||||
placeholder: 'Таңдаңыз',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Жүктелуде',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Сәйкес деректер жоқ',
|
||||
loading: 'Жүктелуде',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'គ្មានទិន្នន័យ',
|
||||
placeholder: 'ជ្រើសរើស',
|
||||
},
|
||||
mention: {
|
||||
loading: 'កំពុងផ្ទុក',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'គ្មានទិន្នន័យដូច',
|
||||
loading: 'កំពុងផ្ទុក',
|
||||
|
@ -79,6 +79,9 @@ export default {
|
||||
noData: '데이터 없음',
|
||||
placeholder: '선택',
|
||||
},
|
||||
mention: {
|
||||
loading: '불러오는 중',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: '드롭다운 전환',
|
||||
},
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Agahî tune',
|
||||
placeholder: 'Bibijêre',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Bardibe',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Li hembere ve agahî tune',
|
||||
loading: 'Bardibe',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'маалымат жок',
|
||||
placeholder: 'тандоо',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Жүктөлүүдө',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Дал келген маалыматтар',
|
||||
loading: 'Жүктөлүүдө',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Nėra duomenų',
|
||||
placeholder: 'Pasirink',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Kraunasi',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Duomenų nerasta',
|
||||
loading: 'Kraunasi',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Nav datu',
|
||||
placeholder: 'Izvēlēties',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Ielādē',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nav atbilstošu datu',
|
||||
loading: 'Ielādē',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Tsy misy angona',
|
||||
placeholder: 'Safidy',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Eo ampiandrasana',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Tsy misy angona mifanentana',
|
||||
loading: 'Eo ampiandrasana',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Өгөгдөл байхгүй',
|
||||
placeholder: 'Сонгох',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Ачаалж байна',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Тохирох өгөгдөл байхгүй',
|
||||
loading: 'Ачаалж байна',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Ingen resulater',
|
||||
placeholder: 'Velg',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Laster',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Ingen samsvarende resultater',
|
||||
loading: 'Laster',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Geen data',
|
||||
placeholder: 'Selecteer',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Laden',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Geen overeenkomende resultaten',
|
||||
loading: 'Laden',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'هیڅ معلومات نشته',
|
||||
placeholder: 'ځای لرونکی',
|
||||
},
|
||||
mention: {
|
||||
loading: 'بار کول',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'هیڅه ونه موندل شول',
|
||||
loading: 'بار کول',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Brak danych',
|
||||
placeholder: 'Wybierz',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Ładowanie',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Brak dopasowań',
|
||||
loading: 'Ładowanie',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Sem dados',
|
||||
placeholder: 'Selecione',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Carregando',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Sem resultados',
|
||||
loading: 'Carregando',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Sem dados',
|
||||
placeholder: 'Selecione',
|
||||
},
|
||||
mention: {
|
||||
loading: 'A carregar',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Sem correspondência',
|
||||
loading: 'A carregar',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Nu există date',
|
||||
placeholder: 'Selectează',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Se încarcă',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Nu există date potrivite',
|
||||
loading: 'Se încarcă',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Нет данных',
|
||||
placeholder: 'Выбрать',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Загрузка',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Совпадений не найдено',
|
||||
loading: 'Загрузка',
|
||||
|
@ -70,6 +70,9 @@ export default {
|
||||
noData: 'Žiadne dáta',
|
||||
placeholder: 'Vybrať',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Načítavanie',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Žiadna zhoda',
|
||||
loading: 'Načítavanie',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Ni podatkov',
|
||||
placeholder: 'Izberi',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Nalaganje',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Ni ustreznih podatkov',
|
||||
loading: 'Nalaganje',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Нема података',
|
||||
placeholder: 'Изабери',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Учитавање',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Нема резултата',
|
||||
loading: 'Учитавање',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Ingen data',
|
||||
placeholder: 'Välj',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Laddar',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Hittade inget',
|
||||
loading: 'Laddar',
|
||||
|
@ -90,6 +90,9 @@ export default {
|
||||
noData: 'hakuna data',
|
||||
placeholder: 'chagua',
|
||||
},
|
||||
mention: {
|
||||
loading: 'inapakia',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: 'geuza kunyuzi',
|
||||
},
|
||||
|
@ -67,6 +67,9 @@ export default {
|
||||
noData: 'தரவு இல்லை',
|
||||
placeholder: 'தேர்வு செய்',
|
||||
},
|
||||
mention: {
|
||||
loading: 'தயாராகிக்கொண்டிருக்கிறது',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'பொருத்தமான தரவு கிடைக்கவில்லை',
|
||||
loading: 'தயாராகிக்கொண்டிருக்கிறது',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'ไม่พบข้อมูล',
|
||||
placeholder: 'เลือก',
|
||||
},
|
||||
mention: {
|
||||
loading: 'กำลังโหลด',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
|
||||
loading: 'กำลังโหลด',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Hiçzat ýok',
|
||||
placeholder: 'Saýla',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Indirilýär',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Hiçzat tapylmady',
|
||||
loading: 'Indirilýär',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Veri yok',
|
||||
placeholder: 'Seç',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Yükleniyor',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Eşleşen veri bulunamadı',
|
||||
loading: 'Yükleniyor',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'ئۇچۇر يوق',
|
||||
placeholder: 'تاللاڭ',
|
||||
},
|
||||
mention: {
|
||||
loading: 'يۈكلىنىۋاتىدۇ',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'ئۇچۇر تېپىلمىدى',
|
||||
loading: 'يۈكلىنىۋاتىدۇ',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Немає даних',
|
||||
placeholder: 'Обрати',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Завантаження',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Співпадінь не знайдено',
|
||||
loading: 'Завантаження',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Maʼlumot yoʻq',
|
||||
placeholder: 'Tanladizngiz',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Yuklanmoqda',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Mos maʼlumot topilmadi',
|
||||
loading: 'Yuklanmoqda',
|
||||
|
@ -68,6 +68,9 @@ export default {
|
||||
noData: 'Không tìm thấy dữ liệu',
|
||||
placeholder: 'Chọn',
|
||||
},
|
||||
mention: {
|
||||
loading: 'Đang tải',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: 'Dữ liệu không phù hợp',
|
||||
loading: 'Đang tải',
|
||||
|
@ -71,6 +71,9 @@ export default {
|
||||
noData: '无数据',
|
||||
placeholder: '请选择',
|
||||
},
|
||||
mention: {
|
||||
loading: '加载中',
|
||||
},
|
||||
cascader: {
|
||||
noMatch: '无匹配数据',
|
||||
loading: '加载中',
|
||||
|
@ -87,6 +87,9 @@ export default {
|
||||
noData: '無資料',
|
||||
placeholder: '請選擇',
|
||||
},
|
||||
mention: {
|
||||
loading: '載入中',
|
||||
},
|
||||
dropdown: {
|
||||
toggleDropdown: '切換下拉選單',
|
||||
},
|
||||
|
@ -851,6 +851,31 @@ $segmented: map.merge(
|
||||
$segmented
|
||||
);
|
||||
|
||||
// Mention
|
||||
// css3 var in packages/theme-chalk/src/mention.scss
|
||||
$mention: () !default;
|
||||
$mention: map.merge(
|
||||
(
|
||||
'font-size': getCssVar('font-size-base'),
|
||||
'bg-color': getCssVar('bg-color', 'overlay'),
|
||||
'shadow': getCssVar('box-shadow-light'),
|
||||
'border': 1px solid getCssVar('border-color-light'),
|
||||
'option-color': getCssVar('text-color-regular'),
|
||||
'option-height': 34px,
|
||||
'option-min-width': 100px,
|
||||
'option-hover-background': getCssVar('fill-color', 'light'),
|
||||
'option-selected-color': getCssVar('color-primary'),
|
||||
'option-disabled-color': getCssVar('text-color-placeholder'),
|
||||
'option-loading-color': getCssVar('text-color-secondary'),
|
||||
'option-loading-padding': 10px 0,
|
||||
'max-height': 174px,
|
||||
'padding': 6px 0,
|
||||
'header-padding': 10px,
|
||||
'footer-padding': 10px,
|
||||
),
|
||||
$mention
|
||||
);
|
||||
|
||||
// Table
|
||||
// css3 var in packages/theme-chalk/src/table.scss
|
||||
$table: () !default;
|
||||
|
@ -107,3 +107,4 @@
|
||||
@use './anchor.scss';
|
||||
@use './anchor-link.scss';
|
||||
@use './segmented.scss';
|
||||
@use './mention.scss';
|
||||
|
90
packages/theme-chalk/src/mention.scss
Normal file
90
packages/theme-chalk/src/mention.scss
Normal file
@ -0,0 +1,90 @@
|
||||
@use 'sass:map';
|
||||
|
||||
@use 'mixins/mixins' as *;
|
||||
@use 'mixins/var' as *;
|
||||
@use 'common/var' as *;
|
||||
|
||||
@include b(mention) {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@include e(popper) {
|
||||
@include picker-popper(
|
||||
map.get($mention, 'bg-color'),
|
||||
map.get($mention, 'border'),
|
||||
map.get($mention, 'shadow')
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@include b(mention-dropdown) {
|
||||
|
||||
@include set-component-css-var('mention', $mention);
|
||||
|
||||
@include e(item) {
|
||||
font-size: getCssVar('mention-font-size');
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: getCssVar('mention-option-color');
|
||||
height: getCssVar('mention-option-height');
|
||||
line-height: getCssVar('mention-option-height');
|
||||
box-sizing: border-box;
|
||||
min-width: getCssVar('mention-option-min-width');
|
||||
cursor: pointer;
|
||||
|
||||
@include when(hovering) {
|
||||
background-color: getCssVar('mention-option-hover-background');
|
||||
}
|
||||
|
||||
@include when(selected) {
|
||||
color: getCssVar('mention-option-selected-color');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@include when(disabled) {
|
||||
color: getCssVar('mention-option-disabled-color');
|
||||
cursor: not-allowed;
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include b(mention-dropdown) {
|
||||
z-index: calc(#{getCssVar('index-top')} + 1);
|
||||
border-radius: getCssVar('border-radius-base');
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@include b(mention-dropdown__loading) {
|
||||
padding: 10px 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
color: getCssVar('mention-option-loading-color');
|
||||
font-size: 12px;
|
||||
min-width: getCssVar('mention-option-min-width');
|
||||
}
|
||||
|
||||
@include b(mention-dropdown__wrap) {
|
||||
max-height: getCssVar('mention-max-height');
|
||||
}
|
||||
|
||||
@include b(mention-dropdown__list) {
|
||||
list-style: none;
|
||||
padding: getCssVar('mention-padding');
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@include b(mention-dropdown__header) {
|
||||
padding: getCssVar('mention-header-padding');
|
||||
border-bottom: getCssVar('mention-border');
|
||||
}
|
||||
|
||||
@include b(mention-dropdown__footer) {
|
||||
padding: getCssVar('mention-footer-padding');
|
||||
border-top: getCssVar('mention-border');
|
||||
}
|
1
typings/components.d.ts
vendored
1
typings/components.d.ts
vendored
@ -101,6 +101,7 @@ declare module 'vue' {
|
||||
ElAnchor: typeof import('../packages/element-plus')['ElAnchor']
|
||||
ElAnchorLink: typeof import('../packages/element-plus')['ElAnchorLink']
|
||||
ElSegmented: typeof import('../packages/element-plus')['ElSegmented']
|
||||
ElMention: typeof import('../packages/element-plus')['ElMention']
|
||||
}
|
||||
|
||||
interface ComponentCustomProperties {
|
||||
|
Loading…
Reference in New Issue
Block a user