mirror of
https://github.com/tenrok/maska.git
synced 2026-05-15 11:59:38 +03:00
feat: simple mode for mask directive
Allow string passing to set a mask without data-maska attribute
This commit is contained in:
+21
-17
@@ -24,38 +24,42 @@
|
||||
|
||||
<main>
|
||||
<article>
|
||||
<div x-data="{
|
||||
maskedvalue: '123',
|
||||
unmaskedvalue: '',
|
||||
options: { eager: true }
|
||||
}">
|
||||
<input
|
||||
x-model="maskedvalue"
|
||||
x-maska:unmaskedvalue.unmasked="options"
|
||||
x-on:maska="console.log($event.detail)"
|
||||
data-maska="##-##"
|
||||
>
|
||||
<div><label><input type="checkbox" x-model="options.eager"> eager?</label></div>
|
||||
<div>masked value: <span x-text="maskedvalue"></span></div>
|
||||
<div>unmasked value: <span x-text="unmaskedvalue"></span></div>
|
||||
<div x-data="{ enabled: true }">
|
||||
<div><label><input type="checkbox" x-model="enabled"> enabled?</label></div>
|
||||
<template x-if="enabled">
|
||||
<div x-data="{
|
||||
maskedvalue: '123',
|
||||
unmaskedvalue: '',
|
||||
options: { mask: '#-#', eager: true }
|
||||
}">
|
||||
<input
|
||||
x-model="maskedvalue"
|
||||
x-maska:unmaskedvalue.unmasked="options"
|
||||
>
|
||||
<div><label><input type="checkbox" x-model="options.eager"> eager?</label></div>
|
||||
<div>masked value: <span x-text="maskedvalue"></span></div>
|
||||
<div>unmasked value: <span x-text="unmaskedvalue"></span></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div x-data="{
|
||||
maskedvalue: '-1234.90',
|
||||
unmaskedvalue: '',
|
||||
options: { number: { locale: 'ru', fraction: 2 }}
|
||||
}" style="margin-top: 1em">
|
||||
<input
|
||||
x-model="maskedvalue"
|
||||
x-maska:unmaskedvalue.unmasked="options"
|
||||
x-maska:unmaskedvalue.unmasked
|
||||
x-on:maska="console.log($event.detail)"
|
||||
data-maska-number-locale="ru"
|
||||
data-maska-number-fraction="2"
|
||||
>
|
||||
<div>masked value: <span x-text="maskedvalue"></span></div>
|
||||
<div>unmasked value: <span x-text="unmaskedvalue"></span></div>
|
||||
</div>
|
||||
|
||||
<div x-data style="margin-top: 1em">
|
||||
<input x-maska data-maska="+1 (###) ###-####" data-maska-eager value="1234567">
|
||||
<input x-maska="'+1 (###) ###-####'" data-maska-eager value="1234567">
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
|
||||
+15
-6
@@ -3,7 +3,7 @@
|
||||
Maska provides custom Alpine.js directive for use with input:
|
||||
|
||||
```html
|
||||
<input x-maska:argument.modifier="options">
|
||||
<input x-maska:argument.modifier="value">
|
||||
```
|
||||
|
||||
- `argument` is a name of the bound variable (see example below)
|
||||
@@ -11,21 +11,24 @@ Maska provides custom Alpine.js directive for use with input:
|
||||
- `masked` (default): variable will get a masked value (as in v-model)
|
||||
- `unmasked`: variable will get an unmasked (raw) value
|
||||
- `completed`: variable will be boolean, showing that mask is completed
|
||||
- `options` is object with default options
|
||||
- `value` could be one of:
|
||||
- `string` for the mask value (should be enclosed in additional quotation marks: `"'#-#'"`)
|
||||
- `object` with a default options
|
||||
|
||||
|
||||
## Minimal example
|
||||
|
||||
Apply `xMaska` directive to the input along with `data-maska` attribite:
|
||||
Apply `xMaska` directive to the input:
|
||||
|
||||
```html
|
||||
<input x-maska data-maska="#-#">
|
||||
<input x-maska="'#-#'">
|
||||
```
|
||||
|
||||
?> Please note that the mask value is enclosed in additional quotation marks: `"'#-#'"`.
|
||||
|
||||
## Set mask options
|
||||
|
||||
To set default [options](/options) for the mask, pass options via **directive value**:
|
||||
To set a default [options](/options) for the mask, pass options via **directive value**:
|
||||
|
||||
```html
|
||||
<div x-data="{ options: { mask: '#-#', eager: true }}">
|
||||
@@ -33,6 +36,12 @@ To set default [options](/options) for the mask, pass options via **directive va
|
||||
</div>
|
||||
```
|
||||
|
||||
Or you can pass an options object directly:
|
||||
|
||||
```html
|
||||
<input x-maska="{ mask: '#-#', eager: true }" data-maska-reversed>
|
||||
```
|
||||
|
||||
You can override default options with `data-maska-` attributes on the input. In the example above we set **eager** mode using options and **reversed** mode using `data-maska-reversed` attribute.
|
||||
|
||||
|
||||
@@ -42,7 +51,7 @@ To get masked value you can use standard `x-model` directive on the input. But i
|
||||
|
||||
```html
|
||||
<div x-data="{ maskedvalue: '', unmaskedvalue: '' }">
|
||||
<input x-maska:unmaskedvalue.unmasked data-maska="#-#" x-model="maskedvalue">
|
||||
<input x-maska:unmaskedvalue.unmasked="'#-#'" x-model="maskedvalue">
|
||||
|
||||
Masked value: <span x-text="maskedvalue"></span>
|
||||
Unmasked value: <span x-text="unmaskedvalue"></span>
|
||||
|
||||
+15
-11
@@ -12,7 +12,7 @@ npm install maska
|
||||
And then import it in your code:
|
||||
|
||||
```js
|
||||
import { Mask, MaskInput } from 'maska'
|
||||
import { Mask, MaskInput } from "maska"
|
||||
|
||||
new MaskInput("[data-maska]") // for masked input
|
||||
const mask = new Mask({ mask: "#-#" }) // for programmatic use
|
||||
@@ -23,22 +23,26 @@ const mask = new Mask({ mask: "#-#" }) // for programmatic use
|
||||
And then register it as custom plugin:
|
||||
|
||||
```js
|
||||
import Alpine from 'alpinejs'
|
||||
import { xMaska } from 'maska/alpine'
|
||||
import Alpine from "alpinejs"
|
||||
import { xMaska } from "maska/alpine"
|
||||
|
||||
Alpine.plugin(xMaska)
|
||||
```
|
||||
|
||||
```html
|
||||
<input x-maska="'#-#'">
|
||||
```
|
||||
|
||||
### **Svelte**
|
||||
|
||||
And then use it in your `.svelte` component:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { maska } from 'maska/svelte'
|
||||
import { maska } from "maska/svelte"
|
||||
</script>
|
||||
|
||||
<input use:maska data-maska="#-#" />
|
||||
<input use:maska={'#-#'}>
|
||||
```
|
||||
|
||||
### **Vue**
|
||||
@@ -47,11 +51,11 @@ And then use it in your `.vue` component:
|
||||
|
||||
```js
|
||||
<script setup>
|
||||
import { vMaska } from 'maska/vue'
|
||||
import { vMaska } from "maska/vue"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska data-maska="#-#" />
|
||||
<input v-maska="'#-#'">
|
||||
</template>
|
||||
```
|
||||
<!-- tabs:end -->
|
||||
@@ -118,7 +122,7 @@ For modern browsers you can use ES modules build with (optional) [import maps](h
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { Mask, MaskInput } from 'maska'
|
||||
import { Mask, MaskInput } from "maska"
|
||||
|
||||
new MaskInput("[data-maska]") // for masked input
|
||||
const mask = new Mask({ mask: "#-#" }) // for programmatic use
|
||||
@@ -136,8 +140,8 @@ For modern browsers you can use ES modules build with (optional) [import maps](h
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import Alpine from 'alpinejs'
|
||||
import { xMaska } from 'maska/alpine'
|
||||
import Alpine from "alpinejs"
|
||||
import { xMaska } from "maska/alpine"
|
||||
|
||||
Alpine.plugin(xMaska)
|
||||
</script>
|
||||
@@ -154,7 +158,7 @@ For modern browsers you can use ES modules build with (optional) [import maps](h
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import { vMaska } from 'maska/vue'
|
||||
import { vMaska } from "maska/vue"
|
||||
|
||||
Vue.createApp({ directives: { maska: vMaska }}).mount('#app')
|
||||
</script>
|
||||
|
||||
+12
-8
@@ -3,34 +3,38 @@
|
||||
Maska provides simple Svelte action for use with input:
|
||||
|
||||
```html
|
||||
<input use:maska={options}>
|
||||
<input use:maska={value}>
|
||||
```
|
||||
|
||||
- `options` is object with default options
|
||||
- `value` could be one of:
|
||||
- `string` for the mask value (should be enclosed in additional quotation marks: `{'#-#'}`)
|
||||
- `object` with a default options
|
||||
|
||||
|
||||
## Minimal example
|
||||
|
||||
Apply `maska` action to the input along with `data-maska` attribite:
|
||||
Apply `maska` action to the input:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { maska } from '@maskajs/svelte'
|
||||
import { maska } from "maska/svelte"
|
||||
</script>
|
||||
|
||||
<input use:maska data-maska="#-#">
|
||||
<input use:maska={'#-#'}>
|
||||
```
|
||||
|
||||
?> Please note that the mask value is enclosed in additional quotation marks: `"'#-#'"`.
|
||||
|
||||
## Set mask options
|
||||
|
||||
To set default [options](/options) for the mask, pass options via **directive value**:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { maska } from '@maskajs/svelte'
|
||||
<script lang="ts">
|
||||
import { maska } from "maska/svelte"
|
||||
import type { MaskInputOptions } from "maska"
|
||||
|
||||
const options = {
|
||||
const options: MaskInputOptions = {
|
||||
mask: "#-#",
|
||||
eager: true
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,13 +6,13 @@ Maska v3 has different entries for framework-specific exports.
|
||||
Import of vue directive in v2:
|
||||
|
||||
```js
|
||||
import { vMaska } from 'maska'
|
||||
import { vMaska } from "maska"
|
||||
```
|
||||
|
||||
Now you should import vue directive from `maska/vue`:
|
||||
|
||||
```js
|
||||
import { vMaska } from 'maska/vue'
|
||||
import { vMaska } from "maska/vue"
|
||||
```
|
||||
|
||||
!> Filenames have also been changed. Please see the [Installation](install) for more information.
|
||||
|
||||
+14
-10
@@ -3,7 +3,7 @@
|
||||
Maska provides custom Vue directive for use with input:
|
||||
|
||||
```html
|
||||
<input v-maska:argument.modifier="options">
|
||||
<input v-maska:argument.modifier="value">
|
||||
```
|
||||
|
||||
- `argument` is a name of the bound variable (see example below)
|
||||
@@ -11,11 +11,13 @@ Maska provides custom Vue directive for use with input:
|
||||
- `masked` (default): variable will get a masked value (as in v-model)
|
||||
- `unmasked`: variable will get an unmasked (raw) value
|
||||
- `completed`: variable will be boolean, showing that mask is completed
|
||||
- `options` is object with default options
|
||||
- `value` could be one of:
|
||||
- `string` for the mask value (should be enclosed in additional quotation marks: `"'#-#'"`)
|
||||
- `object` with a default options
|
||||
|
||||
## Minimal example
|
||||
|
||||
Import `vMaska` directive and apply it to the input along with `data-maska` attribite:
|
||||
Import `vMaska` directive and apply it to the input:
|
||||
|
||||
<!-- tabs:start -->
|
||||
### **Composition API**
|
||||
@@ -26,7 +28,7 @@ Import `vMaska` directive and apply it to the input along with `data-maska` attr
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska data-maska="#-#">
|
||||
<input v-maska="'#-#'">
|
||||
</template>
|
||||
```
|
||||
|
||||
@@ -42,11 +44,12 @@ Import `vMaska` directive and apply it to the input along with `data-maska` attr
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska data-maska="#-#">
|
||||
<input v-maska="'#-#'">
|
||||
</template>
|
||||
```
|
||||
<!-- tabs:end -->
|
||||
|
||||
?> Please note that the mask value is enclosed in additional quotation marks: `"'#-#'"`.
|
||||
|
||||
## Set mask options
|
||||
|
||||
@@ -56,12 +59,13 @@ To set default [options](/options) for the mask, pass options via **directive va
|
||||
### **Composition API**
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue"
|
||||
import { vMaska } from "maska/vue"
|
||||
import type { MaskInputOptions } from "maska"
|
||||
|
||||
// could be plain object too
|
||||
const options = reactive({
|
||||
const options = reactive<MaskInputOptions>({
|
||||
mask: "#-#",
|
||||
eager: true
|
||||
})
|
||||
@@ -119,7 +123,7 @@ To get masked value you can use standard `v-model` directive on the input. But i
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska:unmaskedValue.unmasked data-maska="#-#" v-model="maskedValue">
|
||||
<input v-maska:unmaskedValue.unmasked="'#-#'" v-model="maskedValue">
|
||||
|
||||
Masked value: {{ maskedValue }}
|
||||
Unmasked value: {{ unmaskedValue }}
|
||||
@@ -144,7 +148,7 @@ To get masked value you can use standard `v-model` directive on the input. But i
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska:unmaskedValue.unmasked data-maska="#-#" v-model="maskedValue">
|
||||
<input v-maska:unmaskedValue.unmasked="'#-#'" v-model="maskedValue">
|
||||
|
||||
Masked value: {{ maskedValue }}
|
||||
Unmasked value: {{ unmaskedValue }}
|
||||
@@ -168,7 +172,7 @@ export default defineNuxtPlugin((nuxtApp) => {
|
||||
Now you can use v-maska directive in your app:
|
||||
|
||||
```html
|
||||
<input v-model="value" v-maska data-maska="#-#" />
|
||||
<input v-maska="'#-#'" />
|
||||
```
|
||||
|
||||
|
||||
|
||||
+9
-6
@@ -9,11 +9,16 @@ export const xMaska = (Alpine: Alpine): void => {
|
||||
|
||||
if (input == null || input?.type === 'file') return
|
||||
|
||||
let opts: MaskInputOptions = {}
|
||||
|
||||
const evaluator = directive.expression !== ''
|
||||
? utilities.evaluateLater<MaskInputOptions | string>(directive.expression)
|
||||
: () => {}
|
||||
|
||||
utilities.effect(() => {
|
||||
const opts =
|
||||
directive.expression !== ''
|
||||
? { ...utilities.evaluate<MaskInputOptions>(directive.expression) }
|
||||
: {}
|
||||
evaluator((expr) => {
|
||||
opts = typeof expr === 'string' ? { mask: expr } : { ...expr }
|
||||
})
|
||||
|
||||
if (directive.value != null) {
|
||||
const updateArg = (detail: MaskaDetail): void => {
|
||||
@@ -43,7 +48,5 @@ export const xMaska = (Alpine: Alpine): void => {
|
||||
masks.set(input, new MaskInput(input, opts))
|
||||
}
|
||||
})
|
||||
|
||||
utilities.cleanup(() => masks.get(input)?.destroy())
|
||||
}).before('model')
|
||||
}
|
||||
|
||||
+12
-2
@@ -3,19 +3,29 @@ import { MaskaDetail, MaskInput, MaskInputOptions } from '..'
|
||||
|
||||
const masks = new WeakMap<HTMLInputElement, MaskInput>()
|
||||
|
||||
type MaskaAction = Action<HTMLElement, MaskInputOptions | undefined, {
|
||||
type MaskaAction = Action<HTMLElement, MaskInputOptions | string | undefined, {
|
||||
'on:maska': (detail: CustomEvent<MaskaDetail>) => void
|
||||
}>
|
||||
|
||||
export const maska: MaskaAction = (node, opts = {}) => {
|
||||
export const maska: MaskaAction = (node, value = {}) => {
|
||||
const input = node instanceof HTMLInputElement ? node : node.querySelector('input')
|
||||
|
||||
if (input == null || input?.type === 'file') return
|
||||
|
||||
let opts = value
|
||||
|
||||
if (typeof opts === 'string') {
|
||||
opts = { mask: opts }
|
||||
}
|
||||
|
||||
masks.set(input, new MaskInput(input, opts))
|
||||
|
||||
return {
|
||||
update (opts) {
|
||||
if (typeof opts === 'string') {
|
||||
opts = { mask: opts }
|
||||
}
|
||||
|
||||
masks.get(input)?.update(opts)
|
||||
},
|
||||
|
||||
|
||||
+7
-2
@@ -1,7 +1,7 @@
|
||||
import { Directive, DirectiveBinding } from 'vue'
|
||||
import { MaskaDetail, MaskInput, MaskInputOptions } from '..'
|
||||
|
||||
type MaskaDirective = Directive<HTMLElement, MaskInputOptions | undefined>
|
||||
type MaskaDirective = Directive<HTMLElement, MaskInputOptions | string | undefined>
|
||||
|
||||
const masks = new WeakMap<HTMLInputElement, MaskInput>()
|
||||
|
||||
@@ -20,10 +20,15 @@ const setArg = (binding: DirectiveBinding, value: string | boolean): void => {
|
||||
|
||||
export const vMaska: MaskaDirective = (el, binding) => {
|
||||
const input = el instanceof HTMLInputElement ? el : el.querySelector('input')
|
||||
const opts = binding.value != null ? { ...binding.value } : {}
|
||||
|
||||
if (input == null || input?.type === 'file') return
|
||||
|
||||
let opts: MaskInputOptions = {}
|
||||
|
||||
if (binding.value != null) {
|
||||
opts = typeof binding.value === 'string' ? { mask: binding.value } : { ...binding.value }
|
||||
}
|
||||
|
||||
if (binding.arg != null) {
|
||||
const updateArg = (detail: MaskaDetail): void => {
|
||||
const value = binding.modifiers.unmasked
|
||||
|
||||
@@ -20,6 +20,12 @@ const prepareInput = async (markup: string) => {
|
||||
}
|
||||
|
||||
describe('init', () => {
|
||||
test('with string', async () => {
|
||||
input = await prepareInput(`<input x-maska="'#-#'" value="123">`)
|
||||
|
||||
expect(input).toHaveValue('1-2')
|
||||
})
|
||||
|
||||
test('with data attr', async () => {
|
||||
input = await prepareInput(`<input x-maska data-maska="#-#" value="123">`)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { maska } from '../../src/svelte'
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<input use:maska data-maska="#-#" data-maska-eager />
|
||||
</main>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BindValue from './BindValue.svelte'
|
||||
import DataAttr from './DataAttr.svelte'
|
||||
import Event from './Event.svelte'
|
||||
import InitialValue from './InitialValue.svelte'
|
||||
import Number from './Number.svelte'
|
||||
@@ -12,6 +13,7 @@
|
||||
<main>
|
||||
<select bind:value={component}>
|
||||
<option value={BindValue}>BindValue</option>
|
||||
<option value={DataAttr}>DataAttr</option>
|
||||
<option value={Event}>Event</option>
|
||||
<option value={InitialValue}>InitialValue</option>
|
||||
<option value={Number}>Number</option>
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<input type="checkbox" bind:checked={options.eager} />
|
||||
<input type="checkbox" bind:checked={options.eager} /> Eager<br>
|
||||
<input type="text" use:maska={options} />
|
||||
</main>
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<input use:maska data-maska="#-#" />
|
||||
<input use:maska={'#-#'} />
|
||||
</main>
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@ import { vMaska } from '../../src/vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input v-maska data-maska="#-#" />
|
||||
<input v-maska="'#-#'" />
|
||||
</template>
|
||||
|
||||
+25
-35
@@ -1,62 +1,52 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { vMaska } from '../../src/vue'
|
||||
import type { MaskInputOptions, MaskaDetail } from '../../src'
|
||||
|
||||
const mask = ref('+1 (###) ###-####')
|
||||
const show = ref(true)
|
||||
const eager = ref(true)
|
||||
const valueMasked = ref('1234567')
|
||||
const valueUnmasked = ref('1')
|
||||
|
||||
const onMaska = (e) => console.log(e.detail)
|
||||
const onMaska = (e: CustomEvent<MaskaDetail>) => console.log(e.detail)
|
||||
|
||||
const options = reactive({
|
||||
mask,
|
||||
eager
|
||||
const options = reactive<MaskInputOptions>({
|
||||
mask: '+1 (###) ###-####',
|
||||
eager: true
|
||||
})
|
||||
|
||||
const options2 = {
|
||||
preProcess: val => val.replace(/[$,]/g, ''),
|
||||
postProcess: val => {
|
||||
if (!val) return ''
|
||||
|
||||
const sub = 3 - (val.includes('.') ? val.length - val.indexOf('.') : 0)
|
||||
|
||||
return Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(val)
|
||||
.slice(0, sub ? -sub : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ valueUnmasked })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<p>
|
||||
<div>
|
||||
show: <input type="checkbox" v-model="show">
|
||||
eager: <input type="checkbox" v-model="eager">
|
||||
eager: <input type="checkbox" v-model="options.eager">
|
||||
</div>
|
||||
<div><input v-model="mask"></div>
|
||||
|
||||
<input v-model="options.mask">
|
||||
|
||||
<div v-if="show">
|
||||
<input v-maska:valueUnmasked.unmasked="options" v-model="valueMasked" @maska="onMaska">
|
||||
<div>Masked: {{ valueMasked }}</div>
|
||||
<div>Unmasked: {{ valueUnmasked }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="row"><input type="email" v-maska data-maska="##-##" data-maska-eager value="12"></div>
|
||||
<p>
|
||||
<input v-maska data-maska="##-##" data-maska-eager value="123">
|
||||
</p>
|
||||
|
||||
<div class="row"><input v-maska data-maska="####" value="1234"></div>
|
||||
<p>
|
||||
<input v-maska="'####'" value="12345">
|
||||
</p>
|
||||
|
||||
<div><input v-maska="options2" value="1234567" data-maska="0.99" data-maska-tokens="0:\d:multiple|9:\d:optional"></div>
|
||||
<p>
|
||||
<input
|
||||
v-maska
|
||||
value="1234567.89"
|
||||
data-maska-number-locale="en"
|
||||
data-maska-number-fraction="2"
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.row {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user