PluginProbe
The Innovative Form Builder – IvyForms / 1.1
The Innovative Form Builder – IvyForms v1.1
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 / button / IvyButtonAction.vue

IvyButtonAction.vue in The Innovative Form Builder – IvyForms 1.1, at frontend/src/views/_components/button/IvyButtonAction.vue

711 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div class="ivyforms-button-wrapper">
3 <button
4 ref="menuButtonRef"
5 type="button"
6 class="ivyforms-button-action"
7 :disabled="props.disabled || props.loading || props.comingSoon"
8 :class="[
9 'ivyforms-button-action__priority__' + props.priority,
10 'ivyforms-button-action__type__' + props.type,
11 'ivyforms-button-action__size__' + props.size,
12 {
13 'is-loading': props.loading,
14 'is-icon-start': props.iconStart && $slots.default && !props.iconOnly,
15 'is-icon-end': props.iconEnd && $slots.default && !props.iconOnly,
16 'is-icon-only': ((props.iconStart || props.iconEnd) && !$slots.default) || props.iconOnly,
17 },
18 ]"
19 :style="{ width: props.fullWidth ? '100%' : undefined }"
20 :aria-label="props.ariaLabel || getLabel('action_button')"
21 @click="handleClick"
22 @mousedown="handleMouseDown"
23 >
24 <span v-if="props.loading" class="ivyforms-button-action__spinner-container">
25 <svg viewBox="0 0 50 50" class="ivyforms-button-action__spinner">>
26 <circle class="ivyforms-button-action__spinner__line" cx="25" cy="25" r="22.5" />
27 </svg>
28 </span>
29 <IvyIcon
30 v-if="props.iconStart && !props.loading"
31 :name="props.iconStart"
32 :category="props.iconStartCategory"
33 :type="props.iconStartType"
34 :size="props.size"
35 :color="props.iconColor"
36 class="ivyforms-button-action__icon-start"
37 />
38 <span v-if="$slots.default && !props.iconOnly && !props.loading">
39 <slot />
40 </span>
41 <IvyIcon
42 v-if="props.iconEnd"
43 :name="props.iconEnd"
44 :category="props.iconEndCategory"
45 :type="props.iconEndType"
46 :size="props.size"
47 class="ivyforms-button-action__icon-end"
48 />
49 <svg class="ivyforms-button-action__transition" width="100%" height="100%" fill="none">>
50 <g>
51 <rect width="100%" height="100%" />
52 </g>
53 </svg>
54 </button>
55 <ComingSoonBadge
56 v-if="props.comingSoon"
57 :size="props.size === 'xs' ? 's' : props.size"
58 class="ivyforms-button-wrapper__coming-soon"
59 />
60 <ProBadge
61 v-if="props.pro"
62 image="pro-text"
63 :size="props.size"
64 class="ivyforms-button-wrapper__pro-badge ivyforms-flex-shrink-0"
65 />
66 </div>
67 </template>
68
69 <script setup lang="ts">
70 import type { IconCategory } from '@/types/icons/icon-category'
71 import type { IconType } from '@/types/icons/icon-type'
72 import { useLabels } from '@/composables/useLabels'
73 import ProBadge from '@/views/_components/badges/ProBadge.vue'
74 const { getLabel } = useLabels()
75
76 interface Props {
77 priority?:
78 | 'primary'
79 | 'secondary'
80 | 'tertiary'
81 | 'success'
82 | 'warning'
83 | 'danger'
84 | 'white'
85 | 'shadow-white'
86 | 'pro'
87 type?: 'fill' | 'border' | 'border-light' | 'ghost'
88 size?: 'l' | 'd' | 's' | 'xs'
89 disabled?: boolean
90 loading?: boolean
91 iconStart?: string
92 iconEnd?: string
93 iconStartCategory?: IconCategory
94 iconEndCategory?: IconCategory
95 iconStartType?: IconType
96 iconEndType?: IconType
97 fullWidth?: boolean
98 iconOnly?: boolean
99 comingSoon?: boolean
100 pro?: boolean
101 ariaLabel?: string
102 iconColor?: string
103 }
104
105 const props = withDefaults(defineProps<Props>(), {
106 priority: 'primary',
107 type: 'fill',
108 size: 'd',
109 disabled: false,
110 loading: false,
111 iconStart: '',
112 iconEnd: '',
113 iconStartCategory: 'global',
114 iconEndCategory: 'global',
115 iconStartType: 'fill',
116 iconEndType: 'fill',
117 fullWidth: false,
118 iconOnly: false,
119 ariaLabel: '',
120 iconColor: '',
121 })
122
123 const emit = defineEmits<{
124 (e: 'click', event: Event): void
125 }>()
126
127 const handleClick = (event: Event) => {
128 ;(document.activeElement as HTMLElement).blur()
129 emit('click', event)
130 }
131
132 const handleMouseDown = (event: Event) => {
133 event.preventDefault()
134 }
135 </script>
136
137 <style lang="scss">
138 @use 'sass:list' as *;
139 @use 'sass:meta' as *;
140
141 $icon-start: '.ivyforms-button-action__icon-start';
142 $icon-end: '.ivyforms-button-action__icon-end';
143 $transition: '.ivyforms-button-action__transition';
144 $type-fill: '.ivyforms-button-action__type__fill';
145 $type-border: '.ivyforms-button-action__type__border';
146 $type-border-light: '.ivyforms-button-action__type__border-light';
147 $type-ghost: '.ivyforms-button-action__type__ghost';
148
149 .ivyforms-button-wrapper {
150 position: relative;
151
152 &__coming-soon,
153 &__pro-badge {
154 position: absolute;
155 }
156 &__pro-badge {
157 border-radius: 16px;
158 padding: 2px 8px;
159 }
160
161 // Button Option
162 .ivyforms-button-action {
163 position: relative;
164 border-radius: 8px;
165 display: inline-flex;
166 align-items: center;
167 white-space: nowrap;
168 justify-content: center;
169 flex-shrink: 0;
170 -webkit-tap-highlight-color: transparent;
171 user-select: none;
172 outline: none;
173 border: none;
174 cursor: pointer;
175 font-weight: 500;
176 max-width: 100%;
177
178 // Everything except transition
179 > *:not(#{$transition}) {
180 z-index: 1;
181 }
182
183 // Transition
184 & #{$transition} {
185 position: absolute;
186 top: 0;
187 left: 0;
188 width: 100%;
189 height: 100%;
190 border-radius: 8px;
191 fill: transparent;
192 opacity: 0;
193 }
194
195 // Hover
196 @media (hover: hover) and (pointer: fine) {
197 &:hover:not(:focus):not(:disabled):not(:active) {
198 // Transition
199 & #{$transition} {
200 opacity: 0.05;
201 }
202
203 // Ghost and Border types for all priorities except secondary
204 &:not(.ivyforms-button-action__priority__secondary) {
205 &#{$type-ghost},
206 &#{$type-border},
207 &#{$type-border-light} {
208 & #{$transition} {
209 opacity: 0.08;
210 }
211 }
212 }
213 }
214 }
215
216 // Active
217 &:active:not(:disabled),
218 &[aria-expanded='true']='true'] {
219 // Transition
220 & #{$transition} {
221 opacity: 0.16;
222 fill: var(--map-ground-level-2-background);
223 }
224 }
225
226 // Focus
227 &:focus:not(:active) {
228 // Transition
229 & #{$transition} {
230 opacity: 0.4;
231 }
232 }
233
234 // Disabled & Loading
235 &:disabled,
236 &.is-loading {
237 opacity: 0.5;
238 }
239
240 // Priorities
241 $priorities: (
242 'primary': (
243 var(--map-base-brand-fill-0),
244 var(--primitive-white),
245 var(--map-base-brand-stroke-0),
246 var(--map-base-brand-fill-4),
247 var(--map-base-brand-fill-0),
248 var(--map-base-brand-symbol-0),
249 ),
250 'secondary': (
251 var(--map-base-purple-fill-0),
252 var(--primitive-white),
253 var(--map-base-purple-stroke-0),
254 var(--map-base-purple-fill-4),
255 var(--map-base-purple-fill-0),
256 var(--map-base-purple-symbol-0),
257 ),
258 'tertiary': (
259 var(--map-base-dusk-fill--4),
260 var(--map-base-dusk-symbol-2),
261 var(--map-base-dusk-stroke-0),
262 var(--map-base-dusk-fill-0),
263 var(--map-base-dusk-fill-4),
264 var(--map-base-dusk-symbol-2),
265 ),
266 'success': (
267 var(--map-status-success-fill-0),
268 var(--primitive-white),
269 var(--map-status-success-stroke-0),
270 var(--map-status-success-fill-4),
271 var(--map-status-success-fill-0),
272 var(--map-status-success-symbol-0),
273 ),
274 'warning': (
275 var(--map-status-warning-fill-0),
276 var(--primitive-white),
277 var(--map-status-warning-stroke-0),
278 var(--map-status-warning-fill-4),
279 var(--map-status-warning-fill-0),
280 var(--map-status-warning-symbol-0),
281 ),
282 'danger': (
283 var(--map-status-error-fill-0),
284 var(--primitive-white),
285 var(--map-status-error-stroke-0),
286 var(--map-status-error-fill-4),
287 var(--map-status-error-fill-0),
288 var(--map-status-error-symbol-0),
289 ),
290 'white': (
291 var(--primitive-white),
292 var(--primitive-black),
293 var(--primitive-white),
294 var(--primitive-white),
295 var(--map-bw-o90),
296 var(--map-bw),
297 ),
298 'shadow-white': (
299 var(--map-ground-level-2-background),
300 var(--map-base-dusk-symbol-2),
301 var(--primitive-white),
302 var(--primitive-white),
303 var(--primitive-white),
304 var(--map-base-dusk-symbol-2),
305 ),
306 'pro': (
307 var(--map-accent-amber-fill-0),
308 var(--primitive-white),
309 var(--map-accent-amber-stroke-0),
310 var(--map-accent-amber-fill-4),
311 var(--map-accent-amber-fill-0),
312 var(--map-accent-amber-symbol-0),
313 ),
314 );
315
316 // Priority Styles
317 @mixin priorityStyles($priority, $colors: ()) {
318 $fill-color: nth($colors, 1);
319 $text-color: nth($colors, 2);
320 $border-focus-color: nth($colors, 3);
321 $hover-fill-fill: nth($colors, 4);
322 $hover-fill-border-ghost: null;
323 @if length($colors) >= 5 {
324 $hover-fill-border-ghost: nth($colors, 5);
325 } @else {
326 $hover-fill-border-ghost: nth($colors, 4);
327 }
328 $text-color-border-ghost: null;
329 @if length($colors) >= 6 {
330 $text-color-border-ghost: nth($colors, 6);
331 } @else {
332 $text-color-border-ghost: nth($colors, 2);
333 }
334
335 // Priority
336 &__priority__#{$priority} {
337 color: $text-color;
338 --spinner-line-color: #{$text-color};
339 @if $priority == 'shadow-white' {
340 box-shadow: 0px 4px 15px 0px rgba(0, 0, 0, 0.05);
341 }
342
343 // Icon
344 #{$icon-start},
345 #{$icon-end} {
346 fill: $text-color;
347 stroke: $text-color;
348 }
349
350 // Focus
351 &:focus:not(:active) {
352 & .ivyforms-button-action__transition {
353 box-shadow: 0 0 0 4px $border-focus-color;
354 }
355 }
356
357 // Fill
358 &.ivyforms-button-action__type__fill {
359 background-color: $fill-color;
360
361 // Hover
362 @media (hover: hover) and (pointer: fine) {
363 &:hover:not(:focus):not(:disabled):not(:active) {
364 & .ivyforms-button-action__transition {
365 fill: $hover-fill-fill;
366 }
367 }
368 }
369 }
370
371 // Border & Ghost
372 &.ivyforms-button-action__type__border,
373 &.ivyforms-button-action__type__ghost,
374 &.ivyforms-button-action__type__border-light {
375 color: $text-color-border-ghost;
376 --spinner-line-color: #{$text-color-border-ghost};
377
378 @if $priority == 'shadow-white' {
379 box-shadow: none;
380 }
381
382 // Hover
383 @media (hover: hover) and (pointer: fine) {
384 &:hover:not(:focus):not(:disabled):not(:active) {
385 & .ivyforms-button-action__transition {
386 fill: $hover-fill-border-ghost;
387 }
388 }
389 }
390
391 // Primary
392 &.ivyforms-button-action__priority__primary {
393 // Icon
394 #{$icon-start},
395 #{$icon-end} {
396 fill: var(--map-base-brand-symbol-0);
397 }
398 }
399
400 // Secondary
401 &.ivyforms-button-action__priority__secondary {
402 // Icon
403 #{$icon-start},
404 #{$icon-end} {
405 fill: var(--map-base-purple-symbol-0);
406 }
407 }
408 // Success
409 &.ivyforms-button-action__priority__success {
410 // Icon
411 #{$icon-start},
412 #{$icon-end} {
413 fill: var(--map-base-brand-symbol-0);
414 }
415 }
416 // Warning
417 &.ivyforms-button-action__priority__warning {
418 // Icon
419 #{$icon-start},
420 #{$icon-end} {
421 fill: var(--map-status-warning-symbol-0);
422 }
423 }
424 // Danger
425 &.ivyforms-button-action__priority__danger {
426 // Icon
427 #{$icon-start},
428 #{$icon-end} {
429 fill: var(--map-status-error-symbol-0);
430 }
431 }
432 // White
433 &.ivyforms-button-action__priority__white {
434 color: var(--primitive-white);
435 // Icon
436 #{$icon-start},
437 #{$icon-end} {
438 fill: var(--primitive-white);
439 }
440 }
441 // Shadow White
442 &.ivyforms-button-action__priority__shadow-white {
443 color: var(--primitive-white);
444 // Icon
445 #{$icon-start},
446 #{$icon-end} {
447 fill: var(--primitive-white);
448 }
449 }
450
451 // Border
452 &.ivyforms-button-action__type__border {
453 border: 1px solid $border-focus-color;
454
455 // Focus
456 &:focus:not(:active) {
457 // Transition
458 & #{$transition} {
459 box-shadow: 0 0 0 5px $border-focus-color;
460 }
461 }
462 }
463
464 // Border light version - for dark backgrounds
465 &.ivyforms-button-action__type__border-light {
466 border: 1px solid var(--map-ground-level-3);
467 color: var(--map-ground-level-3);
468
469 // Focus state
470 &:focus:not(:active) {
471 & #{$transition} {
472 box-shadow: 0 0 0 5px var(--map-ground-level-3);
473 }
474 }
475 }
476 }
477 }
478 }
479
480 @each $priority, $colors in $priorities {
481 @include priorityStyles($priority, $colors);
482 }
483
484 // Type
485 &__type {
486 // Border
487 &__border,
488 &__border-light,
489 &__ghost {
490 background-color: transparent;
491 }
492 }
493
494 // Define sizes in a map
495 $sizes: (
496 'l': (
497 48px,
498 16px,
499 normal,
500 (10px, 24px, 10px, 24px),
501 // padding
502 (10px, 24px, 10px, 18px),
503 // padding with icon on start
504 (10px, 18px, 10px, 24px),
505 //padding with icon on end
506 8px,
507 2px,
508 -12px,
509 //top coming soon position
510 -9px,
511 //right coming soon position
512 ),
513 'd': (
514 40px,
515 16px,
516 normal,
517 (8px, 16px, 8px, 16px),
518 // padding
519 (8px, 16px, 8px, 10px),
520 // padding with icon on start
521 (8px, 10px, 8px, 16px),
522 //padding with icon on end
523 8px,
524 2px,
525 -13px,
526 //top coming soon position
527 -11px,
528 //right coming soon position
529 ),
530 's': (
531 32px,
532 14px,
533 normal,
534 (4px, 12px, 4px, 12px),
535 // padding
536 (4px, 12px, 4px, 6px),
537 // padding with icon on start
538 (4px, 6px, 4px, 12px),
539 //padding with icon on end
540 8px,
541 2px,
542 -8px,
543 //top coming soon position
544 -12px,
545 //right coming soon position
546 ),
547 'xs': (
548 24px,
549 12px,
550 normal,
551 (0, 8px, 0, 8px),
552 // padding
553 (0, 8px, 0, 4px),
554 // padding with icon on start
555 (0, 4px, 0, 8px),
556 //padding with icon on end
557 4px,
558 4px,
559 -8px,
560 //top coming soon position
561 -12px,
562 //right coming soon position
563 ),
564 );
565
566 // Coming Soon Badge positioning using the $sizes map
567 @each $size, $values in $sizes {
568 &__size__#{$size} ~ .ivyforms-button-wrapper__coming-soon {
569 top: nth($values, 9);
570 right: nth($values, 10);
571 }
572 &__size__#{$size} ~ .ivyforms-button-wrapper__pro-badge {
573 top: nth($values, 9);
574 right: nth($values, 10);
575 }
576 }
577
578 // Function to process each size value if is array
579 @function process-values($values) {
580 $processed: ();
581 @each $value in $values {
582 // Check if the item is a list (this is where padding array could be)
583 @if type-of($value) == 'list' {
584 $processed: append($processed, join($value, ', ')); // Convert list to string
585 } @else {
586 $processed: append($processed, $value);
587 }
588 }
589 @return $processed;
590 }
591
592 // Iterate over each size to apply styles
593 @each $size, $values in $sizes {
594 &__size__#{$size} {
595 height: nth($values, 1);
596 font-size: nth($values, 2);
597 line-height: nth($values, 3);
598 gap: nth($values, 7);
599 padding: process-values(nth($values, 4));
600
601 // For icon only buttons
602 &.is-icon-start {
603 padding: process-values(nth($values, 5));
604 }
605
606 // For icon only buttons
607 &.is-icon-end {
608 padding: process-values(nth($values, 6));
609 }
610
611 // For icon only buttons
612 &.is-icon-only {
613 width: nth($values, 1);
614 padding: 0;
615
616 // Adjust spinner container for icon only buttons
617 .ivyforms-button-action__spinner-container {
618 margin: 0;
619 }
620 }
621
622 // Spinner
623 .ivyforms-button-action__spinner {
624 padding: nth($values, 8);
625 }
626 }
627 }
628
629 // Icon Containers
630 #{$icon-start},
631 #{$icon-end} {
632 display: flex;
633 align-items: center;
634 justify-content: center;
635 flex-shrink: 0;
636 }
637
638 // Disabled & Loading
639 &:disabled,
640 &.is-loading {
641 cursor: not-allowed;
642 }
643
644 // Text
645 span {
646 display: inline-flex;
647 align-items: center;
648 text-overflow: ellipsis;
649 overflow: hidden;
650 white-space: nowrap;
651 }
652
653 // Icon
654 i {
655 display: inline-block;
656 }
657
658 // Spinner Container
659 &__spinner-container {
660 width: 24px;
661 height: 24px;
662 display: flex;
663 align-items: center;
664 justify-content: center;
665 }
666
667 // Spinner
668 &__spinner {
669 --spinner-size: 5;
670 --spinner-ring-size: 5;
671
672 display: block;
673 font-size: calc(var(--spinner-size) * 1em);
674 border-radius: 50%;
675
676 // Spinner Line
677 &__line {
678 fill: none;
679 stroke: var(--spinner-line-color);
680 stroke-width: var(--spinner-ring-size);
681 stroke-linecap: round;
682 transform-origin: 50% 50%;
683 transform: rotate3d(0, 0, 1, 0deg);
684 animation:
685 2156ms spinner-arc ease-in-out infinite,
686 1829ms spinner-rotate linear infinite;
687 }
688 }
689
690 // Loader Animation Rotate
691 @keyframes spinner-rotate {
692 to {
693 transform: rotate3d(0, 0, 1, 360deg);
694 }
695 }
696
697 // Loader Animation Arc
698 @keyframes spinner-arc {
699 from {
700 stroke-dasharray: 0 150;
701 stroke-dashoffset: 0;
702 }
703 to {
704 stroke-dasharray: 100 150;
705 stroke-dashoffset: -140;
706 }
707 }
708 }
709 }
710 </style>
711