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:
赵添
2024-08-02 11:00:26 +08:00
committed by GitHub
parent 76ee10ab69
commit 0456c790db
91 changed files with 1737 additions and 12 deletions

View File

@@ -106,6 +106,11 @@
"link": "/input-number",
"text": "Input Number"
},
{
"link": "/mention",
"text": "Mention",
"promotion": "2.8.0"
},
{
"link": "/radio",
"text": "Radio"

View File

@@ -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,
}

View File

@@ -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>

View File

@@ -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']

View File

@@ -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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>