@saasmakers/ui 2.1.4 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -46,17 +46,15 @@ const id = useId()
|
|
|
46
46
|
const modelValue = defineModel<FieldInput['modelValue']>({ default: '' })
|
|
47
47
|
const input = ref<HTMLInputElement>()
|
|
48
48
|
const showPassword = ref(false)
|
|
49
|
-
const
|
|
50
|
-
const { isDesktopBrowser, isIos } = useDevice()
|
|
49
|
+
const { isDesktopBrowser } = useDevice()
|
|
51
50
|
|
|
52
|
-
const
|
|
53
|
-
|
|
51
|
+
const { isActiveOnIos, isTypingOnIos, onBlur: onIosUppercaseBlur, onCompositionEnd: onIosUppercaseCompositionEnd, onFocus: onIosUppercaseFocus, onInput: onIosUppercaseInput } = useIosUppercase({
|
|
52
|
+
enabled: () => {
|
|
53
|
+
const isProseField = props.type === 'text' || props.type === 'search'
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const hideCssUppercase = computed(() => {
|
|
59
|
-
return iosUppercaseTyping.value && focused.value
|
|
55
|
+
return props.uppercase && isProseField && !props.lowercaseOnly && !props.slugOnly
|
|
56
|
+
},
|
|
57
|
+
modelValue,
|
|
60
58
|
})
|
|
61
59
|
|
|
62
60
|
onMounted(() => {
|
|
@@ -78,8 +76,7 @@ function focus() {
|
|
|
78
76
|
}
|
|
79
77
|
|
|
80
78
|
function onFieldBlur(event: FocusEvent) {
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
onIosUppercaseBlur()
|
|
83
80
|
syncModelValueFromTarget(event.target)
|
|
84
81
|
|
|
85
82
|
if (props.trim) {
|
|
@@ -90,20 +87,12 @@ function onFieldBlur(event: FocusEvent) {
|
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
function onFieldFocus(event: FocusEvent) {
|
|
93
|
-
|
|
94
|
-
const uppercased = modelValue.value.toString().toUpperCase()
|
|
95
|
-
|
|
96
|
-
if (uppercased !== modelValue.value) {
|
|
97
|
-
modelValue.value = uppercased
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
focused.value = true
|
|
102
|
-
|
|
90
|
+
onIosUppercaseFocus()
|
|
103
91
|
emit('focus', event)
|
|
104
92
|
}
|
|
105
93
|
|
|
106
94
|
function onFieldInput(event: Event) {
|
|
95
|
+
onIosUppercaseInput(event)
|
|
107
96
|
syncModelValueFromTarget(event.target)
|
|
108
97
|
}
|
|
109
98
|
|
|
@@ -216,12 +205,12 @@ defineExpose({
|
|
|
216
205
|
:id="id"
|
|
217
206
|
ref="input"
|
|
218
207
|
:autocomplete="autocomplete ? 'on' : 'off'"
|
|
219
|
-
:autocapitalize="
|
|
208
|
+
:autocapitalize="isActiveOnIos ? 'characters' : undefined"
|
|
220
209
|
v-bind="autofocus ? { 'data-autofocus': '' } : {}"
|
|
221
210
|
class="h-full w-full appearance-none items-center rounded-lg font-medium tracking-tight outline-none placeholder-gray-600 dark:placeholder-gray-400"
|
|
222
211
|
:class="{
|
|
223
|
-
'uppercase': uppercase && !
|
|
224
|
-
'placeholder:uppercase':
|
|
212
|
+
'uppercase': uppercase && !isTypingOnIos,
|
|
213
|
+
'placeholder:uppercase': isTypingOnIos,
|
|
225
214
|
'field-disabled': disabled,
|
|
226
215
|
'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
|
|
227
216
|
'text-gray-900 dark:text-gray-100': !lineThrough,
|
|
@@ -256,6 +245,7 @@ defineExpose({
|
|
|
256
245
|
:type="type === 'password' && showPassword ? 'text' : type"
|
|
257
246
|
:value="modelValue"
|
|
258
247
|
@blur="onFieldBlur"
|
|
248
|
+
@compositionend="onIosUppercaseCompositionEnd"
|
|
259
249
|
@focus="onFieldFocus"
|
|
260
250
|
@input="onFieldInput"
|
|
261
251
|
@keydown="onFieldKeyDown"
|
|
@@ -30,15 +30,11 @@ const emit = defineEmits<{
|
|
|
30
30
|
const id = useId()
|
|
31
31
|
const modelValue = defineModel<FieldTextarea['modelValue']>({ default: '' })
|
|
32
32
|
const textarea = ref<HTMLTextAreaElement>()
|
|
33
|
-
const
|
|
34
|
-
const { isDesktopBrowser, isIos } = useDevice()
|
|
33
|
+
const { isDesktopBrowser } = useDevice()
|
|
35
34
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const hideCssUppercase = computed(() => {
|
|
41
|
-
return iosUppercaseTyping.value && focused.value
|
|
35
|
+
const { isActiveOnIos, isTypingOnIos, onBlur: onIosUppercaseBlur, onCompositionEnd: onIosUppercaseCompositionEnd, onFocus: onIosUppercaseFocus, onInput: onIosUppercaseInput } = useIosUppercase({
|
|
36
|
+
enabled: () => props.uppercase,
|
|
37
|
+
modelValue,
|
|
42
38
|
})
|
|
43
39
|
|
|
44
40
|
onMounted(() => {
|
|
@@ -48,8 +44,7 @@ onMounted(() => {
|
|
|
48
44
|
})
|
|
49
45
|
|
|
50
46
|
function onFieldBlur(event: FocusEvent) {
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
onIosUppercaseBlur()
|
|
53
48
|
syncModelValueFromTarget(event.target)
|
|
54
49
|
emit('blur', event, modelValue.value || '')
|
|
55
50
|
}
|
|
@@ -58,18 +53,6 @@ function onFieldClick(event: MouseEvent) {
|
|
|
58
53
|
emit('click', event, modelValue.value || '')
|
|
59
54
|
}
|
|
60
55
|
|
|
61
|
-
function onFieldFocus() {
|
|
62
|
-
if (iosUppercaseTyping.value) {
|
|
63
|
-
const uppercased = modelValue.value.toString().toUpperCase()
|
|
64
|
-
|
|
65
|
-
if (uppercased !== modelValue.value) {
|
|
66
|
-
modelValue.value = uppercased
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
focused.value = true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
56
|
function syncModelValueFromTarget(target: EventTarget | null) {
|
|
74
57
|
if (!(target instanceof HTMLTextAreaElement)) {
|
|
75
58
|
return
|
|
@@ -100,11 +83,11 @@ function syncModelValueFromTarget(target: EventTarget | null) {
|
|
|
100
83
|
ref="textarea"
|
|
101
84
|
v-model="modelValue"
|
|
102
85
|
v-bind="{ 'data-enable-grammarly': 'false' }"
|
|
103
|
-
:autocapitalize="
|
|
86
|
+
:autocapitalize="isActiveOnIos ? 'characters' : undefined"
|
|
104
87
|
class="w-full flex-1 appearance-none rounded-lg text-gray-900 font-medium tracking-tight outline-none dark:text-gray-100 placeholder-gray-600 dark:placeholder-gray-400"
|
|
105
88
|
:class="{
|
|
106
|
-
'uppercase': uppercase && !
|
|
107
|
-
'placeholder:uppercase':
|
|
89
|
+
'uppercase': uppercase && !isTypingOnIos,
|
|
90
|
+
'placeholder:uppercase': isTypingOnIos,
|
|
108
91
|
'field-disabled': disabled,
|
|
109
92
|
'focus:placeholder-gray-900 hover:placeholder-gray-900 dark:focus:placeholder-gray-100 dark:hover:placeholder-gray-100': !disabled,
|
|
110
93
|
'px-sm py-2.5': padding && size === 'sm',
|
|
@@ -127,7 +110,9 @@ function syncModelValueFromTarget(target: EventTarget | null) {
|
|
|
127
110
|
spellcheck="false"
|
|
128
111
|
@blur="onFieldBlur"
|
|
129
112
|
@click="onFieldClick"
|
|
130
|
-
@
|
|
113
|
+
@compositionend="onIosUppercaseCompositionEnd"
|
|
114
|
+
@focus="onIosUppercaseFocus"
|
|
115
|
+
@input="onIosUppercaseInput"
|
|
131
116
|
/>
|
|
132
117
|
|
|
133
118
|
<FieldMessage
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default function useDevice() {
|
|
2
2
|
const { isIOS, isNative } = useCapacitor()
|
|
3
|
-
const
|
|
3
|
+
const isIosDevice = ref(false)
|
|
4
4
|
|
|
5
5
|
const isDesktopBrowser = computed(() => {
|
|
6
6
|
if (typeof navigator === 'undefined') {
|
|
@@ -20,24 +20,16 @@ export default function useDevice() {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
onMounted(() => {
|
|
23
|
-
|
|
23
|
+
isIosDevice.value = isIOS.value || isIosBrowser()
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
isDesktopBrowser,
|
|
28
|
-
|
|
28
|
+
isIosDevice,
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
if (isNativeIos) {
|
|
34
|
-
return true
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (typeof navigator === 'undefined') {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
|
|
32
|
+
function isIosBrowser() {
|
|
41
33
|
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
|
|
42
34
|
return true
|
|
43
35
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// iOS WebKit drops keyboard suggestions while `text-transform: uppercase` is active,
|
|
2
|
+
// so the transform is lifted during typing and the value itself is uppercased instead.
|
|
3
|
+
export default function useIosUppercase(options: {
|
|
4
|
+
enabled: MaybeRefOrGetter<boolean>
|
|
5
|
+
modelValue: Ref<string>
|
|
6
|
+
}) {
|
|
7
|
+
const { isIosDevice } = useDevice()
|
|
8
|
+
const isFocused = ref(false)
|
|
9
|
+
|
|
10
|
+
const isActiveOnIos = computed(() => {
|
|
11
|
+
return toValue(options.enabled) && isIosDevice.value
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const isTypingOnIos = computed(() => {
|
|
15
|
+
return isActiveOnIos.value && isFocused.value
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function onFocus() {
|
|
19
|
+
if (isActiveOnIos.value) {
|
|
20
|
+
options.modelValue.value = options.modelValue.value.toUpperCase()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isFocused.value = true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function onBlur() {
|
|
27
|
+
isFocused.value = false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onInput(event: Event) {
|
|
31
|
+
// Rewriting the value mid-composition breaks IME and accent input.
|
|
32
|
+
if (event instanceof InputEvent && event.isComposing) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
forceUppercase(event.target)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function onCompositionEnd(event: CompositionEvent) {
|
|
40
|
+
forceUppercase(event.target)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function forceUppercase(target: EventTarget | null) {
|
|
44
|
+
if (!isActiveOnIos.value) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const uppercased = target.value.toUpperCase()
|
|
53
|
+
|
|
54
|
+
if (uppercased === target.value) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { selectionEnd, selectionStart } = target
|
|
59
|
+
|
|
60
|
+
target.value = uppercased
|
|
61
|
+
|
|
62
|
+
target.setSelectionRange(selectionStart ?? uppercased.length, selectionEnd ?? uppercased.length)
|
|
63
|
+
|
|
64
|
+
options.modelValue.value = uppercased
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
isActiveOnIos,
|
|
69
|
+
isTypingOnIos,
|
|
70
|
+
onBlur,
|
|
71
|
+
onCompositionEnd,
|
|
72
|
+
onFocus,
|
|
73
|
+
onInput,
|
|
74
|
+
}
|
|
75
|
+
}
|