PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / form / IvyFormItem.vue

IvyFormItem.vue in The Innovative Form Builder – IvyForms 0.8, at frontend/src/views/_components/form/IvyFormItem.vue

532 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div ref="formItem" class="ivyforms-form-item">
3 <ElFormItem
4 v-bind="{ ...$props, ...$attrs }"
5 :label-position="elementLabelPosition"
6 :label="props.label"
7 :class="[
8 {
9 'has-value': hasValue,
10 'is-active': isFormItemActive,
11 'is-form-element-hovered': isContentHovered,
12 'is-disabled': isFormItemDisabled,
13 'is-warning': warning,
14 'is-secondary': secondary,
15 'has-info-message': showInfo,
16 'has-custom-label': true,
17 },
18 getFormItemTypeClass(),
19 ]"
20 >
21 <!-- Always use custom label template to maintain consistent styling -->
22 <template #label>
23 <slot name="label" />
24 <!-- Conditionally use left or right label wrapper based on labelPosition -->
25 <div
26 v-if="props.labelPosition === 'left'"
27 class="ivyforms-form-item__left-label ivyforms-flex ivyforms-align-items-center ivyforms-gap-4"
28 >
29 <!--Label-->
30 <div class="ivyforms-form-item__label ivyforms-gap-2 ivyforms-p-0 medium-14">
31 <span>{{ props.label }}</span>
32 </div>
33
34 <span
35 v-if="props.required"
36 class="ivyforms-form-item__asterisk ivyforms-flex-shrink-0 medium-14"
37 >*</span
38 >
39
40 <!--Tooltip-->
41 <IvyTooltip
42 v-if="tooltip"
43 :content="tooltip"
44 raw-content
45 placement="top"
46 theme="inverted"
47 class="ivyforms-form-item__tooltip ivyforms-flex ivyforms-align-items-center ivyforms-flex-shrink-0"
48 >
49 <IvyIcon
50 name="question"
51 type="outline"
52 size="s"
53 outer-size="16px"
54 color="var(--map-base-dusk-symbol-2)"
55 @click.prevent
56 />
57 </IvyTooltip>
58 </div>
59
60 <!-- For right positioned labels -->
61 <div
62 v-else-if="props.labelPosition === 'right'"
63 class="ivyforms-form-item__right-label medium-14"
64 >
65 <!-- Label-->
66 <div class="ivyforms-form-item__label">
67 <span>{{ props.label }}</span>
68 </div>
69 <span v-if="props.required" class="ivyforms-form-item__asterisk medium-14">*</span>
70
71 <!-- Tooltip-->
72 <IvyTooltip
73 v-if="tooltip"
74 :content="tooltip"
75 raw-content
76 placement="top"
77 theme="inverted"
78 class="ivyforms-form-item__tooltip ivyforms-flex ivyforms-align-items-center ivyforms-flex-shrink-0"
79 >
80 <IvyIcon
81 name="question"
82 type="outline"
83 size="s"
84 outer-size="16px"
85 color="var(--map-base-dusk-symbol-2)"
86 @click.prevent
87 />
88 </IvyTooltip>
89 </div>
90
91 <!-- Default top positioning -->
92 <div
93 v-else
94 class="ivyforms-form-item__left-label ivyforms-flex ivyforms-align-items-center ivyforms-gap-4"
95 >
96 <!-- Label-->
97 <div class="ivyforms-form-item__label ivyforms-gap-2 ivyforms-p-0 medium-14">
98 <span>{{ props.label }}</span>
99 </div>
100 <span
101 v-if="props.required"
102 class="ivyforms-form-item__asterisk ivyforms-flex-shrink-0 medium-14"
103 >*</span
104 >
105
106 <!-- Tooltip-->
107 <IvyTooltip
108 v-if="tooltip"
109 :content="tooltip"
110 raw-content
111 placement="top"
112 theme="inverted"
113 class="ivyforms-form-item__tooltip"
114 >
115 <IvyIcon
116 name="question"
117 type="outline"
118 size="s"
119 outer-size="16px"
120 color="var(--map-base-dusk-symbol-2)"
121 @click.prevent
122 />
123 </IvyTooltip>
124 </div>
125
126 <!-- Original Right Label (for additional content) -->
127 <div v-if="rightLabel" class="ivyforms-form-item__right-label">
128 <!-- TODO This should be a link component with an icon-->
129 {{ props.rightLabel }}
130 </div>
131 </template>
132 <template #error="{ error }">
133 <slot name="error" />
134 <div class="el-form-item__error">
135 <IvyIcon
136 name="danger"
137 size="s"
138 type="fill-duo"
139 outer-size="16px"
140 color="var(--map-status-error-symbol-0)"
141 />
142 {{ error }}
143 </div>
144 </template>
145 <slot />
146 <div
147 v-if="showInfo"
148 class="ivyforms-form-item__info ivyforms-flex ivyforms-align-items-center regular-14"
149 :class="`ivyforms-${props.infoType}`"
150 >
151 <IvyIcon
152 v-if="props.showInfoIcon"
153 :name="infoIcon()"
154 type="fill-duo"
155 size="s"
156 :color="infoIconColor()"
157 outer-size="16px"
158 />
159 <div
160 class="ivyforms-form-item__info-text"
161 :style="{ '--ivyforms-info-color': infoIconColor() }"
162 >
163 {{ props.infoText }}
164 </div>
165 </div>
166 </ElFormItem>
167 </div>
168 </template>
169
170 <script setup lang="ts">
171 import { ref, provide, nextTick, computed } from 'vue'
172 import type { Editor } from '@tiptap/core'
173
174 interface Props {
175 tooltip?: string
176 label?: string
177 rightLabel?: string
178 inputRef?: { focus?: () => void; editor?: Editor; hasContent?: () => boolean }
179 showInfo?: boolean
180 infoType?: 'default' | 'success' | 'warning' | 'info' | 'error'
181 infoText?: string
182 staticLabel?: boolean
183 warning?: boolean
184 secondary?: boolean
185 showInfoIcon?: boolean
186 labelPosition?: string
187 required?: boolean
188 }
189
190 const props = withDefaults(defineProps<Props>(), {
191 tooltip: '',
192 label: '',
193 rightLabel: '',
194 inputRef: () => ({}),
195 showInfo: false,
196 infoType: 'default',
197 infoText: '',
198 staticLabel: false,
199 warning: false,
200 secondary: false,
201 showInfoIcon: true,
202 labelPosition: 'default',
203 required: false,
204 })
205
206 // Convert custom label position to Element Plus format
207 const elementLabelPosition = computed(() => {
208 switch (props.labelPosition) {
209 case 'left':
210 return 'left'
211 case 'right':
212 return 'right'
213 case 'top':
214 return 'top'
215 default:
216 return 'top' // Changed default to 'top'
217 }
218 })
219
220 const formItem = ref<HTMLElement | null>(null)
221 const hasValue = ref(false)
222 const isFormItemActive = ref(false)
223 const isContentHovered = ref(false)
224 const isFormItemDisabled = ref(false)
225
226 const getFormItemType = () => {
227 if (formItem.value?.querySelector('.el-date-editor')) {
228 return 'datepicker'
229 } else if (formItem.value?.querySelector('.el-select')) {
230 return 'select'
231 } else if (formItem.value?.querySelector('.ivyforms-phone-input')) {
232 return 'phone'
233 } else if (formItem.value?.querySelector('.el-textarea')) {
234 return 'textarea'
235 } else if (formItem.value?.querySelector('.el-input')) {
236 return 'input'
237 } else if (formItem.value?.querySelector('.ivyforms-editor')) {
238 return 'editor'
239 } else {
240 return ''
241 }
242 }
243
244 const getFormItemTypeClass = () => {
245 return `ivyforms-form-item-${getFormItemType()}`
246 }
247
248 const infoIcon = () => {
249 switch (props.infoType) {
250 case 'success':
251 return 'check-circle'
252 default:
253 return 'info'
254 }
255 }
256
257 const infoIconColor = () => {
258 switch (props.infoType) {
259 case 'success':
260 return 'var(--map-status-success-symbol-0)'
261 case 'warning':
262 return 'var(--map-status-warning-symbol-0)'
263 case 'error':
264 return 'var(--map-status-error-symbol-0)'
265 case 'info':
266 return 'var(--map-base-text-0)'
267 default:
268 return 'var(--map-base-text-0)'
269 }
270 }
271
272 const updateHasValue = () => {
273 nextTick(() => {
274 const input = formItem.value?.querySelector('.el-input__inner') as HTMLInputElement | null
275 const textarea = formItem.value?.querySelector('.el-textarea__inner') as HTMLInputElement | null
276 const select = formItem.value?.querySelector('.el-select') as HTMLElement | null
277
278 if (input) {
279 hasValue.value = input.value !== ''
280 } else if (textarea) {
281 hasValue.value = textarea.value !== ''
282 } else if (select) {
283 checkSelectHasValue()
284 }
285 })
286 }
287
288 const checkSelectHasValue = () => {
289 const parent = formItem.value?.querySelector(
290 '.ivyforms-select, .ivyforms-time-select, .ivyforms-multi-select',
291 )
292
293 if (parent) {
294 hasValue.value = parent.classList.contains('has-value')
295 }
296 }
297 defineExpose({
298 updateHasValue,
299 })
300 provide('isSecondary', props.secondary)
301 </script>
302
303 <style lang="scss">
304 //Form Item
305 .ivyforms-form-item {
306 position: relative;
307
308 &__asterisk {
309 color: var(--map-status-error-fill-0);
310 }
311
312 // Element Form Item
313 .el-form-item {
314 margin-bottom: 0; // Default margin
315 // When labelPosition is 'left' - use left label class for positioning
316 &[class*='el-form-item--label-left']*='el-form-item--label-left'] {
317 flex-direction: row;
318 align-items: flex-start;
319
320 .el-form-item__label {
321 flex-shrink: 0;
322 margin-right: 16px; // Add spacing between label and input
323 margin-bottom: 0; // Remove bottom margin for side positioning
324 min-width: 50px; // Ensure label has minimum width
325
326 // Style the left label when positioned on the left
327 .ivyforms-form-item__left-label {
328 align-items: center;
329 display: flex;
330 gap: 4px;
331 }
332 }
333
334 .el-form-item__content {
335 flex: 1;
336 }
337 }
338
339 // When labelPosition is 'right' - use right label class for positioning
340 &[class*='el-form-item--label-right']*='el-form-item--label-right'] {
341 flex-direction: row;
342 align-items: flex-start;
343
344 .el-form-item__label {
345 flex-shrink: 0;
346 margin-left: 16px; // Add spacing for right-positioned labels
347 margin-right: 0;
348 margin-bottom: 0;
349 min-width: 50px;
350 order: 2; // Move label to the right side
351
352 // Style the right label when positioned on the right
353 .ivyforms-form-item__right-label {
354 align-items: center;
355 display: flex;
356 gap: 4px;
357 // Override the underline styling for positional right labels
358 color: var(--map-base-text-0);
359 text-decoration: none;
360 margin-left: 0;
361 }
362 }
363
364 .el-form-item__content {
365 flex: 1;
366 order: 1; // Move content to the left side
367 }
368 }
369
370 // Remove margin when info message is present using class selector
371 &.has-info-message {
372 margin-bottom: 0;
373 }
374
375 // Content
376 .el-form-item__content {
377 display: flex;
378 flex-direction: column;
379 gap: var(--Spacing-xs, 6px);
380 align-items: flex-start;
381 justify-content: flex-start;
382 text-align: left;
383
384 // Error message
385 .el-form-item__error {
386 position: relative;
387 display: flex;
388 align-items: center;
389 padding: 0 var(--Spacing-4xs, 1px);
390 gap: var(--Spacing-2xs, 4px);
391 align-self: stretch;
392 font-size: 14px;
393 font-style: normal;
394 font-weight: 400;
395 line-height: 20px;
396
397 .ivyforms-icon {
398 .ivyforms-icon__svg {
399 width: auto;
400 }
401 }
402 }
403 // Force left alignment for radio groups
404 > .ivyforms-radio-group {
405 justify-content: flex-start !important;
406 margin-left: 0 !important;
407 }
408 }
409
410 // Label
411 .el-form-item__label {
412 display: flex;
413 align-items: center;
414 justify-content: flex-start;
415 padding: 0;
416 margin-bottom: 6px;
417 height: auto;
418
419 // Mandatory astrix
420 &::before {
421 display: none !important;
422 }
423
424 // Left label
425 .ivyforms-form-item__left-label {
426 // Label
427 .ivyforms-form-item__label {
428 cursor: text;
429 background-color: transparent;
430 color: var(--map-base-text-0);
431 order: 0;
432 height: auto;
433 }
434
435 // Asterisk
436 .ivyforms-form-item__asterisk {
437 order: 1;
438 }
439
440 // Tooltip
441 .ivyforms-form-item__tooltip {
442 order: 2;
443
444 .el-tooltip__trigger {
445 height: 20px;
446 display: flex;
447 align-items: center;
448 }
449 }
450 }
451
452 // Right label
453 .ivyforms-form-item__right-label {
454 order: 3;
455 margin-left: auto;
456 color: var(--map-base-dusk-symbol-2);
457 text-align: center;
458 text-decoration-line: none;
459 text-decoration-style: solid;
460 text-decoration-skip-ink: auto;
461 text-decoration-color: var(--map-base-dusk-symbol-2);
462 text-decoration-thickness: 10%;
463 text-underline-offset: 15.5%;
464 text-underline-position: from-font;
465 }
466 }
467
468 // Icons
469 .ivyforms-icon:not(.ivyforms-input__icon-end):not(.ivyforms-input__icon-start):not(
470 .ivyforms-input__clear
471 ):not(.selector__country-flag):not(.ivyforms-button-option__icon-start) {
472 display: contents;
473 }
474
475 // Disabled
476 &.is-disabled {
477 opacity: 0.5;
478 cursor: not-allowed;
479
480 // Label
481 .el-form-item__label {
482 cursor: not-allowed !important;
483 }
484 }
485
486 // Filled
487 &.is-filled {
488 .el-select__wrapper {
489 border: 1px solid var(--map-base-dusk-stroke--2);
490 background: var(--map-base-dusk-o05);
491
492 // Hover
493 &:hover {
494 border: 1px solid var(--map-base-dusk-stroke-0);
495 background: var(--map-base-dusk-o05);
496 }
497
498 // Active & Focus
499 &:active,
500 &.is-active,
501 &:focus,
502 &.is-focus,
503 &:focus-within {
504 border: 1px solid var(--map-base-brand-stroke-0);
505 background: var(--map-base-dusk-o05);
506 }
507 }
508 }
509
510 // Hint info message
511 .ivyforms-form-item__info {
512 align-self: stretch;
513 font-style: normal;
514 margin: 0;
515
516 .ivyforms-form-item__info-text {
517 color: var(--ivyforms-info-color);
518 padding: 0 var(--Spacing-4xs, 1px);
519 word-break: break-word;
520 }
521 }
522 }
523
524 .el-form-item.is-error,
525 .ivyforms-form-item__info {
526 svg {
527 min-width: 24px;
528 }
529 }
530 }
531 </style>
532