refactor(ui): improve textarea component
This commit is contained in:
parent
6e69a74b75
commit
61e6b3fa0f
|
@ -1,14 +1,22 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { nextTick, computed, ref, type ComputedRef } from 'vue'
|
import { nextTick, computed, ref, type ComputedRef, onMounted } from 'vue'
|
||||||
import { useTextareaAutosize, computedWithControl, useManualRefHistory, watchDebounced } from '@vueuse/core'
|
import { useTextareaAutosize, computedWithControl, useManualRefHistory, watchDebounced } from '@vueuse/core'
|
||||||
|
|
||||||
import Button from './Button.vue'
|
import Button from './Button.vue'
|
||||||
|
import Spacer from './Spacer.vue'
|
||||||
import Markdown from './Markdown.vue'
|
import Markdown from './Markdown.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from './Layout.vue'
|
||||||
|
|
||||||
const { max=Infinity, placeholder='', ...restProps } = defineProps<{ max?:number,placeholder?:string, label?:string }>()
|
const { charLimit = Infinity, placeholder = '', initialLines: minLines=3, ...props } = defineProps<{
|
||||||
|
label?: string,
|
||||||
|
placeholder?: string,
|
||||||
|
charLimit?: number,
|
||||||
|
initialLines?: number | string,
|
||||||
|
autofocus?: true,
|
||||||
|
required?: true
|
||||||
|
}>()
|
||||||
|
|
||||||
const model = defineModel<string>({ default: '' })
|
const model = defineModel<string>({ required: true })
|
||||||
|
|
||||||
const { undo, redo, commit: commitHistory, last } = useManualRefHistory(model)
|
const { undo, redo, commit: commitHistory, last } = useManualRefHistory(model)
|
||||||
const { textarea, triggerResize } = useTextareaAutosize({ input: model })
|
const { textarea, triggerResize } = useTextareaAutosize({ input: model })
|
||||||
|
@ -37,7 +45,7 @@ const lineNumber = computedWithControl(
|
||||||
const updateLineNumber = () => setTimeout(lineNumber.trigger, 0)
|
const updateLineNumber = () => setTimeout(lineNumber.trigger, 0)
|
||||||
|
|
||||||
const currentLine = computed({
|
const currentLine = computed({
|
||||||
get: () => model.value.split('\n')[lineNumber.value],
|
get: () => (model.value.split('\n')[model.value.split('\n').length>lineNumber.value ? lineNumber.value : 0]),
|
||||||
set: (line) => {
|
set: (line) => {
|
||||||
const content = model.value.split('\n')
|
const content = model.value.split('\n')
|
||||||
content[lineNumber.value] = line
|
content[lineNumber.value] = line
|
||||||
|
@ -175,17 +183,20 @@ const link = async () => {
|
||||||
|
|
||||||
// Fix focus
|
// Fix focus
|
||||||
const focus = () => textarea.value.focus()
|
const focus = () => textarea.value.focus()
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.autofocus) focus();
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Layout stack no-gap label
|
<Layout stack gap-8 label
|
||||||
class="funkwhale textarea-label"
|
class="funkwhale textarea-label"
|
||||||
>
|
>
|
||||||
<span v-if="$slots['label']" class="label">
|
<span v-if="$slots['label']" class="label">
|
||||||
<slot name="label" />
|
<slot name="label" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="restProps.label" class="label">
|
<span v-if="props.label" class="label">
|
||||||
{{ restProps.label }}
|
{{ props.label }}
|
||||||
</span>
|
</span>
|
||||||
<div :class="{ 'has-preview': preview }" class="funkwhale textarea" @mousedown.prevent="focus" @mouseup.prevent="focus">
|
<div :class="{ 'has-preview': preview }" class="funkwhale textarea" @mousedown.prevent="focus" @mouseup.prevent="focus">
|
||||||
<Markdown :md="model" class="preview" />
|
<Markdown :md="model" class="preview" />
|
||||||
|
@ -193,31 +204,47 @@ const focus = () => textarea.value.focus()
|
||||||
@keydown.right="updateLineNumber" @keydown.up="updateLineNumber" @keydown.down="updateLineNumber"
|
@keydown.right="updateLineNumber" @keydown.up="updateLineNumber" @keydown.down="updateLineNumber"
|
||||||
@keydown.enter="newline" @keydown.ctrl.shift.z.exact.prevent="redo" @keydown.ctrl.z.exact.prevent="undo"
|
@keydown.enter="newline" @keydown.ctrl.shift.z.exact.prevent="redo" @keydown.ctrl.z.exact.prevent="undo"
|
||||||
@keydown.ctrl.b.exact.prevent="bold" @keydown.ctrl.i.exact.prevent="italics"
|
@keydown.ctrl.b.exact.prevent="bold" @keydown.ctrl.i.exact.prevent="italics"
|
||||||
@keydown.ctrl.shift.x.exact.prevent="strikethrough" @keydown.ctrl.k.exact.prevent="link" :maxlength="max"
|
@keydown.ctrl.shift.x.exact.prevent="strikethrough" @keydown.ctrl.k.exact.prevent="link" :maxlength="charLimit"
|
||||||
:placeholder="placeholder" v-model="model" id="textarea_id" />
|
v-bind="$attrs"
|
||||||
<div class="textarea-buttons">
|
:required="required"
|
||||||
<Button ghost @click="preview = !preview" icon="bi-eye" color="secondary" :aria-pressed="preview || undefined" />
|
:placeholder="placeholder" v-model="model" id="textarea_id" :rows="initialLines"
|
||||||
|
:style="`min-height:${((typeof(initialLines) === 'string' ? parseInt(initialLines) : (initialLines ?? 3)) + 1.2) * 1.5}rem`"
|
||||||
<div class="separator" />
|
/>
|
||||||
|
<label class="textarea-buttons"
|
||||||
<Button ghost @click="heading1" icon="bi-type-h1" color="secondary" :aria-pressed="isHeading1 || undefined" :disabled="preview" />
|
:for="preview ? 'expanded-preview-button' : 'nothing'"
|
||||||
<Button ghost @click="heading2" icon="bi-type-h2" color="secondary" :aria-pressed="isHeading2 || undefined" :disabled="preview" />
|
>
|
||||||
<Button ghost @click="paragraph" icon="bi-paragraph" color="secondary" :aria-pressed="isParagraph || undefined" :disabled="preview" />
|
<Button ghost square-small @click="paragraph" icon="bi-paragraph" :aria-pressed="isParagraph || undefined" :disabled="preview" />
|
||||||
<Button ghost @click="quote" icon="bi-quote" color="secondary" :aria-pressed="isQuote || undefined" :disabled="preview" />
|
<Button ghost square-small @click="heading1" icon="bi-type-h1" :aria-pressed="isHeading1 || undefined" :disabled="preview" />
|
||||||
<Button ghost @click="orderedList" icon="bi-list-ol" color="secondary" :aria-pressed="isOrderedList || undefined"
|
<Button ghost square-small @click="heading2" icon="bi-type-h2" :aria-pressed="isHeading2 || undefined" :disabled="preview" />
|
||||||
|
<Button ghost square-small @click="quote" icon="bi-quote" :aria-pressed="isQuote || undefined" :disabled="preview" />
|
||||||
|
<Button ghost square-small @click="orderedList" icon="bi-list-ol" :aria-pressed="isOrderedList || undefined"
|
||||||
:disabled="preview" />
|
:disabled="preview" />
|
||||||
<Button ghost @click="unorderedList" icon="bi-list-ul" color="secondary" :aria-pressed="isUnorderedList || undefined"
|
<Button ghost square-small @click="unorderedList" icon="bi-list-ul" :aria-pressed="isUnorderedList || undefined"
|
||||||
:disabled="preview" />
|
:disabled="preview" />
|
||||||
|
|
||||||
<div class="separator" />
|
<Spacer />
|
||||||
|
|
||||||
<Button ghost @click="bold" icon="bi-type-bold" color="secondary" :disabled="preview" />
|
<Button ghost square-small @click="bold" icon="bi-type-bold" :disabled="preview" />
|
||||||
<Button ghost @click="italics" icon="bi-type-italic" color="secondary" :disabled="preview" />
|
<Button ghost square-small @click="italics" icon="bi-type-italic" :disabled="preview" />
|
||||||
<Button ghost @click="strikethrough" icon="bi-type-strikethrough" color="secondary" :disabled="preview" />
|
<Button ghost square-small @click="strikethrough" icon="bi-type-strikethrough" :disabled="preview" />
|
||||||
<Button ghost @click="link" icon="bi-link-45deg" color="secondary" :disabled="preview" />
|
<Button ghost square-small @click="link" icon="bi-link-45deg" :disabled="preview" />
|
||||||
|
|
||||||
<span v-if="max !== Infinity && typeof max === 'number'" class="letter-count">{{ max - model.length }}</span>
|
<span v-if="charLimit !== Infinity && typeof charLimit === 'number'" class="letter-count">{{ charLimit - model.length }}</span>
|
||||||
</div>
|
|
||||||
|
<Spacer />
|
||||||
|
|
||||||
|
<Spacer h grow v-if="!$slots.default"/>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<Button ghost low-height min-content
|
||||||
|
id="expanded-preview-button"
|
||||||
|
@click="preview = !preview"
|
||||||
|
icon="bi-eye"
|
||||||
|
:aria-pressed="preview || undefined">
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
.funkwhale {
|
.funkwhale {
|
||||||
&.textarea-label {
|
&.textarea-label {
|
||||||
display: block;
|
|
||||||
> .label {
|
> .label {
|
||||||
padding-bottom: 4px;
|
margin-top: -18px;
|
||||||
font-size:14px;
|
font-size: 14px;
|
||||||
font-weight:600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.textarea {
|
&.textarea {
|
||||||
|
@ -15,19 +14,27 @@
|
||||||
background-color: var(--fw-bg-color);
|
background-color: var(--fw-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> textarea {
|
||||||
|
resize: none;
|
||||||
|
order: 0;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
> .textarea-buttons {
|
> .textarea-buttons {
|
||||||
border-top: 1px solid var(--fw-buttons-border-color);
|
flex-wrap: wrap;
|
||||||
|
// Offset padding of the textarea
|
||||||
|
margin: -8px;
|
||||||
|
|
||||||
> .funkwhale.button:not(:hover):not(:active):not(.is-active) {
|
> button+button {
|
||||||
--fw-bg-color: transparent;
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
> button:has(+button) {
|
||||||
> .separator {
|
border-top-right-radius: 0;
|
||||||
background-color: var(--fw-buttons-border-color);
|
border-bottom-right-radius: 0;
|
||||||
}
|
|
||||||
|
|
||||||
> .letter-count {
|
|
||||||
color: var(--fw-buttons-border-color);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,8 +49,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-preview,
|
&.has-preview,
|
||||||
&:focus-within {
|
&:hover:focus-within {
|
||||||
--fw-border-color: var(--fw-primary) !important;
|
--fw-border-color: transparent;
|
||||||
--fw-bg-color: var(--fw-blue-010);
|
--fw-bg-color: var(--fw-blue-010);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,16 +66,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.has-preview,
|
&.has-preview,
|
||||||
&:focus-within {
|
&:hover:focus-within {
|
||||||
--fw-border-color: var(--fw-primary) !important;
|
--fw-border-color: transparent;
|
||||||
--fw-bg-color: var(--fw-gray-800);
|
--fw-bg-color: var(--fw-gray-800);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: var(--fw-border-radius);
|
|
||||||
|
|
||||||
|
// Make border trace the radius of the buttons smoothly
|
||||||
|
border-radius: calc(var(--fw-border-radius) + 4px);
|
||||||
|
|
||||||
&.has-preview,
|
&.has-preview,
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
|
@ -100,10 +108,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
> textarea {
|
> textarea {
|
||||||
|
line-height: 1.5rem;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 160px;
|
padding: 8px 12px 8px;
|
||||||
padding: 8px 12px 40px;
|
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
|
@ -116,33 +124,12 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
position: absolute;
|
position: relative;
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(.5rem);
|
transform: translateY(1rem) scale(1.03);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: all .2s ease;
|
transition: all .2s ease;
|
||||||
|
|
||||||
padding-top: 4px;
|
|
||||||
|
|
||||||
> .funkwhale.button {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .separator {
|
|
||||||
width: 1px;
|
|
||||||
height: 28px;
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .letter-count {
|
|
||||||
margin-left: auto;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,86 +1,71 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Textarea from '~/components/ui/Textarea.vue'
|
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import Textarea from '~/components/ui/Textarea.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
|
|
||||||
|
const text = ref('# Funk\nwhale')
|
||||||
const text1 = ref('# Funk\nwhale')
|
const text1 = ref('# Funk\nwhale')
|
||||||
const text2 = ref('# Funk\nwhale')
|
const text2 = ref('0123456789abcdefghij')
|
||||||
const text3 = ref('')
|
const text3 = ref('')
|
||||||
|
|
||||||
|
const reset = () => { text.value = '' }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import Textarea from "~/components/ui/Textarea.vue"
|
import Textarea from "~/components/ui/Textarea.vue";
|
||||||
```
|
```
|
||||||
|
|
||||||
# Textarea
|
# Textarea
|
||||||
|
|
||||||
Textareas are input blocks that enable users to write in textual information. These blocks are used throughout the Funkwhale interface for entering item descriptions, moderation notes, and custom notifications.
|
Textareas are input blocks that enable users to write formatted text (format: Markdown). These blocks are used throughout the Funkwhale interface for entering item descriptions, moderation notes, and custom notifications.
|
||||||
|
|
||||||
|
::: details Props
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const {
|
||||||
|
charLimit = 5000,
|
||||||
|
placeholder = "",
|
||||||
|
minLines = 3,
|
||||||
|
...props
|
||||||
|
} = defineProps<{
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
charLimit?: number;
|
||||||
|
minLines?: number | string;
|
||||||
|
autofocus?: true;
|
||||||
|
required?: true;
|
||||||
|
}>();
|
||||||
|
```
|
||||||
|
|
||||||
::: tip
|
|
||||||
Funkwhale supports Markdown syntax in textarea blocks.
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
| Prop | Data type | Required? | Description |
|
Create a textarea and attach its input to a value using a `v-model` of type `string` (required):
|
||||||
| --------------- | --------- | --------- | ------------------------------------------------------------------ |
|
|
||||||
| `max` | Number | No | The maximum number of characters a user can enter in the textarea. |
|
|
||||||
| `placeholder` | String | No | The placeholder text shown on an empty textarea. |
|
|
||||||
| `v-model:value` | String | Yes | The text entered into the textarea. |
|
|
||||||
|
|
||||||
## Textarea model
|
```ts
|
||||||
|
const text = ref("# Funk\nwhale");
|
||||||
Create a textarea and attach its input to a value using a `v-model` directive.
|
```
|
||||||
|
|
||||||
```vue-html{2}
|
```vue-html{2}
|
||||||
<Textarea
|
<Textarea v-model="text" />
|
||||||
v-model="text" label="My Area"
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<ClientOnly>
|
<Textarea v-model="text1" />
|
||||||
<Textarea v-model="text1" label="My Area"/>
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
## Textarea max length
|
## Add a label
|
||||||
|
|
||||||
You can set the maximum length (in characters) that a user can enter in a textarea by passing a `max` prop.
|
```vue-html{3-5}
|
||||||
|
<Spacer size="16" />
|
||||||
```vue-html{3}
|
|
||||||
<Textarea
|
|
||||||
v-model="text"
|
|
||||||
:max="20"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<Textarea v-model="text2" :max="20" />
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
## Textarea placeholder
|
|
||||||
|
|
||||||
Add a placeholder to a textarea to guide users on what they should enter by passing a `placeholder` prop.
|
|
||||||
|
|
||||||
```vue-html{3}
|
|
||||||
<Textarea
|
|
||||||
v-model="text"
|
|
||||||
placeholder="Describe this track here…"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
<ClientOnly>
|
|
||||||
<Textarea v-model="text3" placeholder="Describe this track here…" />
|
|
||||||
</ClientOnly>
|
|
||||||
|
|
||||||
## Label slot
|
|
||||||
|
|
||||||
```vue-html{2-4}
|
|
||||||
<Textarea>
|
<Textarea>
|
||||||
<template #label>
|
<template #label>
|
||||||
About my music
|
About my music <span style="color:red; float:right;">*required</span>
|
||||||
</template>
|
</template>
|
||||||
</Textarea>
|
</Textarea>
|
||||||
```
|
```
|
||||||
|
|
||||||
<Textarea>
|
<Spacer size="16" />
|
||||||
|
<Textarea v-model="text">
|
||||||
<template #label>
|
<template #label>
|
||||||
About my music <span style="color:red; float:right;">*required</span>
|
About my music <span style="color:red; float:right;">*required</span>
|
||||||
</template>
|
</template>
|
||||||
|
@ -89,7 +74,87 @@ Add a placeholder to a textarea to guide users on what they should enter by pass
|
||||||
If you just have a string, we have a convenience prop, so instead you can write:
|
If you just have a string, we have a convenience prop, so instead you can write:
|
||||||
|
|
||||||
```vue-html
|
```vue-html
|
||||||
<Textarea label="About my music" />
|
<Spacer size="16" />
|
||||||
|
<Textarea v-model="text" label="About my music" />
|
||||||
```
|
```
|
||||||
|
|
||||||
<Textarea label="About my music" />
|
<Spacer size="16" />
|
||||||
|
<Textarea v-model="text" label="About my music" />
|
||||||
|
|
||||||
|
Note that the label text sits atop of the upper end of the component. This way, you can define the distance between baselines (the vertical rhythm) with spacers or gaps.
|
||||||
|
|
||||||
|
## Add a placeholder
|
||||||
|
|
||||||
|
```vue-html{3}
|
||||||
|
<Textarea
|
||||||
|
v-model="text"
|
||||||
|
placeholder="Describe this track here…"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Textarea v-model="text3" placeholder="Describe this track here…" />
|
||||||
|
|
||||||
|
## Limit the number of characters
|
||||||
|
|
||||||
|
You can set the maximum length (in characters) that a user can enter in a textarea by passing a `charLimit` prop.
|
||||||
|
|
||||||
|
```vue-html{3}
|
||||||
|
<Textarea v-model="text" :charLimit="20" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<Textarea v-model="text2" :charLimit="20" />
|
||||||
|
|
||||||
|
::: warning Caveats
|
||||||
|
|
||||||
|
- You can still set the model longer than allowed by changing it via a script or another Textarea
|
||||||
|
- A line break counts as one character, which may confuse users
|
||||||
|
- The character counting algorithm has not been tested on non-Latin based languages
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Set the initial height
|
||||||
|
|
||||||
|
Specify the number of lines with the `initial-lines` prop (the default is 5):
|
||||||
|
|
||||||
|
```vue-html
|
||||||
|
<Textarea v-model="text" initial-lines="1" />
|
||||||
|
```
|
||||||
|
|
||||||
|
<Textarea v-model="text3" initialLines="1" />
|
||||||
|
|
||||||
|
## Autofocus
|
||||||
|
|
||||||
|
If you add the `autofocus` attribute, the text input field will receive focus as soon as it is rendered on the page. Make sure to only add this prop to a single component per page!
|
||||||
|
|
||||||
|
## Require this form to be filled before submitting
|
||||||
|
|
||||||
|
The `required` attribute prevents the `submit` button of this form.
|
||||||
|
|
||||||
|
Make sure to also add an indicator such as the text "required" or a star to prevent confusion.
|
||||||
|
|
||||||
|
See [mdn on the `required` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required) if you want to know more.
|
||||||
|
|
||||||
|
## Additional attributes
|
||||||
|
|
||||||
|
Any prop that is not declared in the `Props` type will be added to the `<textarea>` element directly. See [mdn on `textarea` attributes here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea). Examples: `autocomplete`, `cols`, `minlength`, `readonly`, `wrap`, `disabled` etc.
|
||||||
|
|
||||||
|
## Add custom toolbar items
|
||||||
|
|
||||||
|
By default, there are text formatting buttons and a 'preview' toggle button in the toolbar. You can add custom content there by adding them into the default slot. They will be drawn before the `preview` button.
|
||||||
|
|
||||||
|
```vue-html
|
||||||
|
<Textarea v-model="text">
|
||||||
|
<Button ghost low-height min-content
|
||||||
|
v-if="text !== ''"
|
||||||
|
icon="bi-arrow-counterclockwise"
|
||||||
|
@click.prevent="reset()">Reset</Button>
|
||||||
|
</Textarea>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Textarea v-model="text">
|
||||||
|
<Spacer no-size grow />
|
||||||
|
<Button ghost low-height min-content
|
||||||
|
v-if="text !== ''"
|
||||||
|
icon="bi-arrow-counterclockwise"
|
||||||
|
@click.prevent="reset()">Reset</Button>
|
||||||
|
</Textarea>
|
||||||
|
|
Loading…
Reference in New Issue