PluginProbe
The Innovative Form Builder – IvyForms / 0.6
The Innovative Form Builder – IvyForms v0.6
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.6, at frontend/src/views/_components/form/IvyFormItem.vue

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