@geckou/ui-vue 0.3.0
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.
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/assets/scss/functions.scss +6 -0
- package/dist/assets/scss/mixin.scss +33 -0
- package/dist/components/ArticleList/Card/Artistic.vue +310 -0
- package/dist/components/ArticleList/Card/Entertainment.vue +244 -0
- package/dist/components/ArticleList/Card/Gallery.vue +254 -0
- package/dist/components/ArticleList/Card/Grid.vue +247 -0
- package/dist/components/ArticleList/Card/News.vue +150 -0
- package/dist/components/ArticleList/Card/Rounded.vue +154 -0
- package/dist/components/ArticleList/Card/Row.vue +167 -0
- package/dist/components/ArticleList/Card/Simple.vue +232 -0
- package/dist/components/ArticleList/Card/Standard.vue +187 -0
- package/dist/components/ArticleList/Card/Tile.vue +171 -0
- package/dist/components/ArticleList/GenericArticleList.vue +143 -0
- package/dist/components/ArticleList/List/Artistic.vue +24 -0
- package/dist/components/ArticleList/List/Entertainment.vue +24 -0
- package/dist/components/ArticleList/List/Gallery.vue +24 -0
- package/dist/components/ArticleList/List/Grid.vue +216 -0
- package/dist/components/ArticleList/List/News.vue +24 -0
- package/dist/components/ArticleList/List/Rounded.vue +24 -0
- package/dist/components/ArticleList/List/Row.vue +24 -0
- package/dist/components/ArticleList/List/Simple.vue +24 -0
- package/dist/components/ArticleList/List/Standard.vue +24 -0
- package/dist/components/ArticleList/List/Tile.vue +24 -0
- package/dist/components/ArticleList/Parts/AuthorInfo.vue +100 -0
- package/dist/components/ArticleList/Parts/CardContainer.vue +23 -0
- package/dist/components/ArticleList/Parts/CardHeading.vue +43 -0
- package/dist/components/ArticleList/Parts/CategoryList.vue +41 -0
- package/dist/components/ArticleList/Parts/ExcerptText.vue +43 -0
- package/dist/components/ArticleList/Parts/MetadataList.vue +120 -0
- package/dist/components/ArticleList/Parts/NoImage.vue +36 -0
- package/dist/components/ArticleList/Parts/PostedDate.vue +41 -0
- package/dist/components/ArticleList/Parts/TagList.vue +41 -0
- package/dist/components/ArticleList/Parts/ThumbnailImage.vue +85 -0
- package/dist/components/BasicButton.vue +143 -0
- package/dist/components/CheckBox.vue +140 -0
- package/dist/components/CheckBoxes.vue +100 -0
- package/dist/components/CheckButton.vue +127 -0
- package/dist/components/DatePicker.vue +234 -0
- package/dist/components/DateRangePicker.vue +85 -0
- package/dist/components/DateSelector.vue +273 -0
- package/dist/components/DropdownUi.vue +118 -0
- package/dist/components/ErrorMessage.vue +61 -0
- package/dist/components/Icon/CalendarIcon.vue +8 -0
- package/dist/components/Icon/CheckIcon.vue +8 -0
- package/dist/components/Icon/CloseIcon.vue +8 -0
- package/dist/components/Icon/Folder.vue +8 -0
- package/dist/components/Icon/Image.vue +14 -0
- package/dist/components/Icon/KeyboardArrowDownIcon.vue +8 -0
- package/dist/components/Icon/Tag.vue +8 -0
- package/dist/components/InputBox.vue +134 -0
- package/dist/components/InputGroup.vue +41 -0
- package/dist/components/LabeledCheckbox.vue +82 -0
- package/dist/components/LabeledFieldset.vue +41 -0
- package/dist/components/LoadingSpinner.vue +56 -0
- package/dist/components/ModalBox.vue +192 -0
- package/dist/components/PopupBox.vue +93 -0
- package/dist/components/RadioButtons.vue +163 -0
- package/dist/components/SelectBox.vue +166 -0
- package/dist/components/SlideDownUi.vue +123 -0
- package/dist/components/TabUI.vue +123 -0
- package/dist/components/TextArea.vue +115 -0
- package/dist/components/TextBox.vue +111 -0
- package/dist/components/TextButton.vue +47 -0
- package/dist/components/ToggleButton.vue +179 -0
- package/dist/const/index.ts +2 -0
- package/dist/const/list-theme.ts +62 -0
- package/dist/index.ts +126 -0
- package/dist/scripts/form-validation-manager.ts +63 -0
- package/dist/scripts/utils.ts +36 -0
- package/dist/types/custom.d.ts +1 -0
- package/dist/types/index.d.ts +93 -0
- package/dist/types/vue-shim.d.ts +5 -0
- package/package.json +44 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
Option,
|
|
4
|
+
InputBoxStyleForEachStatus,
|
|
5
|
+
} from '../types'
|
|
6
|
+
import {
|
|
7
|
+
ref,
|
|
8
|
+
computed,
|
|
9
|
+
watch,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
import InputBox from '../components/InputBox.vue'
|
|
12
|
+
import ErrorMessage from '../components/ErrorMessage.vue'
|
|
13
|
+
import KeyboardArrowDownIcon from '../components/Icon/KeyboardArrowDownIcon.vue'
|
|
14
|
+
|
|
15
|
+
type SelectValue = string | number
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{ (e: 'update:modelValue', newValue: SelectValue): void }>()
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
options : Array<Option | Record<string, Option[]>>
|
|
21
|
+
name: string
|
|
22
|
+
modelValue?: SelectValue
|
|
23
|
+
cssStyle?: InputBoxStyleForEachStatus | undefined
|
|
24
|
+
placeholder?: string
|
|
25
|
+
canOmitSelect?: boolean
|
|
26
|
+
isDisabled? : boolean
|
|
27
|
+
isRequired?: boolean
|
|
28
|
+
}>(), {
|
|
29
|
+
modelValue : undefined,
|
|
30
|
+
cssStyle : undefined,
|
|
31
|
+
placeholder: '選択してください',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const errorMessages = ref<Array<string>>([])
|
|
35
|
+
|
|
36
|
+
const selectedValue = computed<SelectValue>({
|
|
37
|
+
get: () => props.modelValue ?? '',
|
|
38
|
+
set: (newValue: SelectValue) => emit('update:modelValue', newValue),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const isOption = (obj: unknown): obj is Option => {
|
|
42
|
+
return (
|
|
43
|
+
typeof obj === 'object' &&
|
|
44
|
+
obj !== null &&
|
|
45
|
+
'label' in obj && typeof obj.label === 'string' &&
|
|
46
|
+
'value' in obj
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const validateValue = () => {
|
|
51
|
+
errorMessages.value = []
|
|
52
|
+
if (!selectedValue.value && props.isRequired) errorMessages.value.push('必須項目です')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watch(() => selectedValue.value, () => validateValue(), { immediate: !!props.modelValue, flush: 'post' })
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<InputBox
|
|
60
|
+
:cssStyle="cssStyle"
|
|
61
|
+
:class="$style.select_box"
|
|
62
|
+
:isDisabled="isDisabled"
|
|
63
|
+
>
|
|
64
|
+
<select
|
|
65
|
+
v-model="selectedValue"
|
|
66
|
+
:disabled="isDisabled"
|
|
67
|
+
:required="isRequired"
|
|
68
|
+
:class="$style.select"
|
|
69
|
+
@blur="validateValue()"
|
|
70
|
+
>
|
|
71
|
+
<option
|
|
72
|
+
v-if="canOmitSelect"
|
|
73
|
+
value=""
|
|
74
|
+
>
|
|
75
|
+
{{ placeholder || '選択しない' }}
|
|
76
|
+
</option>
|
|
77
|
+
<option
|
|
78
|
+
v-else
|
|
79
|
+
disabled
|
|
80
|
+
selected
|
|
81
|
+
value=""
|
|
82
|
+
>
|
|
83
|
+
{{ placeholder || '選択してください' }}
|
|
84
|
+
</option>
|
|
85
|
+
<option
|
|
86
|
+
v-if="['0', 'NA'].includes(selectedValue.toString()) && isDisabled"
|
|
87
|
+
disabled
|
|
88
|
+
selected
|
|
89
|
+
:value="selectedValue"
|
|
90
|
+
>
|
|
91
|
+
{{ placeholder || '選択してください' }}
|
|
92
|
+
</option>
|
|
93
|
+
<template
|
|
94
|
+
v-for="option in options"
|
|
95
|
+
:key="isOption(option) ? option.value : Object.keys(option)[0]"
|
|
96
|
+
>
|
|
97
|
+
<option
|
|
98
|
+
v-if="isOption(option)"
|
|
99
|
+
:value="option.value"
|
|
100
|
+
:disabled="option.isDisabled"
|
|
101
|
+
>
|
|
102
|
+
{{ option.label }}
|
|
103
|
+
</option>
|
|
104
|
+
<template v-else>
|
|
105
|
+
<optgroup
|
|
106
|
+
v-for="(opt, key) in option"
|
|
107
|
+
:key="key"
|
|
108
|
+
:label="key"
|
|
109
|
+
>
|
|
110
|
+
<option
|
|
111
|
+
v-for="o in opt"
|
|
112
|
+
:key="o.value"
|
|
113
|
+
:value="o.value"
|
|
114
|
+
:disabled="o.isDisabled"
|
|
115
|
+
>
|
|
116
|
+
{{ o.label }}
|
|
117
|
+
</option>
|
|
118
|
+
</optgroup>
|
|
119
|
+
<hr>
|
|
120
|
+
</template>
|
|
121
|
+
</template>
|
|
122
|
+
</select>
|
|
123
|
+
<div :class="$style.arrow_container">
|
|
124
|
+
<slot name="arrow" />
|
|
125
|
+
<KeyboardArrowDownIcon
|
|
126
|
+
v-if="!$slots.arrow"
|
|
127
|
+
:class="$style.arrow"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
<ErrorMessage
|
|
131
|
+
:errorMessages="errorMessages"
|
|
132
|
+
:cssStyle="{
|
|
133
|
+
textColor: cssStyle?.error?.backgroundColor,
|
|
134
|
+
backgroundColor: cssStyle?.error?.textColor,
|
|
135
|
+
}"
|
|
136
|
+
/>
|
|
137
|
+
</InputBox>
|
|
138
|
+
</template>
|
|
139
|
+
|
|
140
|
+
<style lang="scss" module>
|
|
141
|
+
:is(.select_box) {
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
|
|
144
|
+
> select {
|
|
145
|
+
flex : 1 1 auto;
|
|
146
|
+
padding-inline-end: 2rem;
|
|
147
|
+
cursor : pointer;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:is(.arrow_container) {
|
|
152
|
+
display : flex;
|
|
153
|
+
align-items : center;
|
|
154
|
+
position : absolute;
|
|
155
|
+
margin : auto;
|
|
156
|
+
inset-inline-end: .5rem;
|
|
157
|
+
inset-block : 0;
|
|
158
|
+
pointer-events : none;
|
|
159
|
+
|
|
160
|
+
> * {
|
|
161
|
+
color : currentColor;
|
|
162
|
+
fill : currentColor;
|
|
163
|
+
block-size: 1rem;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Ref } from 'vue'
|
|
3
|
+
import { ref, onMounted, onUpdated, watch } from 'vue'
|
|
4
|
+
import IconChevronDown from '../components/Icon/KeyboardArrowDownIcon.vue'
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
isOpened?: boolean | null
|
|
7
|
+
isHiddenArrow?: boolean
|
|
8
|
+
isDisabled?: boolean
|
|
9
|
+
isDisableClickOutside?: boolean
|
|
10
|
+
duration?: number
|
|
11
|
+
}>(), {
|
|
12
|
+
isOpened: null,
|
|
13
|
+
duration: .3,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const isOpenedContents = ref(props.isOpened || false)
|
|
17
|
+
const contents: Ref<HTMLElement | null> = ref(null)
|
|
18
|
+
const contentsHeight = ref(0)
|
|
19
|
+
const toggleBox = () => isOpenedContents.value = !isOpenedContents.value
|
|
20
|
+
|
|
21
|
+
const closeDropDown = () => {
|
|
22
|
+
if (!props.isDisableClickOutside) isOpenedContents.value = false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const updateContentsHeight = () => {
|
|
26
|
+
const contentsValue = contents.value
|
|
27
|
+
if (contentsValue) {
|
|
28
|
+
contentsHeight.value = contentsValue.clientHeight
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
onMounted(()=> updateContentsHeight())
|
|
33
|
+
onUpdated(() => updateContentsHeight())
|
|
34
|
+
watch(() => props.isOpened, newValue => {
|
|
35
|
+
if (typeof newValue === 'boolean') isOpenedContents.value = newValue
|
|
36
|
+
})
|
|
37
|
+
defineExpose({ isOpenedContents })
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<div
|
|
42
|
+
v-click-outside="closeDropDown"
|
|
43
|
+
:class="{ [$style.opened]: isOpenedContents }"
|
|
44
|
+
:style="{ '--accordion-toggle-duration': `${props.duration}s` }"
|
|
45
|
+
>
|
|
46
|
+
<button
|
|
47
|
+
:class="$style.trigger"
|
|
48
|
+
:disabled="isDisabled"
|
|
49
|
+
type="button"
|
|
50
|
+
@click.prevent="toggleBox"
|
|
51
|
+
>
|
|
52
|
+
<div class="width:100% text-align:left">
|
|
53
|
+
<slot name="trigger" />
|
|
54
|
+
</div>
|
|
55
|
+
<IconChevronDown
|
|
56
|
+
v-if="!isHiddenArrow"
|
|
57
|
+
:class="$style.icon"
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
<div
|
|
61
|
+
:style="{ height: isOpenedContents ? `${contentsHeight}px` : 0 }"
|
|
62
|
+
:class="$style.contents"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
ref="contents"
|
|
66
|
+
:class="$style.container"
|
|
67
|
+
>
|
|
68
|
+
<slot />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<style lang="scss" module>
|
|
75
|
+
@use '../assets/scss/mixin' as *;
|
|
76
|
+
|
|
77
|
+
@keyframes overflow {
|
|
78
|
+
0% {
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
99% {
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
100% {
|
|
87
|
+
overflow: visible;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.trigger {
|
|
92
|
+
display : grid;
|
|
93
|
+
grid-template-columns: 1fr auto;
|
|
94
|
+
align-items : center;
|
|
95
|
+
inline-size : 100%;
|
|
96
|
+
justify-items : start;
|
|
97
|
+
cursor : pointer;
|
|
98
|
+
position : relative;
|
|
99
|
+
color : var(--link-color);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.icon {
|
|
103
|
+
@include icon($color: var(--link-color));
|
|
104
|
+
flex : 0 0 auto;
|
|
105
|
+
transition: all .1s;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.contents {
|
|
109
|
+
transition: height var(--accordion-toggle-duration);
|
|
110
|
+
overflow : hidden;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.opened {
|
|
114
|
+
.icon {
|
|
115
|
+
transform: rotate(180deg);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.contents {
|
|
119
|
+
animation : overflow var(--accordion-toggle-duration);
|
|
120
|
+
animation-fill-mode: forwards;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { COLOR } from '../const'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
tabs: {
|
|
7
|
+
key: string
|
|
8
|
+
label: string
|
|
9
|
+
}[]
|
|
10
|
+
color?: {
|
|
11
|
+
active: string
|
|
12
|
+
background: string
|
|
13
|
+
text: string
|
|
14
|
+
}
|
|
15
|
+
cssStyle?: {
|
|
16
|
+
textColor: string
|
|
17
|
+
backgroundColor: string
|
|
18
|
+
border: {
|
|
19
|
+
color: string
|
|
20
|
+
size: string
|
|
21
|
+
radius: string
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
type?: 'tab' | 'button' | 'border'
|
|
25
|
+
initialIndex?: number
|
|
26
|
+
}>(), {
|
|
27
|
+
color : undefined,
|
|
28
|
+
cssStyle : undefined,
|
|
29
|
+
type : 'tab',
|
|
30
|
+
initialIndex: 0,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const activeTab = ref(props.tabs[props.initialIndex].key)
|
|
34
|
+
const tabRefs = ref<HTMLButtonElement[]>([])
|
|
35
|
+
const changeTabs = (key: string) => activeTab.value = key
|
|
36
|
+
|
|
37
|
+
const activateTab = (index: number) => {
|
|
38
|
+
activeTab.value = props.tabs[index].key
|
|
39
|
+
tabRefs.value[index]?.focus()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 以前は window 全体に keydown を張っていたため、フォーカス位置と無関係にタブが
|
|
43
|
+
// 切り替わり、1 画面に複数設置すると互いに競合した。
|
|
44
|
+
// タブリストにフォーカスがあるときだけ矢印キーで移動する(WAI-ARIA Tabs パターン)
|
|
45
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
46
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
|
47
|
+
|
|
48
|
+
const currentIndex = props.tabs.findIndex(tab => tab.key === activeTab.value)
|
|
49
|
+
if (currentIndex === -1) return
|
|
50
|
+
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
const lastIndex = props.tabs.length - 1
|
|
53
|
+
|
|
54
|
+
if (event.key === 'ArrowLeft') {
|
|
55
|
+
activateTab(currentIndex > 0 ? currentIndex - 1 : lastIndex)
|
|
56
|
+
} else {
|
|
57
|
+
activateTab(currentIndex < lastIndex ? currentIndex + 1 : 0)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div>
|
|
64
|
+
<div
|
|
65
|
+
:class="$style.tabs"
|
|
66
|
+
:style="{
|
|
67
|
+
'--active-color': color?.active || COLOR.blue,
|
|
68
|
+
'--background-color': color?.background || 'transparent',
|
|
69
|
+
}"
|
|
70
|
+
role="tablist"
|
|
71
|
+
@keydown="handleKeydown"
|
|
72
|
+
>
|
|
73
|
+
<button
|
|
74
|
+
v-for="(tab, index) in tabs"
|
|
75
|
+
:id="tab.key"
|
|
76
|
+
:key="tab.key"
|
|
77
|
+
:ref="el => { if (el) tabRefs[index] = el as HTMLButtonElement }"
|
|
78
|
+
role="tab"
|
|
79
|
+
:aria-controls="`${tab.key}_${index}`"
|
|
80
|
+
:aria-selected="activeTab === tab.key"
|
|
81
|
+
:tabindex="activeTab === tab.key ? 0 : -1"
|
|
82
|
+
:class="{ [$style.active]: activeTab === tab.key }"
|
|
83
|
+
@click="changeTabs(tab.key)"
|
|
84
|
+
>
|
|
85
|
+
<slot :name="tab.key" />
|
|
86
|
+
<template v-if="!$slots[tab.key]">
|
|
87
|
+
{{ tab.label }}
|
|
88
|
+
</template>
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
<div
|
|
92
|
+
v-for="(tab, index) in tabs"
|
|
93
|
+
v-show="activeTab === tab.key"
|
|
94
|
+
:id="`${tab.key}_${index}`"
|
|
95
|
+
:key="`${tab.key}_panel`"
|
|
96
|
+
role="tabpanel"
|
|
97
|
+
:aria-labelledby="tab.key"
|
|
98
|
+
>
|
|
99
|
+
<slot :name="`${tab.key}Contents`" />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
103
|
+
|
|
104
|
+
<style lang="scss" module>
|
|
105
|
+
.tabs {
|
|
106
|
+
display : flex;
|
|
107
|
+
background-color: var(--background-color);
|
|
108
|
+
|
|
109
|
+
> button {
|
|
110
|
+
&[role="tab"] {
|
|
111
|
+
border : none;
|
|
112
|
+
background-color: transparent;
|
|
113
|
+
font-size : 1rem;
|
|
114
|
+
padding : .5rem 1rem;
|
|
115
|
+
cursor : pointer;
|
|
116
|
+
|
|
117
|
+
&.active {
|
|
118
|
+
cursor: auto;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type {
|
|
3
|
+
Validates,
|
|
4
|
+
InputBoxStyleForEachStatus,
|
|
5
|
+
} from '../types'
|
|
6
|
+
import {
|
|
7
|
+
ref,
|
|
8
|
+
computed,
|
|
9
|
+
watch,
|
|
10
|
+
nextTick,
|
|
11
|
+
onMounted,
|
|
12
|
+
} from 'vue'
|
|
13
|
+
import { isEmptyValue, validateInputValue } from '@geckou/ui-core'
|
|
14
|
+
import InputBox from '../components/InputBox.vue'
|
|
15
|
+
import ErrorMessage from '../components/ErrorMessage.vue'
|
|
16
|
+
|
|
17
|
+
type InputValue = string | null
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{ (e: 'update:modelValue', newValue: InputValue): void }>()
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(defineProps<{
|
|
22
|
+
name: string
|
|
23
|
+
modelValue?: InputValue
|
|
24
|
+
cssStyle?: InputBoxStyleForEachStatus | undefined
|
|
25
|
+
placeholder?: string
|
|
26
|
+
isDisabled? : boolean
|
|
27
|
+
isRequired?: boolean
|
|
28
|
+
rows?: number
|
|
29
|
+
maxLength?: number
|
|
30
|
+
autocomplete?: string
|
|
31
|
+
validates?: Validates
|
|
32
|
+
autoAdjustHeight?: boolean
|
|
33
|
+
}>(), {
|
|
34
|
+
modelValue : undefined,
|
|
35
|
+
cssStyle : undefined,
|
|
36
|
+
isRequired : false,
|
|
37
|
+
placeholder : '入力してください',
|
|
38
|
+
rows : undefined,
|
|
39
|
+
maxLength : 100,
|
|
40
|
+
isDisabled : false,
|
|
41
|
+
autocomplete: 'off',
|
|
42
|
+
validates : () => [],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const inputValue = computed<InputValue>({
|
|
46
|
+
get: () => props.modelValue ?? '',
|
|
47
|
+
set: (newValue: InputValue) => emit('update:modelValue', newValue),
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const errorMessages = ref<string[]>([])
|
|
51
|
+
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
|
52
|
+
|
|
53
|
+
const validateValue = () => {
|
|
54
|
+
errorMessages.value = validateInputValue(inputValue.value, {
|
|
55
|
+
isRequired: props.isRequired,
|
|
56
|
+
validates : props.validates,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const adjustTextareaHeight = () => {
|
|
61
|
+
if (!props.autoAdjustHeight) return
|
|
62
|
+
|
|
63
|
+
nextTick(() => {
|
|
64
|
+
if (textareaRef.value) {
|
|
65
|
+
textareaRef.value.style.height = 'auto'
|
|
66
|
+
textareaRef.value.style.height = `calc(${textareaRef.value.scrollHeight}px - 2rem)`
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
onMounted(adjustTextareaHeight)
|
|
72
|
+
|
|
73
|
+
watch(inputValue, () => {
|
|
74
|
+
validateValue()
|
|
75
|
+
adjustTextareaHeight()
|
|
76
|
+
},
|
|
77
|
+
// 数値 0 も初期値として扱うため truthy 判定は使わない
|
|
78
|
+
{ immediate: !isEmptyValue(props.modelValue), flush: 'post' })
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<InputBox
|
|
83
|
+
:cssStyle="cssStyle"
|
|
84
|
+
:class="$style.text_area"
|
|
85
|
+
:isErrored="!!errorMessages.length"
|
|
86
|
+
:isDisabled="isDisabled"
|
|
87
|
+
>
|
|
88
|
+
<textarea
|
|
89
|
+
ref="textareaRef"
|
|
90
|
+
v-model="inputValue"
|
|
91
|
+
:name="name"
|
|
92
|
+
:required="isRequired"
|
|
93
|
+
:placeholder="placeholder"
|
|
94
|
+
:disabled="isDisabled"
|
|
95
|
+
:autocomplete="autocomplete"
|
|
96
|
+
:rows="rows"
|
|
97
|
+
:maxlength="maxLength"
|
|
98
|
+
:aria-invalid="!!errorMessages.length ? 'true' : undefined"
|
|
99
|
+
@blur="validateValue()"
|
|
100
|
+
/>
|
|
101
|
+
<ErrorMessage
|
|
102
|
+
:errorMessages="errorMessages"
|
|
103
|
+
:cssStyle="{
|
|
104
|
+
textColor: cssStyle?.error?.backgroundColor,
|
|
105
|
+
backgroundColor: cssStyle?.error?.textColor,
|
|
106
|
+
}"
|
|
107
|
+
/>
|
|
108
|
+
</InputBox>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<style module>
|
|
112
|
+
:is(.text_area) {
|
|
113
|
+
min-block-size: 4em;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import type {
|
|
3
|
+
Validates,
|
|
4
|
+
InputBoxStyleForEachStatus,
|
|
5
|
+
} from '../types'
|
|
6
|
+
import {
|
|
7
|
+
ref,
|
|
8
|
+
computed,
|
|
9
|
+
watch,
|
|
10
|
+
} from 'vue'
|
|
11
|
+
import {
|
|
12
|
+
convertFullWidthToHalfWidth,
|
|
13
|
+
isEmptyValue,
|
|
14
|
+
validateInputValue,
|
|
15
|
+
} from '@geckou/ui-core'
|
|
16
|
+
import InputBox from '../components/InputBox.vue'
|
|
17
|
+
import ErrorMessage from '../components/ErrorMessage.vue'
|
|
18
|
+
|
|
19
|
+
type InputValue = string | number
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{ (e: 'update:modelValue', newValue: InputValue): void }>()
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<{
|
|
24
|
+
name: string
|
|
25
|
+
modelValue?: InputValue
|
|
26
|
+
cssStyle?: InputBoxStyleForEachStatus | undefined
|
|
27
|
+
inputType? : string
|
|
28
|
+
placeholder?: string
|
|
29
|
+
isDisabled? : boolean
|
|
30
|
+
isRequired?: boolean
|
|
31
|
+
maxLength?: number
|
|
32
|
+
autocomplete?: string
|
|
33
|
+
validates?: Validates
|
|
34
|
+
}>(), {
|
|
35
|
+
modelValue : undefined,
|
|
36
|
+
cssStyle : undefined,
|
|
37
|
+
inputType : 'text',
|
|
38
|
+
placeholder : '入力してください',
|
|
39
|
+
maxLength : 30,
|
|
40
|
+
autocomplete: 'off',
|
|
41
|
+
validates : () => [],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const errorMessages = ref<string[]>([])
|
|
45
|
+
|
|
46
|
+
const inputValue = computed<InputValue>({
|
|
47
|
+
get: () => props.modelValue ?? '',
|
|
48
|
+
set: (newValue: InputValue) => {
|
|
49
|
+
const convertedValue = typeof newValue === 'number'
|
|
50
|
+
? newValue
|
|
51
|
+
: convertFullWidthToHalfWidth(String(newValue))
|
|
52
|
+
|
|
53
|
+
emit('update:modelValue', convertedValue)
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const validateValue = () => {
|
|
58
|
+
errorMessages.value = validateInputValue(inputValue.value, {
|
|
59
|
+
isRequired: props.isRequired,
|
|
60
|
+
validates : props.validates,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 初期値があるときはマウント時にも検証する。
|
|
65
|
+
// 数値 0 も初期値として扱うため truthy 判定は使わない
|
|
66
|
+
watch(inputValue, () => validateValue(), {
|
|
67
|
+
immediate: !isEmptyValue(props.modelValue),
|
|
68
|
+
flush : 'post',
|
|
69
|
+
})
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<InputBox
|
|
74
|
+
:cssStyle="cssStyle"
|
|
75
|
+
:class="$style.text_box"
|
|
76
|
+
:isErrored="!!errorMessages.length"
|
|
77
|
+
:isDisabled="isDisabled"
|
|
78
|
+
>
|
|
79
|
+
<slot name="before" />
|
|
80
|
+
<input
|
|
81
|
+
v-model="inputValue"
|
|
82
|
+
:type="inputType"
|
|
83
|
+
:name="name"
|
|
84
|
+
:required="isRequired"
|
|
85
|
+
:placeholder="placeholder"
|
|
86
|
+
:disabled="isDisabled"
|
|
87
|
+
:autocomplete="autocomplete"
|
|
88
|
+
:maxlength="maxLength"
|
|
89
|
+
:aria-invalid="!!errorMessages.length ? 'true' : undefined"
|
|
90
|
+
@blur="validateValue()"
|
|
91
|
+
>
|
|
92
|
+
<slot name="after" />
|
|
93
|
+
<ErrorMessage
|
|
94
|
+
:errorMessages="errorMessages"
|
|
95
|
+
:cssStyle="{
|
|
96
|
+
textColor: cssStyle?.error?.backgroundColor,
|
|
97
|
+
backgroundColor: cssStyle?.error?.textColor,
|
|
98
|
+
}"
|
|
99
|
+
/>
|
|
100
|
+
</InputBox>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<style lang="scss" module>
|
|
104
|
+
:is(.text_box) {
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
|
|
107
|
+
> input {
|
|
108
|
+
flex: 1 1 auto;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { COLOR } from '../const'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
text: string
|
|
7
|
+
variant?: 'default' | 'caution'
|
|
8
|
+
isDisabled?: boolean
|
|
9
|
+
}>(), {
|
|
10
|
+
variant : 'default',
|
|
11
|
+
isDisabled: false,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const textColor = computed(() => props.variant === 'caution' ? COLOR.red : 'var(--link-color)')
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
:class="$style.text_button"
|
|
21
|
+
:disabled="isDisabled"
|
|
22
|
+
:style="{ '--text-button-color': textColor }"
|
|
23
|
+
>
|
|
24
|
+
{{ text }}
|
|
25
|
+
</button>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style lang="scss" module>
|
|
29
|
+
.text_button {
|
|
30
|
+
padding : 0;
|
|
31
|
+
border : none;
|
|
32
|
+
background-color: transparent;
|
|
33
|
+
color : var(--text-button-color);
|
|
34
|
+
font-size : inherit;
|
|
35
|
+
line-height : 1;
|
|
36
|
+
cursor : pointer;
|
|
37
|
+
|
|
38
|
+
&:hover:not(:disabled) {
|
|
39
|
+
text-decoration: underline;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&:disabled {
|
|
43
|
+
color : var(--disable-text-color);
|
|
44
|
+
cursor: not-allowed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|