|
56 | 56 | </teleport>
|
57 | 57 | </template>
|
58 | 58 |
|
59 |
| -<script lang="ts"> |
60 |
| -import {computed, defineComponent, onMounted, PropType, ref, watch} from 'vue' |
| 59 | +<script setup lang="ts"> |
| 60 | +// import type {BModalEmits, BModalProps} from '@/types/components' |
61 | 61 | import Modal from 'bootstrap/js/dist/modal'
|
62 |
| -import BButton from '../components/BButton/BButton.vue' |
63 |
| -import useEventListener from '../composables/useEventListener' |
64 |
| -import type {ColorVariant, InputSize} from '../types' |
| 62 | +import BButton from '@/components/BButton/BButton.vue' |
| 63 | +import useEventListener from '@/composables/useEventListener' |
| 64 | +import type {ColorVariant, InputSize} from '@/types' |
65 | 65 |
|
66 |
| -export default defineComponent({ |
67 |
| - name: 'BModal', |
68 |
| - components: {BButton}, |
69 |
| - inheritAttrs: false, |
70 |
| - props: { |
71 |
| - bodyBgVariant: {type: String as PropType<ColorVariant>, required: false}, |
72 |
| - bodyClass: {type: String, required: false}, |
73 |
| - bodyTextVariant: {type: String as PropType<ColorVariant>, required: false}, |
74 |
| - busy: {type: Boolean, default: false}, |
75 |
| - buttonSize: {type: String as PropType<InputSize>, default: 'md'}, |
76 |
| - cancelDisabled: {type: Boolean, default: false}, |
77 |
| - cancelTitle: {type: String, default: 'Cancel'}, |
78 |
| - cancelVariant: {type: String as PropType<ColorVariant>, default: 'secondary'}, |
79 |
| - centered: {type: Boolean, default: false}, |
80 |
| - contentClass: {type: String, required: false}, |
81 |
| - dialogClass: {type: String, required: false}, |
82 |
| - footerBgVariant: {type: String as PropType<ColorVariant>, required: false}, |
83 |
| - footerBorderVariant: {type: String as PropType<ColorVariant>, required: false}, |
84 |
| - footerClass: {type: String, required: false}, |
85 |
| - footerTextVariant: {type: String as PropType<ColorVariant>, required: false}, |
86 |
| - fullscreen: {type: [Boolean, String], default: false}, |
87 |
| - headerBgVariant: {type: String as PropType<ColorVariant>, required: false}, |
88 |
| - headerBorderVariant: {type: String as PropType<ColorVariant>, required: false}, |
89 |
| - headerClass: {type: String, required: false}, |
90 |
| - headerCloseLabel: {type: String, default: 'Close'}, |
91 |
| - headerCloseWhite: {type: Boolean, default: false}, |
92 |
| - headerTextVariant: {type: String as PropType<ColorVariant>, required: false}, |
93 |
| - hideBackdrop: {type: Boolean, default: false}, |
94 |
| - hideFooter: {type: Boolean, default: false}, |
95 |
| - hideHeader: {type: Boolean, default: false}, |
96 |
| - hideHeaderClose: {type: Boolean, default: false}, |
97 |
| - id: {type: String, required: false}, |
98 |
| - modalClass: {type: String, required: false}, |
99 |
| - modelValue: {type: Boolean, default: false}, |
100 |
| - noCloseOnBackdrop: {type: Boolean, default: false}, |
101 |
| - noCloseOnEsc: {type: Boolean, default: false}, |
102 |
| - noFade: {type: Boolean, default: false}, |
103 |
| - noFocus: {type: Boolean, default: false}, |
104 |
| - okDisabled: {type: Boolean, default: false}, |
105 |
| - okOnly: {type: Boolean, default: false}, |
106 |
| - okTitle: {type: String, default: 'Ok'}, |
107 |
| - okVariant: {type: String as PropType<ColorVariant>, default: 'primary'}, |
108 |
| - scrollable: {type: Boolean, default: false}, |
109 |
| - show: {type: Boolean, default: false}, |
110 |
| - size: {type: String, required: false}, |
111 |
| - title: {type: String, required: false}, |
112 |
| - titleClass: {type: String, required: false}, |
113 |
| - titleSrOnly: {type: Boolean, default: false}, |
114 |
| - titleTag: {type: String, default: 'h5'}, |
| 66 | +interface BModalProps { |
| 67 | + bodyBgVariant?: ColorVariant |
| 68 | + bodyClass?: string |
| 69 | + bodyTextVariant?: ColorVariant |
| 70 | + busy?: boolean |
| 71 | + buttonSize?: InputSize |
| 72 | + cancelDisabled?: boolean |
| 73 | + cancelTitle?: string |
| 74 | + cancelVariant?: ColorVariant |
| 75 | + centered?: boolean |
| 76 | + contentClass?: string |
| 77 | + dialogClass?: string |
| 78 | + footerBgVariant?: ColorVariant |
| 79 | + footerBorderVariant?: ColorVariant |
| 80 | + footerClass?: string |
| 81 | + footerTextVariant?: ColorVariant |
| 82 | + fullscreen?: boolean | string |
| 83 | + headerBgVariant?: ColorVariant |
| 84 | + headerBorderVariant?: ColorVariant |
| 85 | + headerClass?: string |
| 86 | + headerCloseLabel?: string |
| 87 | + headerCloseWhite?: boolean |
| 88 | + headerTextVariant?: ColorVariant |
| 89 | + hideBackdrop?: boolean |
| 90 | + hideFooter?: boolean |
| 91 | + hideHeader?: boolean |
| 92 | + hideHeaderClose?: boolean |
| 93 | + id?: string |
| 94 | + modalClass?: string |
| 95 | + modelValue?: boolean |
| 96 | + noCloseOnBackdrop?: boolean |
| 97 | + noCloseOnEsc?: boolean |
| 98 | + noFade?: boolean |
| 99 | + noFocus?: boolean |
| 100 | + okDisabled?: boolean |
| 101 | + okOnly?: boolean |
| 102 | + okTitle?: string |
| 103 | + okVariant?: ColorVariant |
| 104 | + scrollable?: boolean |
| 105 | + show?: boolean |
| 106 | + size?: string |
| 107 | + title?: string |
| 108 | + titleClass?: string |
| 109 | + titleSrOnly?: boolean |
| 110 | + titleTag?: string |
| 111 | +} |
| 112 | +
|
| 113 | +const props = withDefaults(defineProps<BModalProps>(), { |
| 114 | + busy: false, |
| 115 | + buttonSize: 'md', |
| 116 | + cancelDisabled: false, |
| 117 | + cancelTitle: 'Cancel', |
| 118 | + cancelVariant: 'secondary', |
| 119 | + centered: false, |
| 120 | + fullscreen: false, |
| 121 | + headerCloseLabel: 'Close', |
| 122 | + headerCloseWhite: false, |
| 123 | + hideBackdrop: false, |
| 124 | + hideFooter: false, |
| 125 | + hideHeader: false, |
| 126 | + hideHeaderClose: false, |
| 127 | + modelValue: false, |
| 128 | + noCloseOnBackdrop: false, |
| 129 | + noCloseOnEsc: false, |
| 130 | + noFade: false, |
| 131 | + noFocus: false, |
| 132 | + okDisabled: false, |
| 133 | + okOnly: false, |
| 134 | + okTitle: 'Ok', |
| 135 | + okVariant: 'primary', |
| 136 | + scrollable: false, |
| 137 | + show: false, |
| 138 | + titleSrOnly: false, |
| 139 | + titleTag: 'h5', |
| 140 | +}) |
| 141 | +
|
| 142 | +interface BModalEmits { |
| 143 | + (e: 'update:modelValue', value: boolean): void |
| 144 | + (e: 'show', value: Event): void |
| 145 | + (e: 'shown', value: Event): void |
| 146 | + (e: 'hide', value: Event): void |
| 147 | + (e: 'hidden', value: Event): void |
| 148 | + (e: 'hide-prevented', value: Event): void |
| 149 | + (e: 'ok'): void |
| 150 | + (e: 'cancel'): void |
| 151 | +} |
| 152 | +
|
| 153 | +const emit = defineEmits<BModalEmits>() |
| 154 | +
|
| 155 | +const slots = useSlots() |
| 156 | +
|
| 157 | +const element = ref<HTMLElement>() |
| 158 | +const instance = ref<Modal>() |
| 159 | +const modalClasses = computed(() => [ |
| 160 | + { |
| 161 | + fade: !props.noFade, |
| 162 | + show: props.show, |
115 | 163 | },
|
116 |
| - emits: ['update:modelValue', 'show', 'shown', 'hide', 'hidden', 'hide-prevented', 'ok', 'cancel'], |
117 |
| - setup(props, {emit, slots}) { |
118 |
| - const element = ref<HTMLElement>() |
119 |
| - const instance = ref<Modal>() |
120 |
| - const modalClasses = computed(() => [ |
121 |
| - { |
122 |
| - fade: !props.noFade, |
123 |
| - show: props.show, |
124 |
| - }, |
125 |
| - props.modalClass, |
126 |
| - ]) |
127 |
| - const modalDialogClasses = computed(() => [ |
128 |
| - { |
129 |
| - 'modal-fullscreen': typeof props.fullscreen === 'boolean' ? props.fullscreen : false, |
130 |
| - [`modal-fullscreen-${props.fullscreen}-down`]: |
131 |
| - typeof props.fullscreen === 'string' ? props.fullscreen : false, |
132 |
| - [`modal-${props.size}`]: props.size, |
133 |
| - 'modal-dialog-centered': props.centered, |
134 |
| - 'modal-dialog-scrollable': props.scrollable, |
135 |
| - }, |
136 |
| - props.dialogClass, |
137 |
| - ]) |
138 |
| -
|
139 |
| - const computedBodyClasses = computed(() => [ |
140 |
| - { |
141 |
| - [`bg-${props.bodyBgVariant}`]: props.bodyBgVariant, |
142 |
| - [`text-${props.bodyTextVariant}`]: props.bodyTextVariant, |
143 |
| - }, |
144 |
| - props.bodyClass, |
145 |
| - ]) |
146 |
| -
|
147 |
| - const computedHeaderClasses = computed(() => [ |
148 |
| - { |
149 |
| - [`bg-${props.headerBgVariant}`]: props.headerBgVariant, |
150 |
| - [`border-${props.headerBorderVariant}`]: props.headerBorderVariant, |
151 |
| - [`text-${props.headerTextVariant}`]: props.headerTextVariant, |
152 |
| - }, |
153 |
| - props.headerClass, |
154 |
| - ]) |
155 |
| -
|
156 |
| - const computedFooterClasses = computed(() => [ |
157 |
| - { |
158 |
| - [`bg-${props.footerBgVariant}`]: props.footerBgVariant, |
159 |
| - [`border-${props.footerBorderVariant}`]: props.footerBorderVariant, |
160 |
| - [`text-${props.footerTextVariant}`]: props.footerTextVariant, |
161 |
| - }, |
162 |
| - props.footerClass, |
163 |
| - ]) |
164 |
| -
|
165 |
| - const computedTitleClasses = computed(() => [ |
166 |
| - { |
167 |
| - ['visually-hidden']: props.titleSrOnly, |
168 |
| - }, |
169 |
| - props.titleClass, |
170 |
| - ]) |
171 |
| -
|
172 |
| - const hasHeaderCloseSlot = computed(() => !!slots['header-close']) |
173 |
| - const computedCloseButtonClasses = computed(() => [ |
174 |
| - { |
175 |
| - [`btn-close-content`]: hasHeaderCloseSlot.value, |
176 |
| - [`d-flex`]: hasHeaderCloseSlot.value, |
177 |
| - [`btn-close-white`]: !hasHeaderCloseSlot.value && props.headerCloseWhite, |
178 |
| - }, |
179 |
| - ]) |
180 |
| -
|
181 |
| - const disableCancel = computed(() => props.cancelDisabled || props.busy) |
182 |
| - const disableOk = computed(() => props.okDisabled || props.busy) |
183 |
| -
|
184 |
| - useEventListener(element, 'shown.bs.modal', (e) => emit('shown', e)) |
185 |
| - useEventListener(element, 'hidden.bs.modal', (e) => emit('hidden', e)) |
186 |
| - useEventListener(element, 'hidePrevented.bs.modal', (e) => emit('hide-prevented', e)) |
187 |
| -
|
188 |
| - useEventListener(element, 'show.bs.modal', (e) => { |
189 |
| - emit('show', e) |
190 |
| - if (!e.defaultPrevented) { |
191 |
| - emit('update:modelValue', true) |
192 |
| - } |
193 |
| - }) |
194 |
| -
|
195 |
| - useEventListener(element, 'hide.bs.modal', (e) => { |
196 |
| - emit('hide', e) |
197 |
| - if (!e.defaultPrevented) { |
198 |
| - emit('update:modelValue', false) |
199 |
| - } |
200 |
| - }) |
201 |
| -
|
202 |
| - onMounted(() => { |
203 |
| - instance.value = new Modal(element.value as HTMLElement, { |
204 |
| - backdrop: props.hideBackdrop |
205 |
| - ? false |
206 |
| - : props.noCloseOnBackdrop |
207 |
| - ? 'static' |
208 |
| - : !props.hideBackdrop, |
209 |
| - keyboard: !props.noCloseOnEsc, |
210 |
| - focus: !props.noFocus, |
211 |
| - }) |
212 |
| -
|
213 |
| - if (props.modelValue) { |
214 |
| - instance.value?.show() |
215 |
| - } |
216 |
| - }) |
217 |
| -
|
218 |
| - watch( |
219 |
| - () => props.modelValue, |
220 |
| - (value) => { |
221 |
| - if (value) { |
222 |
| - instance.value?.show() |
223 |
| - } else { |
224 |
| - instance.value?.hide() |
225 |
| - } |
226 |
| - } |
227 |
| - ) |
228 |
| -
|
229 |
| - return { |
230 |
| - element, |
231 |
| - disableCancel, |
232 |
| - disableOk, |
233 |
| - modalClasses, |
234 |
| - modalDialogClasses, |
235 |
| - computedBodyClasses, |
236 |
| - computedFooterClasses, |
237 |
| - computedHeaderClasses, |
238 |
| - computedTitleClasses, |
239 |
| - computedCloseButtonClasses, |
240 |
| - } |
| 164 | + props.modalClass, |
| 165 | +]) |
| 166 | +const modalDialogClasses = computed(() => [ |
| 167 | + { |
| 168 | + 'modal-fullscreen': typeof props.fullscreen === 'boolean' ? props.fullscreen : false, |
| 169 | + [`modal-fullscreen-${props.fullscreen}-down`]: |
| 170 | + typeof props.fullscreen === 'string' ? props.fullscreen : false, |
| 171 | + [`modal-${props.size}`]: props.size, |
| 172 | + 'modal-dialog-centered': props.centered, |
| 173 | + 'modal-dialog-scrollable': props.scrollable, |
| 174 | + }, |
| 175 | + props.dialogClass, |
| 176 | +]) |
| 177 | +
|
| 178 | +const computedBodyClasses = computed(() => [ |
| 179 | + { |
| 180 | + [`bg-${props.bodyBgVariant}`]: props.bodyBgVariant, |
| 181 | + [`text-${props.bodyTextVariant}`]: props.bodyTextVariant, |
| 182 | + }, |
| 183 | + props.bodyClass, |
| 184 | +]) |
| 185 | +
|
| 186 | +const computedHeaderClasses = computed(() => [ |
| 187 | + { |
| 188 | + [`bg-${props.headerBgVariant}`]: props.headerBgVariant, |
| 189 | + [`border-${props.headerBorderVariant}`]: props.headerBorderVariant, |
| 190 | + [`text-${props.headerTextVariant}`]: props.headerTextVariant, |
| 191 | + }, |
| 192 | + props.headerClass, |
| 193 | +]) |
| 194 | +
|
| 195 | +const computedFooterClasses = computed(() => [ |
| 196 | + { |
| 197 | + [`bg-${props.footerBgVariant}`]: props.footerBgVariant, |
| 198 | + [`border-${props.footerBorderVariant}`]: props.footerBorderVariant, |
| 199 | + [`text-${props.footerTextVariant}`]: props.footerTextVariant, |
| 200 | + }, |
| 201 | + props.footerClass, |
| 202 | +]) |
| 203 | +
|
| 204 | +const computedTitleClasses = computed(() => [ |
| 205 | + { |
| 206 | + ['visually-hidden']: props.titleSrOnly, |
241 | 207 | },
|
| 208 | + props.titleClass, |
| 209 | +]) |
| 210 | +
|
| 211 | +const hasHeaderCloseSlot = computed<boolean>(() => !!slots['header-close']) |
| 212 | +const computedCloseButtonClasses = computed(() => [ |
| 213 | + { |
| 214 | + [`btn-close-content`]: hasHeaderCloseSlot.value, |
| 215 | + [`d-flex`]: hasHeaderCloseSlot.value, |
| 216 | + [`btn-close-white`]: !hasHeaderCloseSlot.value && props.headerCloseWhite, |
| 217 | + }, |
| 218 | +]) |
| 219 | +
|
| 220 | +const disableCancel = computed<boolean>(() => props.cancelDisabled || props.busy) |
| 221 | +const disableOk = computed<boolean>(() => props.okDisabled || props.busy) |
| 222 | +
|
| 223 | +useEventListener(element, 'shown.bs.modal', (e) => emit('shown', e)) |
| 224 | +useEventListener(element, 'hidden.bs.modal', (e) => emit('hidden', e)) |
| 225 | +useEventListener(element, 'hidePrevented.bs.modal', (e) => emit('hide-prevented', e)) |
| 226 | +
|
| 227 | +useEventListener(element, 'show.bs.modal', (e) => { |
| 228 | + emit('show', e) |
| 229 | + if (!e.defaultPrevented) { |
| 230 | + emit('update:modelValue', true) |
| 231 | + } |
| 232 | +}) |
| 233 | +
|
| 234 | +useEventListener(element, 'hide.bs.modal', (e) => { |
| 235 | + emit('hide', e) |
| 236 | + if (!e.defaultPrevented) { |
| 237 | + emit('update:modelValue', false) |
| 238 | + } |
| 239 | +}) |
| 240 | +
|
| 241 | +onMounted(() => { |
| 242 | + instance.value = new Modal(element.value as HTMLElement, { |
| 243 | + backdrop: props.hideBackdrop ? false : props.noCloseOnBackdrop ? 'static' : !props.hideBackdrop, |
| 244 | + keyboard: !props.noCloseOnEsc, |
| 245 | + focus: !props.noFocus, |
| 246 | + }) |
| 247 | +
|
| 248 | + if (props.modelValue) { |
| 249 | + instance.value?.show() |
| 250 | + } |
| 251 | +}) |
| 252 | +
|
| 253 | +watch( |
| 254 | + () => props.modelValue, |
| 255 | + (value) => { |
| 256 | + if (value) { |
| 257 | + instance.value?.show() |
| 258 | + } else { |
| 259 | + instance.value?.hide() |
| 260 | + } |
| 261 | + } |
| 262 | +) |
| 263 | +</script> |
| 264 | + |
| 265 | +<script lang="ts"> |
| 266 | +import {computed, defineComponent, onMounted, ref, useSlots, watch} from 'vue' |
| 267 | +export default defineComponent({ |
| 268 | + inheritAttrs: false, |
242 | 269 | })
|
243 | 270 | </script>
|
0 commit comments