cja-phoenix 0.2.3 → 0.2.5

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.
Files changed (68) hide show
  1. package/dist/cja-phoenix.es.js +2808 -3294
  2. package/dist/style.css +1 -1
  3. package/dist/types/components/composite/CjaMenuBar.vue.d.ts +42 -0
  4. package/dist/types/components/composite/FunnelLayout.vue.d.ts +35 -0
  5. package/dist/types/components/composite/FunnelSubmit.vue.d.ts +2 -2
  6. package/dist/types/components/composite/FunnelSummary.vue.d.ts +14 -9
  7. package/dist/types/components/composite/FunnelTitle.vue.d.ts +1 -1
  8. package/dist/types/components/composite/JourneyMacroSteps.vue.d.ts +16 -0
  9. package/dist/types/components/composite/ProductDetails.vue.d.ts +4 -4
  10. package/dist/types/components/composite/ResultsLayout.vue.d.ts +11 -0
  11. package/dist/types/components/forms/CheckboxInput.vue.d.ts +8 -5
  12. package/dist/types/components/forms/FileInput.vue.d.ts +7 -4
  13. package/dist/types/components/forms/InputToggle.vue.d.ts +11 -7
  14. package/dist/types/components/forms/NumberInput.vue.d.ts +6 -3
  15. package/dist/types/components/forms/PhoneInput.vue.d.ts +18 -16
  16. package/dist/types/components/forms/RadioInput.vue.d.ts +41 -0
  17. package/dist/types/components/forms/SelectInput.vue.d.ts +18 -17
  18. package/dist/types/components/forms/SelectionTiles.vue.d.ts +7 -4
  19. package/dist/types/components/forms/TextInput.vue.d.ts +20 -18
  20. package/dist/types/components/forms/TileCheckboxInput.vue.d.ts +1 -1
  21. package/dist/types/components/forms/structure/InputContainer.vue.d.ts +2 -2
  22. package/dist/types/components/forms/structure/InputError.vue.d.ts +1 -1
  23. package/dist/types/components/forms/structure/InputTitle.vue.d.ts +1 -1
  24. package/dist/types/components/index.d.ts +10 -3
  25. package/dist/types/components/structural/CjaButton.vue.d.ts +8 -5
  26. package/dist/types/components/structural/CollapseContainer.vue.d.ts +3 -3
  27. package/dist/types/components/structural/ContentTabs.vue.d.ts +1 -1
  28. package/dist/types/components/structural/FixedContainer.vue.d.ts +63 -0
  29. package/dist/types/components/structural/GridContainer.vue.d.ts +9 -6
  30. package/dist/types/components/structural/GridItem.vue.d.ts +8 -8
  31. package/dist/types/components/structural/InfoMessage.vue.d.ts +30 -0
  32. package/dist/types/components/structural/LoadingSpinner.vue.d.ts +1 -1
  33. package/dist/types/components/structural/Modal.vue.d.ts +3 -3
  34. package/dist/types/components/structural/Scaffold.vue.d.ts +2 -2
  35. package/dist/types/index.d.ts +1 -0
  36. package/dist/types/stories/Modal.story.vue.d.ts +1 -1
  37. package/dist/types/types/MacroStep.d.ts +5 -0
  38. package/dist/types/types/index.d.ts +5 -0
  39. package/package.json +4 -4
  40. package/src/assets/iconia/demo.html +57 -1
  41. package/src/assets/iconia/fonts/CGG-icomoon.eot +0 -0
  42. package/src/assets/iconia/fonts/CGG-icomoon.svg +4 -0
  43. package/src/assets/iconia/fonts/CGG-icomoon.ttf +0 -0
  44. package/src/assets/iconia/fonts/CGG-icomoon.woff +0 -0
  45. package/src/assets/iconia/selection.json +1 -1
  46. package/src/assets/iconia/style.css +17 -5
  47. package/src/assets/iconia/style.scss +25 -5
  48. package/src/assets/iconia/variables.scss +4 -0
  49. package/src/components/composite/CjaMenuBar.vue +166 -0
  50. package/src/components/composite/FunnelLayout.vue +194 -0
  51. package/src/components/composite/FunnelSubmit.vue +20 -11
  52. package/src/components/composite/FunnelSummary.vue +16 -10
  53. package/src/components/composite/JourneyMacroSteps.vue +73 -0
  54. package/src/components/composite/ProductDetails.vue +4 -2
  55. package/src/components/composite/ResultsLayout.vue +49 -0
  56. package/src/components/forms/InputToggle.vue +6 -1
  57. package/src/components/forms/PhoneInput.vue +15 -6
  58. package/src/components/forms/RadioInput.vue +90 -0
  59. package/src/components/forms/SelectInput.vue +17 -7
  60. package/src/components/forms/SelectionTiles.vue +18 -5
  61. package/src/components/index.ts +16 -2
  62. package/src/components/structural/CjaButton.vue +14 -9
  63. package/src/components/structural/CollapseContainer.vue +35 -32
  64. package/src/components/structural/FixedContainer.vue +87 -0
  65. package/src/components/structural/InfoMessage.vue +97 -0
  66. package/src/index.ts +1 -0
  67. package/src/types/MacroStep.ts +5 -0
  68. package/src/types/index.ts +6 -0
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div class="results-container">
3
+ <slot name="mobile-controls" v-if="!activeViewport.lg"></slot>
4
+ <GridContainer class="results-grid">
5
+ <GridItem :size-lg="3" v-if="activeViewport.lg">
6
+ <div class="btn-container">
7
+ <button class="m-cgg-icon--arrow-back"></button>
8
+ </div>
9
+ <slot name="sidebar"></slot>
10
+ </GridItem>
11
+ <GridItem :size-sm="2" :size-md="4" :size-lg="9">
12
+ <slot name="content"></slot>
13
+ </GridItem>
14
+ </GridContainer>
15
+ </div>
16
+ </template>
17
+
18
+ <script lang="ts" setup>
19
+ import { inject } from "vue";
20
+ import GridContainer from "../structural/GridContainer.vue";
21
+ import GridItem from "../structural/GridItem.vue";
22
+
23
+ const activeViewport: any = inject("activeViewport");
24
+ </script>
25
+
26
+ <style lang="scss" scoped>
27
+ .results-container {
28
+ .results-grid {
29
+ padding-top: 24px;
30
+ padding-bottom: 24px;
31
+ }
32
+
33
+ .btn-container {
34
+ button {
35
+ background: none;
36
+ padding: 0;
37
+ border: none;
38
+ cursor: pointer;
39
+ font-size: 26px;
40
+ line-height: 37px;
41
+ margin-bottom: 15px;
42
+
43
+ :focus {
44
+ outline: none;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ </style>
@@ -2,7 +2,7 @@
2
2
  <div class="input-container">
3
3
  <div
4
4
  class="input-wrapper"
5
- :class="[`size-${size}`, { active: modelValue }]"
5
+ :class="[`size-${size}`, { active: modelValue, 'full-width': fullWidth }]"
6
6
  @click="$emit('update:modelValue', !modelValue)"
7
7
  >
8
8
  <div class="label">
@@ -25,6 +25,7 @@ const props = withDefaults(
25
25
  validation?: any;
26
26
  label: string;
27
27
  modelValue: boolean;
28
+ fullWidth: boolean;
28
29
  error?: string;
29
30
  errorDisplay?: boolean;
30
31
  }>(),
@@ -47,6 +48,10 @@ const emit = defineEmits(["update:modelValue"]);
47
48
  align-items: center;
48
49
  gap: 10px;
49
50
 
51
+ &.full-width {
52
+ justify-content: space-between;
53
+ }
54
+
50
55
  .label {
51
56
  @include input-title;
52
57
 
@@ -15,7 +15,10 @@
15
15
  :value="value"
16
16
  :autocomplete="autocomplete"
17
17
  @input="
18
- emit('update:modelValue', (<HTMLInputElement>$event.target).value)
18
+ emit(
19
+ 'update:modelValue',
20
+ Number((<HTMLInputElement>$event.target).value)
21
+ )
19
22
  "
20
23
  />
21
24
  </div>
@@ -46,7 +49,7 @@ const props = withDefaults(
46
49
  modelValue: InputHTMLAttributes["value"];
47
50
  id?: InputHTMLAttributes["id"];
48
51
  disabled?: InputHTMLAttributes["disabled"];
49
- phoneCountryCode: number;
52
+ phoneCountryCode?: number;
50
53
  autocomplete?: InputHTMLAttributes["autocomplete"];
51
54
  }>(),
52
55
  {
@@ -73,14 +76,20 @@ onMounted(() => {
73
76
  inputEl.value.addEventListener("countrychange", () =>
74
77
  emit(
75
78
  "update:phoneCountryCode",
76
- window.intlTelInputGlobals
77
- .getInstance(inputEl.value)
78
- .getSelectedCountryData().dialCode
79
+ Number(
80
+ window.intlTelInputGlobals
81
+ .getInstance(inputEl.value)
82
+ .getSelectedCountryData().dialCode
83
+ )
79
84
  )
80
85
  );
81
86
 
82
87
  intlTelInput(inputEl.value, {
83
- initialCountry: "pt",
88
+ initialCountry: props.phoneCountryCode
89
+ ? window.intlTelInputGlobals
90
+ .getCountryData()
91
+ .find((c) => Number(c.dialCode) == props.phoneCountryCode)?.iso2
92
+ : "pt",
84
93
  preferredCountries: ["pt"],
85
94
  separateDialCode: true,
86
95
  });
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div class="input-container">
3
+ <div class="input-container-radio">
4
+ <label>
5
+ <input
6
+ type="radio"
7
+ :name="name"
8
+ :checked="modelValue == value"
9
+ @change="handleChange"
10
+ />
11
+ <div class="radio-icon"></div>
12
+ <div class="text-container" v-html="label"></div>
13
+ </label>
14
+ </div>
15
+
16
+ <InputError :error="error" v-if="error && errorDisplay" />
17
+ </div>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import InputError from "./structure/InputError.vue";
22
+
23
+ const props = withDefaults(
24
+ defineProps<{
25
+ name: string;
26
+ value: any;
27
+ label: string;
28
+ modelValue?: any;
29
+ error?: string;
30
+ errorDisplay?: boolean;
31
+ }>(),
32
+ {
33
+ errorDisplay: true,
34
+ }
35
+ );
36
+
37
+ const handleChange = (event: any) => {
38
+ if (event.target.checked) {
39
+ emit("update:modelValue", props.value);
40
+ }
41
+ };
42
+
43
+ const emit = defineEmits(["update:modelValue"]);
44
+ </script>
45
+
46
+ <style lang="scss" scoped>
47
+ .input-container-radio {
48
+ label {
49
+ display: flex;
50
+ flex-direction: row;
51
+ align-items: center;
52
+ flex-wrap: nowrap;
53
+ gap: 15px;
54
+ cursor: pointer;
55
+ margin: 0;
56
+ font-weight: 400;
57
+
58
+ input {
59
+ display: none;
60
+ }
61
+
62
+ .radio-icon {
63
+ width: 20px;
64
+ height: 20px;
65
+ border: 1px solid #64748b;
66
+ border-radius: 50%;
67
+ }
68
+
69
+ input:checked + .radio-icon {
70
+ border: 6px solid #076b9c;
71
+ }
72
+
73
+ .text-container {
74
+ font-size: 16px;
75
+ line-height: 19px;
76
+ user-select: none;
77
+
78
+ a {
79
+ color: inherit;
80
+ font-weight: 700;
81
+ text-decoration: underline;
82
+
83
+ &:hover {
84
+ text-decoration: none;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ </style>
@@ -9,7 +9,11 @@
9
9
  >
10
10
  <div
11
11
  class="select-toggle"
12
- :class="[`size-${size}`, { open: open, disabled: disabled }]"
12
+ :class="[
13
+ `size-${size}`,
14
+ collapsePosition,
15
+ { open: open, disabled: disabled },
16
+ ]"
13
17
  @click="toggleCollapse()"
14
18
  >
15
19
  <span class="select-display">{{ displayValue || placeholder }}</span>
@@ -44,7 +48,7 @@
44
48
  ></span>
45
49
  {{ option.label }}
46
50
  </li>
47
- <li v-if="searchFilter && filteredOptions.length == 0">
51
+ <li v-if="searchFilter && filteredOptions?.length == 0">
48
52
  {{ searchFilter.noResults }}
49
53
  </li>
50
54
  </ul>
@@ -86,7 +90,7 @@ const props = withDefaults(
86
90
  id?: InputHTMLAttributes["id"];
87
91
  disabled?: InputHTMLAttributes["disabled"];
88
92
  modelValue: SelectHTMLAttributes["value"];
89
- options: SelectOption[];
93
+ options: SelectOption[] | null;
90
94
  multiSelect?: boolean;
91
95
  searchFilter?: {
92
96
  placeholder: string;
@@ -108,7 +112,7 @@ const inputEl = ref();
108
112
  const collapseEl = ref();
109
113
  const collapsePosition = ref();
110
114
  const filteredOptions = computed(() =>
111
- search.value
115
+ search.value && props.options
112
116
  ? props.options.filter((opt) =>
113
117
  opt.label.toLowerCase().includes(search.value.toLowerCase())
114
118
  )
@@ -127,7 +131,7 @@ defineExpose({ errorMessage, meta, validate });
127
131
  const emit = defineEmits(["update:modelValue"]);
128
132
 
129
133
  const displayValue = computed(() =>
130
- inputValue.value
134
+ inputValue.value && props.options
131
135
  ? props.multiSelect
132
136
  ? props.options
133
137
  .filter((o) => inputValue.value.includes(o.value))
@@ -193,7 +197,7 @@ const toggleCollapse = () => {
193
197
  }
194
198
  };
195
199
 
196
- const calcPosition = (el: HTMLElement) => {
200
+ const calcPosition = (el: any) => {
197
201
  const elRect = el.getBoundingClientRect();
198
202
  const inputRect = inputEl.value.getBoundingClientRect();
199
203
  const position =
@@ -252,7 +256,13 @@ const selectValue = (value: string) => {
252
256
  }
253
257
 
254
258
  &.open {
255
- border-radius: 5px 5px 0 0;
259
+ &.position-bottom {
260
+ border-radius: 5px 5px 0 0;
261
+ }
262
+
263
+ &.position-top {
264
+ border-radius: 0 0 5px 5px;
265
+ }
256
266
 
257
267
  em {
258
268
  transform: rotate(180deg);
@@ -7,8 +7,8 @@
7
7
  class="tile"
8
8
  :class="{
9
9
  active: multiselect
10
- ? modelValue.includes(option.value)
11
- : option.value == modelValue,
10
+ ? value.includes(option.value)
11
+ : option.value == value,
12
12
  }"
13
13
  @click="$emit('update:modelValue', option.value)"
14
14
  v-tippy="option.tooltip ? option.tooltip : ''"
@@ -72,6 +72,8 @@ defineEmits(["update:modelValue"]);
72
72
  </script>
73
73
 
74
74
  <style lang="scss" scoped>
75
+ @import "../../assets/shadows.scss";
76
+
75
77
  .tiles-container {
76
78
  display: grid;
77
79
  width: 100%;
@@ -95,10 +97,20 @@ defineEmits(["update:modelValue"]);
95
97
  padding-right: 20px;
96
98
  }
97
99
 
98
- &:hover,
100
+ @mixin activeStyle {
101
+ box-shadow: $box-shadow-m;
102
+ background-color: #f4f9fc;
103
+ border-color: #076b9c;
104
+ }
105
+
99
106
  &.active {
100
- color: #076b9c;
101
- box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.1);
107
+ @include activeStyle;
108
+ }
109
+
110
+ @media (any-hover: hover) {
111
+ &:hover {
112
+ @include activeStyle;
113
+ }
102
114
  }
103
115
 
104
116
  .text-wrapper {
@@ -167,6 +179,7 @@ defineEmits(["update:modelValue"]);
167
179
  &.layout-image {
168
180
  .tile {
169
181
  justify-content: center;
182
+ text-align: center;
170
183
 
171
184
  .image-container img {
172
185
  display: block;
@@ -2,31 +2,39 @@ import Modal from "./structural/Modal.vue";
2
2
  import CjaButton from "./structural/CjaButton.vue";
3
3
  import LoadingSpinner from "./structural/LoadingSpinner.vue";
4
4
  import ContentTabs from "./structural/ContentTabs.vue";
5
+ import InfoMessage from "./structural/InfoMessage.vue";
5
6
  import Scaffold from "./structural/Scaffold.vue";
6
7
  import GridContainer from "./structural/GridContainer.vue";
7
8
  import GridItem from "./structural/GridItem.vue";
9
+ import CollapseContainer from "./structural/CollapseContainer.vue";
10
+ import FixedContainer from "./structural/FixedContainer.vue";
8
11
 
9
12
  import TextInput from "./forms/TextInput.vue";
10
13
  import PhoneInput from "./forms/PhoneInput.vue";
11
14
  import CheckboxInput from "./forms/CheckboxInput.vue";
15
+ import RadioInput from "./forms/RadioInput.vue";
12
16
  import TileCheckboxInput from "./forms/TileCheckboxInput.vue";
13
17
  import SelectInput from "./forms/SelectInput.vue";
14
18
  import FileInput from "./forms/FileInput.vue";
15
19
  import NumberInput from "./forms/NumberInput.vue";
16
20
  import InputToggle from "./forms/InputToggle.vue";
17
- import CollapseContainer from "./structural/CollapseContainer.vue";
18
21
  import SelectionTiles from "./forms/SelectionTiles.vue";
19
22
 
20
- import ProductDetails from "./composite/ProductDetails.vue";
23
+ import JourneyMacroSteps from "./composite/JourneyMacroSteps.vue";
24
+ import FunnelLayout from "./composite/FunnelLayout.vue";
21
25
  import FunnelSubmit from "./composite/FunnelSubmit.vue";
22
26
  import FunnelSummary from "./composite/FunnelSummary.vue";
23
27
  import FunnelTitle from "./composite/FunnelTitle.vue";
28
+ import ResultsLayout from "./composite/ResultsLayout.vue";
29
+ import ProductDetails from "./composite/ProductDetails.vue";
30
+ import CjaMenuBar from "./composite/CjaMenuBar.vue";
24
31
 
25
32
  export {
26
33
  Modal,
27
34
  CjaButton,
28
35
  TextInput,
29
36
  PhoneInput,
37
+ RadioInput,
30
38
  CheckboxInput,
31
39
  TileCheckboxInput,
32
40
  NumberInput,
@@ -41,7 +49,13 @@ export {
41
49
  CollapseContainer,
42
50
  GridContainer,
43
51
  GridItem,
52
+ FunnelLayout,
44
53
  FunnelSubmit,
45
54
  FunnelSummary,
46
55
  FunnelTitle,
56
+ JourneyMacroSteps,
57
+ CjaMenuBar,
58
+ FixedContainer,
59
+ ResultsLayout,
60
+ InfoMessage,
47
61
  };
@@ -6,6 +6,7 @@
6
6
  `btn-size-${size}`,
7
7
  `btn-color-${color}`,
8
8
  `icon-${iconPosition}`,
9
+ { 'btn-loading': loading },
9
10
  ]"
10
11
  >
11
12
  <Scaffold v-if="!loading">
@@ -135,7 +136,7 @@ withDefaults(
135
136
  cursor: auto;
136
137
  }
137
138
 
138
- .spinner {
139
+ &.btn-loading .spinner {
139
140
  border-color: #fff;
140
141
  border-top-color: rgba(255, 255, 255, 0.2);
141
142
  }
@@ -165,14 +166,18 @@ withDefaults(
165
166
  flex-direction: row-reverse;
166
167
  }
167
168
 
168
- .spinner {
169
- width: 20px;
170
- height: 20px;
171
- border-width: 2px;
172
- border-style: solid;
173
- border-radius: 50%;
174
- animation: spin 1s infinite;
175
- animation-timing-function: linear;
169
+ &.btn-loading {
170
+ cursor: auto;
171
+
172
+ .spinner {
173
+ width: 20px;
174
+ height: 20px;
175
+ border-width: 2px;
176
+ border-style: solid;
177
+ border-radius: 50%;
178
+ animation: spin 1s infinite;
179
+ animation-timing-function: linear;
180
+ }
176
181
  }
177
182
  }
178
183
 
@@ -1,10 +1,6 @@
1
1
  <template>
2
- <div class="collapse-container">
3
- <div
4
- class="collapse-header"
5
- @click="active = !active"
6
- :class="{ active: active }"
7
- >
2
+ <div class="collapse-container" :class="{ active: active }">
3
+ <div class="collapse-header" @click="active = !active">
8
4
  <div class="header-wrapper">
9
5
  <slot name="header"></slot>
10
6
  </div>
@@ -14,13 +10,14 @@
14
10
  name="slide"
15
11
  @before-enter="setHeightZero"
16
12
  @enter="setHeightSize"
13
+ @after-enter="clearHeight"
17
14
  @leave="setHeightZero"
18
15
  >
19
16
  <div
20
17
  v-show="active"
21
18
  ref="contentContainer"
22
19
  class="content-container"
23
- :class="{ active: active }"
20
+ :style="{ height: containerHeight, overflow: containerOverflow }"
24
21
  >
25
22
  <div ref="contentWrapper" class="content-wrapper">
26
23
  <slot name="content"></slot>
@@ -43,11 +40,13 @@ const props = defineProps<{
43
40
  const active = ref(props.defaultActive);
44
41
  const contentContainer = ref();
45
42
  const contentWrapper = ref();
43
+ const containerHeight = ref();
44
+ const containerOverflow = ref();
46
45
 
47
46
  const setHeightSize = () => {
48
47
  requestAnimationFrame(() => {
49
48
  if (contentContainer.value && contentWrapper.value) {
50
- contentContainer.value.style.height = `${contentWrapper.value.clientHeight}px`;
49
+ containerHeight.value = `${contentWrapper.value.clientHeight}px`;
51
50
 
52
51
  if (active.value && props.scrollToContent) {
53
52
  setTimeout(() => {
@@ -58,24 +57,24 @@ const setHeightSize = () => {
58
57
  });
59
58
  }, 250);
60
59
  }
61
-
62
- setTimeout(() => {
63
- contentContainer.value.style.height = "";
64
- contentContainer.value.style.overflow = "visible";
65
- }, 200);
66
60
  }
67
61
  });
68
62
  };
69
63
 
70
64
  const setHeightZero = () => {
71
- contentContainer.value.style.height = `${contentWrapper.value.clientHeight}px`;
65
+ containerHeight.value = `${contentWrapper.value.clientHeight}px`;
72
66
 
73
67
  requestAnimationFrame(() => {
74
- contentContainer.value.style.height = "0";
75
- contentContainer.value.style.overflow = "";
68
+ containerHeight.value = "0";
69
+ containerOverflow.value = "";
76
70
  });
77
71
  };
78
72
 
73
+ const clearHeight = () => {
74
+ containerHeight.value = "";
75
+ containerOverflow.value = "visible";
76
+ };
77
+
79
78
  onMounted(() => {
80
79
  if (props.defaultActive) {
81
80
  setHeightSize();
@@ -90,26 +89,30 @@ onUnmounted(() => {
90
89
  </script>
91
90
 
92
91
  <style lang="scss" scoped>
93
- .collapse-header {
94
- display: flex;
95
- flex-direction: row;
96
- align-items: center;
97
- justify-content: space-between;
98
- cursor: pointer;
99
- user-select: none;
92
+ .collapse-container {
93
+ .collapse-header {
94
+ display: flex;
95
+ flex-direction: row;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ cursor: pointer;
99
+ user-select: none;
100
100
 
101
- > span {
102
- transition: all 0.2s linear;
101
+ > span {
102
+ transition: all 0.2s linear;
103
+ }
103
104
  }
104
105
 
105
- &.active > span {
106
- transform: rotate(180deg);
106
+ .content-container {
107
+ box-sizing: border-box;
108
+ overflow: hidden;
109
+ transition: all 0.2s linear;
107
110
  }
108
- }
109
111
 
110
- .content-container {
111
- box-sizing: border-box;
112
- overflow: hidden;
113
- transition: all 0.2s linear;
112
+ &.active {
113
+ .collapse-header > span {
114
+ transform: rotate(180deg);
115
+ }
116
+ }
114
117
  }
115
118
  </style>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <div
3
+ class="fixed-container"
4
+ :style="{ height: fixedContainerHeight }"
5
+ ref="fixedContainer"
6
+ >
7
+ <div
8
+ class="fixed-wrapper"
9
+ :class="{ 'position-fixed': positionFixed }"
10
+ :style="{ ...size, ...position }"
11
+ ref="fixedWrapper"
12
+ >
13
+ <slot></slot>
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script lang="ts" setup>
19
+ import { onUnmounted, onMounted } from "vue";
20
+ import { ref } from "vue";
21
+
22
+ const props = withDefaults(
23
+ defineProps<{
24
+ scrollThreshold?: number;
25
+ fixWidth?: boolean;
26
+ size?: {
27
+ height?: string;
28
+ width?: string;
29
+ };
30
+ position?: {
31
+ left?: string;
32
+ top?: string;
33
+ right?: string;
34
+ bottom?: string;
35
+ };
36
+ }>(),
37
+ {
38
+ scrollThreshold: 0,
39
+ fixWidth: true,
40
+ }
41
+ );
42
+
43
+ const positionFixed = ref(false);
44
+ const fixedContainer = ref();
45
+ const fixedContainerHeight = ref("");
46
+ const fixedWrapper = ref();
47
+
48
+ const fixPosition = () => {
49
+ if (fixedContainer.value) {
50
+ positionFixed.value =
51
+ window.scrollY > fixedContainer.value.offsetTop + props.scrollThreshold;
52
+
53
+ fixedContainerHeight.value = positionFixed.value
54
+ ? `${fixedWrapper.value.clientHeight}px`
55
+ : "";
56
+ }
57
+ };
58
+
59
+ const setWidth = () => {
60
+ fixedWrapper.value.style.maxWidth = `${fixedContainer.value.offsetWidth}px`;
61
+ };
62
+
63
+ onMounted(() => {
64
+ window.addEventListener("scroll", fixPosition);
65
+ fixPosition();
66
+
67
+ if (props.fixWidth) {
68
+ window.addEventListener("resize", setWidth);
69
+ setWidth();
70
+ }
71
+ });
72
+
73
+ onUnmounted(() => {
74
+ window.removeEventListener("scroll", fixPosition);
75
+ window.removeEventListener("resize", setWidth);
76
+ });
77
+
78
+ defineExpose({ positionFixed });
79
+ </script>
80
+
81
+ <style lang="scss" scoped>
82
+ .fixed-wrapper {
83
+ &.position-fixed {
84
+ position: fixed;
85
+ }
86
+ }
87
+ </style>