@v1nt1248/3nclient-lib 0.3.27 → 0.3.29

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 (115) hide show
  1. package/dist/chunks/ui3n-notification-Dr_3Td9N.js +279 -0
  2. package/dist/components/index.d.ts +12 -12
  3. package/dist/components/ui3n-autocomplete/types.d.ts +8 -0
  4. package/dist/components/ui3n-autocomplete/ui3n-autocomplete.d.ts +2 -2
  5. package/dist/components/ui3n-badge/types.d.ts +12 -0
  6. package/dist/components/ui3n-badge/ui3n-badge-simple.d.ts +1 -1
  7. package/dist/components/ui3n-badge/ui3n-badge.d.ts +1 -1
  8. package/dist/components/ui3n-breadcrumbs/types.d.ts +1 -1
  9. package/dist/components/ui3n-breadcrumbs/ui3n-breadcrumb.d.ts +2 -2
  10. package/dist/components/ui3n-button/types.d.ts +4 -2
  11. package/dist/components/ui3n-checkbox/types.d.ts +11 -0
  12. package/dist/components/ui3n-checkbox/ui3n-checkbox.d.ts +4 -1
  13. package/dist/components/ui3n-chip/types.d.ts +6 -1
  14. package/dist/components/ui3n-chip/ui3n-chip.d.ts +2 -0
  15. package/dist/components/ui3n-drop-files/types.d.ts +7 -0
  16. package/dist/components/ui3n-drop-files/ui3n-drop-files.d.ts +7 -1
  17. package/dist/components/ui3n-editable/types.d.ts +11 -0
  18. package/dist/components/ui3n-editable/ui3n-editable.d.ts +4 -2
  19. package/dist/components/ui3n-icon/types.d.ts +5 -1
  20. package/dist/components/ui3n-icon/ui3n-icon.d.ts +2 -2
  21. package/dist/components/ui3n-input/types.d.ts +4 -0
  22. package/dist/components/ui3n-input/ui3n-input.d.ts +4 -1
  23. package/dist/components/ui3n-input-file/types.d.ts +11 -0
  24. package/dist/components/ui3n-input-file/ui3n-input-file.d.ts +4 -1
  25. package/dist/components/ui3n-list/types.d.ts +4 -0
  26. package/dist/components/ui3n-menu/types.d.ts +4 -0
  27. package/dist/components/ui3n-menu/ui3n-menu.d.ts +1 -1
  28. package/dist/components/ui3n-notification/ui3n-notification.d.ts +2 -2
  29. package/dist/components/ui3n-progress/ui3n-progress-circular.d.ts +1 -1
  30. package/dist/components/ui3n-progress/ui3n-progress-linear.d.ts +1 -1
  31. package/dist/components/ui3n-radio-group/types.d.ts +18 -0
  32. package/dist/components/ui3n-radio-group/ui3n-radio-group.d.ts +3 -1
  33. package/dist/components/ui3n-radio-group/ui3n-radio.d.ts +6 -7
  34. package/dist/components/ui3n-scrollbar-horizontal/types.d.ts +1 -0
  35. package/dist/components/ui3n-scrollbar-horizontal/ui3n-scroolbar-horizontal.d.ts +1 -0
  36. package/dist/components/ui3n-scrollbar-vertical/types.d.ts +3 -0
  37. package/dist/components/ui3n-scrollbar-vertical/ui3n-scrollbar-vertical.d.ts +3 -0
  38. package/dist/components/ui3n-selector/types.d.ts +11 -0
  39. package/dist/components/ui3n-selector/ui3n-selector.d.ts +2 -2
  40. package/dist/components/ui3n-slider/types.d.ts +11 -0
  41. package/dist/components/ui3n-slider/ui3n-slider.d.ts +4 -1
  42. package/dist/components/ui3n-step-line-bar/types.d.ts +4 -0
  43. package/dist/components/ui3n-switch/types.d.ts +11 -0
  44. package/dist/components/ui3n-switch/ui3n-switch.d.ts +4 -1
  45. package/dist/components/ui3n-table/composables/useTable.d.ts +1 -0
  46. package/dist/components/ui3n-table/types.d.ts +4 -0
  47. package/dist/components/ui3n-table/ui3n-table-sort-icon.d.ts +4 -1
  48. package/dist/components/ui3n-tabs/types.d.ts +4 -0
  49. package/dist/components/ui3n-text/types.d.ts +11 -0
  50. package/dist/components/ui3n-text/ui3n-text.d.ts +4 -1
  51. package/dist/components/ui3n-tooltip/types.d.ts +4 -0
  52. package/dist/components/ui3n-tooltip/ui3n-tooltip.d.ts +4 -2
  53. package/dist/style.css +1 -1
  54. package/dist/ui3n-components.js +2083 -1697
  55. package/dist/ui3n-plugins.js +1 -1
  56. package/package.json +1 -1
  57. package/src/components/index.ts +29 -7
  58. package/src/components/ui3n-autocomplete/types.ts +9 -1
  59. package/src/components/ui3n-autocomplete/ui3n-autocomplete.vue +182 -51
  60. package/src/components/ui3n-badge/types.ts +10 -0
  61. package/src/components/ui3n-badge/ui3n-badge-simple.vue +54 -55
  62. package/src/components/ui3n-badge/ui3n-badge.vue +4 -0
  63. package/src/components/ui3n-breadcrumbs/types.ts +1 -1
  64. package/src/components/ui3n-breadcrumbs/ui3n-breadcrumb.vue +7 -5
  65. package/src/components/ui3n-button/types.ts +4 -2
  66. package/src/components/ui3n-button/ui3n-button.vue +24 -19
  67. package/src/components/ui3n-checkbox/types.ts +12 -0
  68. package/src/components/ui3n-checkbox/ui3n-checkbox.vue +79 -29
  69. package/src/components/ui3n-chip/types.ts +6 -1
  70. package/src/components/ui3n-chip/ui3n-chip.vue +64 -61
  71. package/src/components/ui3n-dialog/ui3n-dialog.vue +3 -3
  72. package/src/components/ui3n-drop-files/types.ts +8 -0
  73. package/src/components/ui3n-drop-files/ui3n-drop-files.vue +73 -29
  74. package/src/components/ui3n-editable/types.ts +12 -0
  75. package/src/components/ui3n-editable/ui3n-editable.vue +56 -24
  76. package/src/components/ui3n-icon/types.ts +5 -1
  77. package/src/components/ui3n-icon/ui3n-icon.vue +45 -50
  78. package/src/components/ui3n-input/types.ts +4 -0
  79. package/src/components/ui3n-input/ui3n-input.vue +14 -1
  80. package/src/components/ui3n-input-file/types.ts +12 -0
  81. package/src/components/ui3n-input-file/ui3n-input-file.vue +33 -16
  82. package/src/components/ui3n-list/types.ts +4 -0
  83. package/src/components/ui3n-list/ui3n-list.vue +3 -1
  84. package/src/components/ui3n-menu/types.ts +4 -0
  85. package/src/components/ui3n-menu/ui3n-menu.vue +43 -32
  86. package/src/components/ui3n-notification/ui3n-notification.vue +190 -175
  87. package/src/components/ui3n-progress/ui3n-progress-circular.vue +40 -22
  88. package/src/components/ui3n-progress/ui3n-progress-linear.vue +14 -7
  89. package/src/components/ui3n-radio-group/types.ts +20 -0
  90. package/src/components/ui3n-radio-group/ui3n-radio-group.vue +61 -48
  91. package/src/components/ui3n-radio-group/ui3n-radio.vue +60 -65
  92. package/src/components/ui3n-scrollbar-horizontal/types.ts +1 -0
  93. package/src/components/ui3n-scrollbar-horizontal/ui3n-scroolbar-horizontal.vue +8 -1
  94. package/src/components/ui3n-scrollbar-vertical/types.ts +3 -0
  95. package/src/components/ui3n-scrollbar-vertical/ui3n-scrollbar-vertical.vue +12 -5
  96. package/src/components/ui3n-selector/types.ts +12 -0
  97. package/src/components/ui3n-selector/ui3n-selector.vue +63 -40
  98. package/src/components/ui3n-slider/types.ts +12 -0
  99. package/src/components/ui3n-slider/ui3n-slider.vue +88 -39
  100. package/src/components/ui3n-step-line-bar/types.ts +4 -0
  101. package/src/components/ui3n-step-line-bar/ui3n-step-line-bar.vue +8 -10
  102. package/src/components/ui3n-switch/types.ts +12 -0
  103. package/src/components/ui3n-switch/ui3n-switch.vue +57 -25
  104. package/src/components/ui3n-table/composables/useTable.ts +17 -0
  105. package/src/components/ui3n-table/types.ts +4 -0
  106. package/src/components/ui3n-table/ui3n-table-sort-icon.vue +9 -8
  107. package/src/components/ui3n-table/ui3n-table.vue +48 -17
  108. package/src/components/ui3n-tabs/types.ts +4 -0
  109. package/src/components/ui3n-tabs/ui3n-tabs.vue +146 -108
  110. package/src/components/ui3n-text/types.ts +12 -0
  111. package/src/components/ui3n-text/ui3n-text.vue +56 -15
  112. package/src/components/ui3n-tooltip/types.ts +4 -0
  113. package/src/components/ui3n-tooltip/ui3n-tooltip.vue +61 -43
  114. package/src/components/ui3n-virtual-scroll/ui3n-virtual-scroll.vue +201 -17
  115. package/dist/chunks/ui3n-notification-DlDM-u-R.js +0 -276
@@ -1,9 +1,11 @@
1
+ <!-- @strictTemplates false -->
1
2
  <script lang="ts" setup>
2
3
  import { onMounted, ref, watch } from 'vue';
3
4
  import { patchTextareaMaxRowsSupport } from '@/utils';
4
- import type { Ui3nTextEmits, Ui3nTextProps } from './types';
5
+ import type { Ui3nTextEmits, Ui3nTextProps, Ui3nTextExpose } from './types';
5
6
 
6
7
  const props = withDefaults(defineProps<Ui3nTextProps>(), {
8
+ name: undefined,
7
9
  rows: 6,
8
10
  maxRows: 6,
9
11
  label: '',
@@ -42,9 +44,10 @@
42
44
  }
43
45
 
44
46
  function onBlur(event: Event) {
47
+ const value = (event.target as HTMLInputElement).value;
48
+
45
49
  setTimeout(() => {
46
50
  isFocused.value = false;
47
- const value = (event.target as HTMLInputElement).value;
48
51
  validate(value);
49
52
  emits('blur', event);
50
53
  emits('change', value);
@@ -82,6 +85,25 @@
82
85
  emits('keydown', event);
83
86
  }
84
87
 
88
+ function clear() {
89
+ text.value = '';
90
+ errorMessage.value = '';
91
+ isDirty.value = false;
92
+
93
+ if (textareaElement.value) {
94
+ textareaElement.value.value = '';
95
+ }
96
+
97
+ emits('update:modelValue', '');
98
+ emits('input', '');
99
+ emits('change', '');
100
+ emits('update:valid', true);
101
+ }
102
+
103
+ defineExpose<Ui3nTextExpose>({
104
+ clear,
105
+ });
106
+
85
107
  onMounted(() => {
86
108
  if (textareaElement.value) {
87
109
  patchTextareaMaxRowsSupport(textareaElement.value!);
@@ -93,6 +115,15 @@
93
115
  }
94
116
  });
95
117
 
118
+ watch(
119
+ () => props.name,
120
+ newName => {
121
+ if (!newName) {
122
+ clear();
123
+ }
124
+ },
125
+ );
126
+
96
127
  watch(
97
128
  () => props.modelValue,
98
129
  (val, oldVal) => {
@@ -106,7 +137,10 @@
106
137
  </script>
107
138
 
108
139
  <template>
109
- <div :class="[$style.ui3nText, disabled && $style.disabled, isFocused && $style.focused]">
140
+ <div
141
+ :id="id"
142
+ :class="[$style.ui3nText, disabled && $style.disabled, isFocused && $style.focused]"
143
+ >
110
144
  <label
111
145
  v-if="label"
112
146
  :class="$style.label"
@@ -119,6 +153,7 @@
119
153
  ref="textareaElement"
120
154
  :class="$style.content"
121
155
  :value="text"
156
+ :name="name"
122
157
  :placeholder="placeholder"
123
158
  :readonly="disabled"
124
159
  :rows="rows"
@@ -138,28 +173,34 @@
138
173
 
139
174
  <style lang="scss" module>
140
175
  .ui3nText {
176
+ --ui3n-text-border-radius: 4px;
177
+ --ui3n-text-label-font-size: 12px;
178
+ --ui3n-text-font-size: 13px;
179
+ --ui3n-text-padding-inline: 8px;
180
+ --ui3n-text-padding-block: 8px;
181
+
141
182
  position: relative;
142
183
  width: 100%;
143
- border-radius: var(--spacing-xs);
184
+ border-radius: var(--ui3n-text-border-radius);
144
185
  padding: 1px;
145
186
  }
146
187
 
147
188
  .label {
148
189
  display: block;
149
190
  width: 100%;
150
- font-size: var(--font-12);
151
- line-height: var(--font-16);
191
+ font-size: var(--ui3n-text-label-font-size);
192
+ line-height: 1.33;
152
193
  font-weight: 600;
153
194
  color: var(--color-text-control-primary-default);
154
- margin-bottom: var(--spacing-xs);
195
+ margin-bottom: 4px;
155
196
  }
156
197
 
157
198
  .body {
158
199
  position: relative;
159
200
  width: 100%;
160
- padding: var(--spacing-s) 0;
201
+ padding: var(--ui3n-text-padding-block) 0;
161
202
  background-color: var(--color-bg-control-secondary-default);
162
- border-radius: var(--spacing-xs);
203
+ border-radius: var(--ui3n-text-border-radius);
163
204
  transition: all 0.2s ease-in-out;
164
205
 
165
206
  &:hover {
@@ -184,12 +225,12 @@
184
225
  box-sizing: border-box;
185
226
  position: relative;
186
227
  width: 100%;
187
- border-radius: var(--spacing-xs);
228
+ border-radius: var(--ui3n-text-border-radius);
188
229
  background-color: var(--color-bg-control-secondary-default);
189
- padding: 0 var(--spacing-s);
230
+ padding: 0 var(--ui3n-text-padding-inline);
190
231
  font-family: Inter, system-ui, sans-serif;
191
- font-size: var(--font-13);
192
- line-height: var(--font-16);
232
+ font-size: var(--ui3n-text-font-size);
233
+ line-height: 1.23;
193
234
  font-weight: 400;
194
235
  color: var(--color-text-control-primary-default);
195
236
  transition: all 0.2s ease-in-out;
@@ -197,8 +238,8 @@
197
238
  &::placeholder {
198
239
  color: var(--color-text-control-secondary-default);
199
240
  font-style: italic;
200
- font-size: var(--font-13);
201
- line-height: var(--font-16);
241
+ font-size: var(--ui3n-text-font-size);
242
+ line-height: 1.23;
202
243
  font-weight: 400;
203
244
  }
204
245
 
@@ -21,6 +21,10 @@ export type Ui3nTooltipPlacement =
21
21
  * Tooltip component properties
22
22
  */
23
23
  export interface Ui3nTooltipProps {
24
+ /**
25
+ * Root element id
26
+ */
27
+ id?: string;
24
28
  /**
25
29
  * Whether tooltip is open
26
30
  */
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { computed, getCurrentInstance, onBeforeUnmount, onMounted, ref, watch } from 'vue';
2
+ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
3
3
  import type { Nullable } from '@/types';
4
4
  import type { Ui3nTooltipProps, Ui3nTooltipEmits, Ui3nTooltipSlots } from './types';
5
5
  import { arrow, autoUpdate, offset, useFloating } from '@floating-ui/vue';
@@ -20,23 +20,17 @@
20
20
  defineSlots<Ui3nTooltipSlots>();
21
21
 
22
22
  const showTooltip = ref(false);
23
- const referenceEl = ref<Nullable<HTMLElement>>(null);
23
+ const referenceContainer = ref<Nullable<HTMLElement>>(null);
24
24
  const floatingEl = ref<Nullable<HTMLElement>>(null);
25
25
  const floatingArrowEl = ref<Nullable<HTMLElement>>(null);
26
26
 
27
- const maxContentWidthCssValue = computed(() => `${props.maxContentWidth}px`);
28
- const baseOffsetCssValue = computed(() => `${-baseOffset}px`);
29
- const arrowSizeCssValue = computed(() => `${baseOffset}px`);
30
27
  const mainPlacement = computed(() => {
31
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
- const [part1, _] = props.placement.split('-');
28
+ const [part1] = props.placement.split('-');
33
29
  return part1;
34
30
  });
31
+
35
32
  const offsetOptions = computed(() => {
36
- const options = {
37
- mainAxis: 0,
38
- crossAxis: 0,
39
- };
33
+ const options = { mainAxis: 0, crossAxis: 0 };
40
34
  switch (mainPlacement.value) {
41
35
  case 'top':
42
36
  options.mainAxis = -1 * Number(props.offsetY) + baseOffset;
@@ -58,16 +52,32 @@
58
52
  return options;
59
53
  });
60
54
 
61
- const { floatingStyles, isPositioned, middlewareData } = useFloating(referenceEl, floatingEl, {
55
+ const middlewareComputed = computed(() => [
56
+ offset(offsetOptions.value),
57
+ arrow({ element: floatingArrowEl, padding: 8 }),
58
+ ]);
59
+
60
+ const { floatingStyles, isPositioned, middlewareData } = useFloating(referenceContainer, floatingEl, {
62
61
  open: showTooltip,
63
62
  placement: props.placement,
64
63
  strategy: props.positionStrategy,
65
- middleware: [offset(offsetOptions.value), arrow({ element: floatingArrowEl, padding: 8 })],
64
+ middleware: middlewareComputed,
66
65
  whileElementsMounted: props.positionStrategy === 'fixed' ? autoUpdate : undefined,
67
66
  });
68
67
 
68
+ const tooltipStylesComputed = computed(() => ({
69
+ ...floatingStyles.value,
70
+ '--ui3n-tooltip-bg-color': props.color,
71
+ '--ui3n-tooltip-text-color': props.textColor,
72
+ '--ui3n-tooltip-max-width': `${props.maxContentWidth}px`,
73
+ '--ui3n-tooltip-arrow-size': `${baseOffset}px`,
74
+ '--ui3n-tooltip-base-offset': `${-baseOffset}px`,
75
+ }));
76
+
69
77
  function onMouseenter() {
70
- if (props.disabled) return;
78
+ if (props.disabled) {
79
+ return;
80
+ }
71
81
 
72
82
  showTooltip.value = true;
73
83
  emits('open');
@@ -75,7 +85,9 @@
75
85
  }
76
86
 
77
87
  function onMouseleave() {
78
- if (props.disabled && !showTooltip.value) return;
88
+ if (props.disabled && !showTooltip.value) {
89
+ return;
90
+ }
79
91
 
80
92
  showTooltip.value = false;
81
93
  emits('close');
@@ -83,7 +95,9 @@
83
95
  }
84
96
 
85
97
  function onClick() {
86
- if (props.disabled && !showTooltip.value) return;
98
+ if (props.disabled && !showTooltip.value) {
99
+ return;
100
+ }
87
101
 
88
102
  if (showTooltip.value) {
89
103
  onMouseleave();
@@ -93,32 +107,28 @@
93
107
  }
94
108
 
95
109
  onMounted(() => {
96
- const inst = getCurrentInstance();
97
- const el = inst?.vnode?.el;
98
- if (!el) {
99
- throw new Error('[ui3n-tooltip] A reference element is absent.');
110
+ if (!referenceContainer.value) {
111
+ return;
100
112
  }
101
113
 
102
- referenceEl.value = el.nodeName === '#text' ? el.nextElementSibling : el;
103
-
104
- if (!referenceEl.value) return;
105
-
106
114
  if (props.trigger === 'hover') {
107
- referenceEl.value!.addEventListener('mouseenter', onMouseenter);
108
- referenceEl.value!.addEventListener('mouseleave', onMouseleave);
115
+ referenceContainer.value.addEventListener('mouseenter', onMouseenter);
116
+ referenceContainer.value.addEventListener('mouseleave', onMouseleave);
109
117
  } else if (props.trigger === 'click') {
110
- referenceEl.value!.addEventListener('click', onClick);
118
+ referenceContainer.value.addEventListener('click', onClick);
111
119
  }
112
120
  });
113
121
 
114
122
  onBeforeUnmount(() => {
115
- if (!referenceEl.value) return;
123
+ if (!referenceContainer.value) {
124
+ return;
125
+ }
116
126
 
117
127
  if (props.trigger === 'hover') {
118
- referenceEl.value!.removeEventListener('mouseenter', onMouseenter);
119
- referenceEl.value!.removeEventListener('mouseleave', onMouseleave);
128
+ referenceContainer.value.removeEventListener('mouseenter', onMouseenter);
129
+ referenceContainer.value.removeEventListener('mouseleave', onMouseleave);
120
130
  } else if (props.trigger === 'click') {
121
- referenceEl.value!.removeEventListener('click', onClick);
131
+ referenceContainer.value.removeEventListener('click', onClick);
122
132
  }
123
133
  });
124
134
 
@@ -134,13 +144,18 @@
134
144
  </script>
135
145
 
136
146
  <template>
137
- <slot />
147
+ <div
148
+ ref="referenceContainer"
149
+ :class="$style.container"
150
+ >
151
+ <slot />
152
+ </div>
138
153
 
139
154
  <div
140
155
  v-if="showTooltip"
141
156
  ref="floatingEl"
142
157
  :class="$style.floating"
143
- :style="floatingStyles"
158
+ :style="tooltipStylesComputed"
144
159
  >
145
160
  <slot name="content">
146
161
  <div :class="$style.content">
@@ -159,10 +174,13 @@
159
174
  </template>
160
175
 
161
176
  <style lang="scss" module>
162
- .floating {
163
- --ui3n-tooltip-bg-color: v-bind(color);
164
- --ui3n-tooltip-text-color: v-bind(textColor);
177
+ .container {
178
+ display: inline-block;
179
+ width: max-content;
180
+ max-width: 100%;
181
+ }
165
182
 
183
+ .floating {
166
184
  width: max-content;
167
185
  z-index: 5;
168
186
  }
@@ -175,41 +193,41 @@
175
193
  &-top,
176
194
  &-bottom {
177
195
  border-style: solid;
178
- border-width: 0 v-bind(arrowSizeCssValue) v-bind(arrowSizeCssValue) v-bind(arrowSizeCssValue);
196
+ border-width: 0 var(--ui3n-tooltip-arrow-size) var(--ui3n-tooltip-arrow-size) var(--ui3n-tooltip-arrow-size);
179
197
  border-color: transparent transparent var(--ui3n-tooltip-bg-color) transparent;
180
198
  }
181
199
 
182
200
  &-top {
183
- bottom: v-bind(baseOffsetCssValue);
201
+ bottom: var(--ui3n-tooltip-base-offset);
184
202
  transform: rotate(180deg);
185
203
  }
186
204
 
187
205
  &-bottom {
188
- top: v-bind(baseOffsetCssValue);
206
+ top: var(--ui3n-tooltip-base-offset);
189
207
  transform: rotate(0deg);
190
208
  }
191
209
 
192
210
  &-left,
193
211
  &-right {
194
212
  border-style: solid;
195
- border-width: v-bind(arrowSizeCssValue) 0 v-bind(arrowSizeCssValue) v-bind(arrowSizeCssValue);
213
+ border-width: var(--ui3n-tooltip-arrow-size) 0 var(--ui3n-tooltip-arrow-size) var(--ui3n-tooltip-arrow-size);
196
214
  border-color: transparent transparent transparent var(--ui3n-tooltip-bg-color);
197
215
  }
198
216
 
199
217
  &-left {
200
- right: v-bind(baseOffsetCssValue);
218
+ right: var(--ui3n-tooltip-base-offset);
201
219
  transform: rotate(0deg);
202
220
  }
203
221
 
204
222
  &-right {
205
- left: v-bind(baseOffsetCssValue);
223
+ left: var(--ui3n-tooltip-base-offset);
206
224
  transform: rotate(180deg);
207
225
  }
208
226
  }
209
227
 
210
228
  .content {
211
229
  position: relative;
212
- max-width: v-bind(maxContentWidthCssValue);
230
+ max-width: var(--ui3n-tooltip-max-width);
213
231
  padding: 6px var(--spacing-s);
214
232
  border-radius: 6px;
215
233
  font-size: var(--font-11);
@@ -1,47 +1,228 @@
1
1
  <script lang="ts" setup generic="T extends { id?: string }">
2
- import { computed, ref } from 'vue';
3
- import Ui3nScrollbarVertical from '@/components/ui3n-scrollbar-vertical/ui3n-scrollbar-vertical.vue';
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { computed, ref, shallowRef, onMounted, onBeforeUnmount, watch } from 'vue';
4
+ import Ui3nScrollbarVertical from '../ui3n-scrollbar-vertical/ui3n-scrollbar-vertical.vue';
4
5
  import type { Ui3nVirtualScrollProps, Ui3nVirtualScrollSlots } from './types';
5
6
 
7
+ interface MeasuredSize {
8
+ height: number;
9
+ top: number;
10
+ }
11
+
6
12
  const props = withDefaults(defineProps<Ui3nVirtualScrollProps<T>>(), {
7
13
  renderAhead: 20,
14
+ minChildHeight: 16,
8
15
  });
9
16
  defineSlots<Ui3nVirtualScrollSlots<T>>();
10
17
 
11
18
  const scrollTop = ref(0);
19
+ const viewportHeight = ref<number | null>(null);
12
20
  const scrollbarComponentRef = ref<InstanceType<typeof Ui3nScrollbarVertical> | null>(null);
13
21
 
14
- const itemCount = computed(() => props.items.length);
15
- const totalHeight = computed(() => itemCount.value * props.minChildHeight);
22
+ const sizeCache = shallowRef<MeasuredSize[]>([]);
23
+ const cacheVersion = ref(0);
24
+
25
+ let isAdjustingScroll = false;
26
+ let listResizeObserver: ResizeObserver | null = null;
27
+ let containerResizeObserver: ResizeObserver | null = null;
28
+
29
+ const itemRefs = new Map<string | number, HTMLElement>();
30
+
31
+ const totalHeight = computed(() => {
32
+ cacheVersion.value;
33
+ if (!sizeCache.value.length) {
34
+ return 0;
35
+ }
36
+
37
+ const lastItem = sizeCache.value[sizeCache.value.length - 1];
38
+ return lastItem.top + lastItem.height;
39
+ });
16
40
 
17
41
  const wrapElementHeight = computed(() => {
42
+ if (viewportHeight.value !== null) {
43
+ return viewportHeight.value;
44
+ }
45
+
18
46
  const el = scrollbarComponentRef.value?.getContainer();
19
47
  return el ? el.clientHeight : 0;
20
48
  });
21
49
 
22
50
  const startNode = computed(() => {
23
- const val = Math.floor(scrollTop.value / props.minChildHeight) - props.renderAhead;
24
- return Math.max(0, val);
51
+ cacheVersion.value;
52
+ const targetTop = scrollTop.value;
53
+
54
+ if (!sizeCache.value.length) {
55
+ return 0;
56
+ }
57
+
58
+ let low = 0;
59
+ let high = sizeCache.value.length - 1;
60
+ let foundIndex = -1;
61
+
62
+ while (low <= high) {
63
+ const mid = Math.floor((low + high) / 2);
64
+ const item = sizeCache.value[mid];
65
+
66
+ if (item.top <= targetTop && item.top + item.height > targetTop) {
67
+ foundIndex = mid;
68
+ break;
69
+ } else if (item.top > targetTop) {
70
+ high = mid - 1;
71
+ } else {
72
+ low = mid + 1;
73
+ }
74
+ }
75
+
76
+ if (foundIndex === -1) {
77
+ foundIndex = Math.max(0, high);
78
+ }
79
+
80
+ return Math.max(0, foundIndex - props.renderAhead);
25
81
  });
26
82
 
27
83
  const visibleNodeCount = computed(() => {
28
- return Math.ceil(wrapElementHeight.value / props.minChildHeight) + 2 * props.renderAhead;
84
+ const height = wrapElementHeight.value;
85
+ if (!height) {
86
+ return props.renderAhead * 2;
87
+ }
88
+
89
+ const estimatedVisible = Math.ceil(height / props.minChildHeight);
90
+ return estimatedVisible + 2 * props.renderAhead;
91
+ });
92
+
93
+ const offsetY = computed(() => {
94
+ cacheVersion.value;
95
+ if (!sizeCache.value.length) {
96
+ return 0;
97
+ }
98
+
99
+ return sizeCache.value[startNode.value]?.top || 0;
29
100
  });
30
101
 
31
- const offsetY = computed(() => startNode.value * props.minChildHeight);
32
- const visibleChildren = computed(() =>
33
- props.items.slice(startNode.value, startNode.value + visibleNodeCount.value),
34
- );
102
+ const visibleChildren = computed(() => {
103
+ const start = startNode.value;
104
+ const count = visibleNodeCount.value;
105
+
106
+ if (start + count >= props.items.length - props.renderAhead) {
107
+ return props.items.slice(start);
108
+ }
109
+
110
+ return props.items.slice(start, start + count);
111
+ });
112
+
113
+ function onScroll(event: Event) {
114
+ if (isAdjustingScroll) {
115
+ return;
116
+ }
35
117
 
36
- const onScroll = (event: Event) => {
37
118
  scrollTop.value = (event.target as HTMLDivElement).scrollTop;
38
- };
119
+ }
120
+
121
+ function setItemRef(el: any, id: string | number) {
122
+ if (el) {
123
+ const element = el.$el || el;
124
+ itemRefs.set(id, element);
125
+ listResizeObserver?.observe(element);
126
+ } else {
127
+ itemRefs.delete(id);
128
+ }
129
+ }
130
+
131
+ function initCache() {
132
+ let currentTop = 0;
133
+ sizeCache.value = props.items.map(() => {
134
+ const size = { height: props.minChildHeight, top: currentTop };
135
+ currentTop += props.minChildHeight;
136
+ return size;
137
+ });
138
+ cacheVersion.value++;
139
+ }
140
+
141
+ onMounted(() => {
142
+ const containerEl = scrollbarComponentRef.value?.getContainer();
143
+
144
+ if (containerEl) {
145
+ viewportHeight.value = containerEl.clientHeight;
146
+ containerResizeObserver = new ResizeObserver(entries => {
147
+ for (const entry of entries) {
148
+ viewportHeight.value = entry.contentRect.height || entry.target.clientHeight;
149
+ }
150
+ });
151
+ containerResizeObserver.observe(containerEl);
152
+ }
153
+
154
+ listResizeObserver = new ResizeObserver(entries => {
155
+ requestAnimationFrame(() => {
156
+ let lowestChangedIndex = Infinity;
157
+ let totalDeltaAboveKey = 0;
158
+
159
+ const updatedCache = [...sizeCache.value];
160
+ const currentScrollTop = scrollTop.value;
161
+
162
+ for (const entry of entries) {
163
+ const sid = entry.target.getAttribute('data-sid');
164
+ if (!sid) {
165
+ continue;
166
+ }
167
+
168
+ const index = props.items.findIndex((item, idx) => (item.id ?? idx).toString() === sid);
169
+ if (index === -1) {
170
+ continue;
171
+ }
172
+
173
+ const realHeight = entry.contentRect.height || entry.target.getBoundingClientRect().height;
174
+
175
+ if (updatedCache[index] && updatedCache[index].height !== realHeight) {
176
+ const oldHeight = updatedCache[index].height;
177
+ updatedCache[index] = { ...updatedCache[index], height: realHeight };
178
+
179
+ if (updatedCache[index].top < currentScrollTop) {
180
+ totalDeltaAboveKey += realHeight - oldHeight;
181
+ }
182
+
183
+ if (index < lowestChangedIndex) {
184
+ lowestChangedIndex = index;
185
+ }
186
+ }
187
+ }
188
+
189
+ if (lowestChangedIndex !== Infinity) {
190
+ let currentTop = updatedCache[lowestChangedIndex].top;
191
+
192
+ for (let i = lowestChangedIndex; i < updatedCache.length; i++) {
193
+ updatedCache[i].top = currentTop;
194
+ currentTop += updatedCache[i].height;
195
+ }
196
+
197
+ sizeCache.value = updatedCache;
198
+ cacheVersion.value++;
199
+
200
+ if (containerEl && totalDeltaAboveKey !== 0) {
201
+ isAdjustingScroll = true;
202
+ containerEl.scrollTop = currentScrollTop + totalDeltaAboveKey;
203
+ scrollTop.value = containerEl.scrollTop;
204
+ isAdjustingScroll = false;
205
+ }
206
+
207
+ scrollbarComponentRef.value?.updateMetrics();
208
+ }
209
+ });
210
+ });
211
+ });
212
+
213
+ onBeforeUnmount(() => {
214
+ listResizeObserver?.disconnect();
215
+ containerResizeObserver?.disconnect();
216
+ });
217
+
218
+ watch(() => props.items, initCache, { immediate: true });
39
219
  </script>
40
220
 
41
221
  <template>
42
222
  <ui3n-scrollbar-vertical
43
223
  ref="scrollbarComponentRef"
44
224
  :class="$style.ui3nVirtualScroll"
225
+ :auto-update="false"
45
226
  @scroll="onScroll"
46
227
  >
47
228
  <div
@@ -55,7 +236,8 @@
55
236
  <div
56
237
  v-for="(item, index) in visibleChildren"
57
238
  :key="item.id"
58
- :data-sid="item.id ?? index"
239
+ :ref="el => setItemRef(el, item.id ?? startNode + index)"
240
+ v-bind="{ 'data-sid': item.id ?? startNode + index }"
59
241
  :class="$style.item"
60
242
  >
61
243
  <slot
@@ -71,8 +253,6 @@
71
253
 
72
254
  <style lang="scss" module>
73
255
  .ui3nVirtualScroll {
74
- --ui3n-virtual-scroll-item-min-height: 16px;
75
-
76
256
  background-color: var(--color-bg-block-primary-default);
77
257
  }
78
258
 
@@ -80,6 +260,9 @@
80
260
  position: relative;
81
261
  overflow: hidden;
82
262
  will-change: transform;
263
+ box-sizing: border-box;
264
+ margin: 0 !important;
265
+ padding: 0 !important;
83
266
  }
84
267
 
85
268
  .content {
@@ -88,6 +271,7 @@
88
271
 
89
272
  .item {
90
273
  position: relative;
91
- min-height: var(--ui3n-virtual-scroll-item-min-height);
274
+ min-height: v-bind('props.minChildHeight + "px"');
275
+ box-sizing: border-box;
92
276
  }
93
277
  </style>