itube-specs 0.0.968 → 0.0.969
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.
|
@@ -89,8 +89,10 @@ defineProps<{
|
|
|
89
89
|
inset: 0;
|
|
90
90
|
overflow-y: auto;
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
/* Контейнер прокрутки обрезает всё, что элементы рисуют ЗА своей границей, —
|
|
93
|
+
кольцо аватара модели и рамки фокуса. Отступ в 2px даёт им место; справа
|
|
94
|
+
к нему добавлено расстояние до полосы прокрутки. */
|
|
95
|
+
padding: 2px 6px 2px 2px;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
</style>
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
{'--center': center},
|
|
9
9
|
{'--open': open},
|
|
10
10
|
{'--separate': separateLastChild},
|
|
11
|
+
{'--escaped': escaped},
|
|
11
12
|
]"
|
|
12
13
|
class="ui-dropdown"
|
|
13
14
|
@mouseleave="mouseHandler(false)"
|
|
@@ -26,7 +27,10 @@
|
|
|
26
27
|
<dialog
|
|
27
28
|
v-if="useVIf ? open && $slots.items : true"
|
|
28
29
|
ref="menuRef"
|
|
29
|
-
:style="
|
|
30
|
+
:style="[
|
|
31
|
+
!useVIf ? { display: (open && $slots.items) ? '' : 'none' } : {},
|
|
32
|
+
escapedStyle,
|
|
33
|
+
]"
|
|
30
34
|
:class="{'--open': open}"
|
|
31
35
|
class="ui-dropdown__menu"
|
|
32
36
|
@click="close"
|
|
@@ -92,9 +96,75 @@ const { registerOverlay, releaseOverlay } = useOverlayStack();
|
|
|
92
96
|
const breakpoints = CSS_BREAKPOINTS;
|
|
93
97
|
const { isTablet } = checkDeviceWidth(breakpoints);
|
|
94
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Выпадашка внутри прокручиваемого блока (боковая колонка плеера, например)
|
|
101
|
+
* обрезается его `overflow` — меню уходит под край. Уводить его в `position: fixed`
|
|
102
|
+
* всегда не стали: у обычных выпадашек координаты считает браузер, и это надёжнее.
|
|
103
|
+
* Поэтому смотрим, есть ли над нами прокручиваемый предок, и только тогда считаем
|
|
104
|
+
* координаты сами. `overflow: hidden` не в счёт — там выпадашки и так живут.
|
|
105
|
+
*/
|
|
106
|
+
function hasScrollableAncestor(element: HTMLElement | null): boolean {
|
|
107
|
+
let node = element?.parentElement || null;
|
|
108
|
+
|
|
109
|
+
while (node && node !== document.body) {
|
|
110
|
+
const { overflowX, overflowY } = getComputedStyle(node);
|
|
111
|
+
|
|
112
|
+
if (/auto|scroll/.test(`${overflowX} ${overflowY}`)) return true;
|
|
113
|
+
|
|
114
|
+
node = node.parentElement;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Меню выведено из потока и позиционируется от экрана. */
|
|
121
|
+
const escaped = ref(false);
|
|
122
|
+
const escapedStyle = ref<Record<string, string>>({});
|
|
123
|
+
|
|
124
|
+
/** Координаты меню от триггера: вниз, а если не влезает — вверх; прижимаем к экрану. */
|
|
125
|
+
function placeEscaped() {
|
|
126
|
+
const trigger = dropdownRef.value?.getBoundingClientRect();
|
|
127
|
+
const menu = menuRef.value;
|
|
128
|
+
if (!trigger || !menu) return;
|
|
129
|
+
|
|
130
|
+
const gap = 4;
|
|
131
|
+
const edge = 8;
|
|
132
|
+
const { offsetWidth: width, offsetHeight: height } = menu;
|
|
133
|
+
|
|
134
|
+
const below = trigger.bottom + gap;
|
|
135
|
+
const flip = below + height > window.innerHeight;
|
|
136
|
+
autoTop.value = flip;
|
|
137
|
+
|
|
138
|
+
let left = props.right ? trigger.right - width : trigger.left;
|
|
139
|
+
if (props.center) left = trigger.left + (trigger.width - width) / 2;
|
|
140
|
+
|
|
141
|
+
escapedStyle.value = {
|
|
142
|
+
top: `${Math.max(edge, flip ? trigger.top - height - gap : below)}px`,
|
|
143
|
+
left: `${Math.min(Math.max(edge, left), window.innerWidth - width - edge)}px`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Прокрутка любого предка сдвигает триггер — пересчитываем, пока меню открыто. */
|
|
148
|
+
function onViewportChange() {
|
|
149
|
+
requestAnimationFrame(placeEscaped);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function bindViewportListeners(bind: boolean) {
|
|
153
|
+
const method = bind ? 'addEventListener' : 'removeEventListener';
|
|
154
|
+
|
|
155
|
+
window[method]('scroll', onViewportChange, true);
|
|
156
|
+
window[method]('resize', onViewportChange);
|
|
157
|
+
}
|
|
158
|
+
|
|
95
159
|
function checkPosition() {
|
|
96
160
|
if (!dropdownRef.value || !menuRef.value || isTablet.value) return;
|
|
97
161
|
requestAnimationFrame(() => {
|
|
162
|
+
if (escaped.value) {
|
|
163
|
+
placeEscaped();
|
|
164
|
+
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
98
168
|
const triggerRect = dropdownRef.value.getBoundingClientRect();
|
|
99
169
|
const menuHeight = menuRef.value.scrollHeight;
|
|
100
170
|
const spaceBelow = window.innerHeight - triggerRect.bottom;
|
|
@@ -104,6 +174,12 @@ function checkPosition() {
|
|
|
104
174
|
|
|
105
175
|
function openDropdown() {
|
|
106
176
|
open.value = !open.value;
|
|
177
|
+
|
|
178
|
+
if (open.value && !isTablet.value) {
|
|
179
|
+
escaped.value = hasScrollableAncestor(dropdownRef.value);
|
|
180
|
+
if (escaped.value) bindViewportListeners(true);
|
|
181
|
+
}
|
|
182
|
+
|
|
107
183
|
if (open.value) nextTick(() => checkPosition());
|
|
108
184
|
|
|
109
185
|
if (isTablet.value && open.value) {
|
|
@@ -117,6 +193,8 @@ function openDropdown() {
|
|
|
117
193
|
|
|
118
194
|
function close() {
|
|
119
195
|
open.value = false;
|
|
196
|
+
bindViewportListeners(false);
|
|
197
|
+
escapedStyle.value = {};
|
|
120
198
|
|
|
121
199
|
if (isTablet.value) {
|
|
122
200
|
nextTick(() => {
|
|
@@ -139,6 +217,8 @@ watch(open, (val) => {
|
|
|
139
217
|
});
|
|
140
218
|
|
|
141
219
|
onBeforeUnmount(() => {
|
|
220
|
+
bindViewportListeners(false);
|
|
221
|
+
|
|
142
222
|
if (open.value && isTablet.value) {
|
|
143
223
|
scrollUnlock();
|
|
144
224
|
releaseOverlay(menuRef.value);
|
|
@@ -278,6 +358,17 @@ defineExpose({ openDropdown, open });
|
|
|
278
358
|
}
|
|
279
359
|
}
|
|
280
360
|
|
|
361
|
+
/* Меню внутри прокручиваемого блока: позиционируется от экрана, координаты считает
|
|
362
|
+
скрипт (см. placeEscaped) — браузерные `bottom`/`transform` здесь только мешают. */
|
|
363
|
+
.ui-dropdown.--escaped .ui-dropdown__menu {
|
|
364
|
+
@include from-br(md) {
|
|
365
|
+
position: fixed;
|
|
366
|
+
right: auto;
|
|
367
|
+
bottom: auto;
|
|
368
|
+
transform: none;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
281
372
|
.ui-dropdown__menu-items > .ui-dropdown__item {
|
|
282
373
|
@include font-md;
|
|
283
374
|
|