@neatui/vue 4.5.0 → 4.7.0
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.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neatui/vue",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "basic component ui",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@fekit/floor": "^3.0.1",
|
|
30
|
-
"@fekit/follow": "^1.
|
|
30
|
+
"@fekit/follow": "^1.8.0",
|
|
31
31
|
"@fekit/itable": "^1.1.6",
|
|
32
32
|
"@fekit/pullload": "^1.3.5",
|
|
33
33
|
"@fekit/scale": "^2.0.1",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
clickTipHide?: boolean;
|
|
57
57
|
mobile?: boolean;
|
|
58
58
|
title?: string;
|
|
59
|
+
zIndex?: string | number | null;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// 入参
|
|
@@ -73,6 +74,7 @@
|
|
|
73
74
|
clickTipHide: false,
|
|
74
75
|
mobile: true,
|
|
75
76
|
title: '',
|
|
77
|
+
zIndex: undefined,
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
const dom: any = ref(null);
|
|
@@ -214,13 +216,20 @@
|
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
},
|
|
217
|
-
{ hideOnScrollOut: false },
|
|
219
|
+
{ hideOnScrollOut: false, zIndex: props.zIndex },
|
|
218
220
|
);
|
|
219
221
|
} else {
|
|
220
222
|
ex.value = null;
|
|
221
223
|
}
|
|
222
224
|
});
|
|
223
225
|
|
|
226
|
+
watch(
|
|
227
|
+
() => props.zIndex,
|
|
228
|
+
(zIndex) => {
|
|
229
|
+
ex.value?.setOptions({ zIndex });
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
224
233
|
onUnmounted(() => {
|
|
225
234
|
document.removeEventListener('click', cancel, true);
|
|
226
235
|
// 移除触摸事件监听器
|
|
@@ -123,8 +123,9 @@
|
|
|
123
123
|
border-radius: 0.5em 0.5em 0 0;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
|
|
126
|
+
// Keep picker keyframes off the shared am-show-am / am-hide-am names
|
|
127
|
+
// so Layer's am="am" preset is not overwritten.
|
|
128
|
+
@keyframes am-picker-show {
|
|
128
129
|
0% {
|
|
129
130
|
opacity: 0;
|
|
130
131
|
transform: translate3d(0, 100%, 0) scale3d(1, 1, 1);
|
|
@@ -135,8 +136,7 @@
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
@keyframes am-hide-am {
|
|
139
|
+
@keyframes am-picker-hide {
|
|
140
140
|
0% {
|
|
141
141
|
opacity: 1;
|
|
142
142
|
transform: translate3d(0, 0%, 0) scale3d(1, 1, 1);
|
|
@@ -147,39 +147,34 @@
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
transform: translate3d(0, -150%, 0);
|
|
150
|
+
%am-picker-show {
|
|
151
|
+
transform: translate3d(0, 100%, 0);
|
|
153
152
|
opacity: 0;
|
|
154
153
|
transform-style: preserve-3d;
|
|
155
154
|
}
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
%am-hide-am {
|
|
156
|
+
%am-picker-hide {
|
|
159
157
|
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
|
160
|
-
opacity:
|
|
158
|
+
opacity: 1;
|
|
161
159
|
transform-style: preserve-3d;
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
// 传参调用
|
|
165
162
|
@mixin am-view-am($time: 450ms, $delay: 100ms) {
|
|
166
163
|
&.show,
|
|
167
164
|
&[view='2'] {
|
|
168
165
|
& > * {
|
|
169
|
-
@extend %am-show
|
|
170
|
-
animation: am-show
|
|
166
|
+
@extend %am-picker-show;
|
|
167
|
+
animation: am-picker-show $time $delay both;
|
|
171
168
|
}
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
&.hide,
|
|
175
172
|
&[view='1'] {
|
|
176
|
-
opacity: 0;
|
|
177
|
-
transition: ($time + $delay) * (4 * 0.1) ($time + $delay) * (6 * 0.1);
|
|
178
173
|
pointer-events: none;
|
|
179
174
|
|
|
180
175
|
& > * {
|
|
181
|
-
@extend %am-hide
|
|
182
|
-
animation: am-hide
|
|
176
|
+
@extend %am-picker-hide;
|
|
177
|
+
animation: am-picker-hide $time $delay both;
|
|
183
178
|
}
|
|
184
179
|
}
|
|
185
180
|
|
|
@@ -189,36 +184,11 @@
|
|
|
189
184
|
}
|
|
190
185
|
}
|
|
191
186
|
|
|
192
|
-
// 动画时间
|
|
193
187
|
$time: 400ms;
|
|
194
188
|
$delay: 100ms;
|
|
195
189
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
&.show,
|
|
199
|
-
&[view='2'] {
|
|
200
|
-
& > * {
|
|
201
|
-
@extend %am-show-am;
|
|
202
|
-
animation: am-show-am $time $delay both;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
&.hide,
|
|
207
|
-
&[view='1'] {
|
|
208
|
-
opacity: 0;
|
|
209
|
-
transition: ($time + $delay) * 0.4 ($time + $delay) * 0.6;
|
|
210
|
-
pointer-events: none;
|
|
211
|
-
|
|
212
|
-
& > * {
|
|
213
|
-
@extend %am-hide-am;
|
|
214
|
-
animation: am-hide-am $time $delay ease-in-out both;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
&.none,
|
|
219
|
-
&[view='0'] {
|
|
220
|
-
display: none;
|
|
221
|
-
}
|
|
190
|
+
.fekit-picker[am-view='am'] {
|
|
191
|
+
@include am-view-am($time, $delay);
|
|
222
192
|
}
|
|
223
193
|
|
|
224
194
|
@keyframes am-dark-show {
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
v-if="state.view"
|
|
7
7
|
:am-view="am"
|
|
8
8
|
:data-key="id"
|
|
9
|
-
:
|
|
9
|
+
:data-recess="state.recess || undefined"
|
|
10
|
+
:data-cover-dark="state.coverDark !== '' ? '' : undefined"
|
|
11
|
+
:style="layerStyle"
|
|
10
12
|
:view="state.view"
|
|
11
13
|
class="fekit-layer"
|
|
12
14
|
>
|
|
13
15
|
<div :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @click="_sys_hide">
|
|
16
|
+
<div v-if="state.coverDark !== ''" class="fekit-layer-cover-dark"></div>
|
|
14
17
|
<slot :param="_tasks[id]?.params"></slot>
|
|
15
18
|
</div>
|
|
16
19
|
</div>
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
</div>
|
|
19
22
|
</template>
|
|
20
23
|
<script lang="ts" setup>
|
|
21
|
-
import { onBeforeUnmount, reactive, watch } from 'vue';
|
|
24
|
+
import { computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue';
|
|
22
25
|
import { env, isFunction } from '@fekit/utils';
|
|
23
26
|
|
|
24
27
|
// 弹层队列
|
|
@@ -60,6 +63,21 @@
|
|
|
60
63
|
const SCROLL_LOCK_STATE = Symbol.for('@neatui/vue.Layer.scrollLock');
|
|
61
64
|
const ESCAPE_STACK = Symbol.for('@neatui/vue.Layer.escapeStack');
|
|
62
65
|
const ESCAPE_LISTENER = Symbol.for('@neatui/vue.Layer.escapeListener');
|
|
66
|
+
const AREA_GROUPS = Symbol.for('@neatui/vue.Layer.areaGroups');
|
|
67
|
+
|
|
68
|
+
type LayerMode = 'cover' | 'cascade' | 'displace' | 'replace' | 'queue';
|
|
69
|
+
|
|
70
|
+
type AreaGroup = {
|
|
71
|
+
dark: {
|
|
72
|
+
afterHide?: () => void;
|
|
73
|
+
el: HTMLElement | null;
|
|
74
|
+
listening: boolean;
|
|
75
|
+
owners: Set<string>;
|
|
76
|
+
view: number;
|
|
77
|
+
};
|
|
78
|
+
queue: Array<{ id: string; kind: 'open' | 'show'; params: any; then: any }>;
|
|
79
|
+
stack: string[];
|
|
80
|
+
};
|
|
63
81
|
|
|
64
82
|
const getScrollLockState = (): ScrollLockState => {
|
|
65
83
|
if (!_tasks[SCROLL_LOCK_STATE]) {
|
|
@@ -119,6 +137,199 @@
|
|
|
119
137
|
});
|
|
120
138
|
};
|
|
121
139
|
|
|
140
|
+
const getAreaGroups = (): Record<string, AreaGroup> => {
|
|
141
|
+
if (!_tasks[AREA_GROUPS]) {
|
|
142
|
+
Object.defineProperty(_tasks, AREA_GROUPS, {
|
|
143
|
+
configurable: true,
|
|
144
|
+
value: {},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return _tasks[AREA_GROUPS];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const getAreaGroup = (area: string): AreaGroup => {
|
|
151
|
+
const groups = getAreaGroups();
|
|
152
|
+
if (!groups[area]) {
|
|
153
|
+
groups[area] = {
|
|
154
|
+
dark: {
|
|
155
|
+
el: null,
|
|
156
|
+
listening: false,
|
|
157
|
+
owners: new Set<string>(),
|
|
158
|
+
view: 0,
|
|
159
|
+
},
|
|
160
|
+
queue: [],
|
|
161
|
+
stack: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return groups[area];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const resolveDarkOpacity = (value: string | number | undefined) => {
|
|
168
|
+
return `${value ?? ''}` === '' ? '0.75' : `${value}`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const participatesInGroup = (dark: string | number | undefined) => `${dark ?? ''}` !== '0';
|
|
172
|
+
const DEFAULT_COVER_DARK = 0.32;
|
|
173
|
+
|
|
174
|
+
const resolveCoverDark = (mode: LayerMode, hasPrev: boolean, coverDark: unknown) => {
|
|
175
|
+
if (mode !== 'cover' || !hasPrev) return '';
|
|
176
|
+
if (coverDark === false || `${coverDark}` === '0') return '';
|
|
177
|
+
if (coverDark === undefined || coverDark === true || `${coverDark}` === '') return DEFAULT_COVER_DARK;
|
|
178
|
+
return coverDark as string | number;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const isBlockingView = (view: number) => view === 1 || view === 2 || view === 3;
|
|
182
|
+
|
|
183
|
+
const occupiesGlobalDark = (task: any) => {
|
|
184
|
+
if (!task?.participates) return false;
|
|
185
|
+
if (task.view === 2 || task.view === 3 || task.view === 4) return true;
|
|
186
|
+
return task.view === 1 && !!task.parkAfterHide;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const setGlobalDarkView = (group: AreaGroup, view: number) => {
|
|
190
|
+
group.dark.view = view;
|
|
191
|
+
if (group.dark.el) group.dark.el.setAttribute('view', `${view}`);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const ensureGlobalDarkEl = (area: string, group: AreaGroup) => {
|
|
195
|
+
if (!env.client) return null;
|
|
196
|
+
if (group.dark.el?.isConnected) return group.dark.el;
|
|
197
|
+
group.dark.el = null;
|
|
198
|
+
group.dark.listening = false;
|
|
199
|
+
const container = document.querySelector(area);
|
|
200
|
+
if (!container) return null;
|
|
201
|
+
let el = container.querySelector(':scope > .fekit-layer-dark') as HTMLElement | null;
|
|
202
|
+
if (!el) {
|
|
203
|
+
el = document.createElement('div');
|
|
204
|
+
el.className = 'fekit-layer-dark';
|
|
205
|
+
el.setAttribute('view', '0');
|
|
206
|
+
container.insertBefore(el, container.firstChild);
|
|
207
|
+
}
|
|
208
|
+
group.dark.el = el;
|
|
209
|
+
el.addEventListener('animationend', (ev) => {
|
|
210
|
+
if (ev.target !== el) return;
|
|
211
|
+
if (group.dark.view === 2) setGlobalDarkView(group, 3);
|
|
212
|
+
if (group.dark.view === 1) {
|
|
213
|
+
setGlobalDarkView(group, 0);
|
|
214
|
+
const afterHide = group.dark.afterHide;
|
|
215
|
+
group.dark.afterHide = undefined;
|
|
216
|
+
afterHide?.();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
group.dark.listening = true;
|
|
220
|
+
return el;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const syncGlobalDarkOpacity = (area: string, group: AreaGroup) => {
|
|
224
|
+
let opacity = '0.75';
|
|
225
|
+
for (let index = group.stack.length - 1; index >= 0; index -= 1) {
|
|
226
|
+
const task = _tasks[group.stack[index]];
|
|
227
|
+
if (occupiesGlobalDark(task)) {
|
|
228
|
+
opacity = resolveDarkOpacity(task.dark);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const el = group.dark.owners.size ? ensureGlobalDarkEl(area, group) : group.dark.el;
|
|
233
|
+
if (el) el.style.setProperty('--layer-dark', opacity);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const acquireGlobalDark = (area: string, group: AreaGroup, layerId: string, immediate = false) => {
|
|
237
|
+
if (!layerId) return;
|
|
238
|
+
if (group.dark.owners.has(layerId)) {
|
|
239
|
+
syncGlobalDarkOpacity(area, group);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
group.dark.owners.add(layerId);
|
|
243
|
+
const el = ensureGlobalDarkEl(area, group);
|
|
244
|
+
if (!el) return;
|
|
245
|
+
syncGlobalDarkOpacity(area, group);
|
|
246
|
+
if (group.dark.owners.size > 1) {
|
|
247
|
+
if (group.dark.view === 1) setGlobalDarkView(group, 3);
|
|
248
|
+
else if (group.dark.view === 0) setGlobalDarkView(group, immediate ? 3 : 2);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (immediate) {
|
|
252
|
+
setGlobalDarkView(group, 3);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (group.dark.view === 0) setGlobalDarkView(group, 2);
|
|
256
|
+
else if (group.dark.view === 1) setGlobalDarkView(group, 3);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const releaseGlobalDark = (area: string, group: AreaGroup, layerId: string, immediate = false) => {
|
|
260
|
+
if (!layerId || !group.dark.owners.delete(layerId)) {
|
|
261
|
+
syncGlobalDarkOpacity(area, group);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
syncGlobalDarkOpacity(area, group);
|
|
265
|
+
if (group.dark.owners.size > 0) return;
|
|
266
|
+
if (immediate || !group.dark.el) {
|
|
267
|
+
setGlobalDarkView(group, 0);
|
|
268
|
+
const afterHide = group.dark.afterHide;
|
|
269
|
+
group.dark.afterHide = undefined;
|
|
270
|
+
afterHide?.();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (group.dark.view === 0) return;
|
|
274
|
+
setGlobalDarkView(group, 1);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const visibleGroupIds = (group: AreaGroup) =>
|
|
278
|
+
group.stack.filter((layerId) => {
|
|
279
|
+
const view = _tasks[layerId]?.view;
|
|
280
|
+
return view === 2 || view === 3;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const groupHasBlocking = (group: AreaGroup, exceptId = '') =>
|
|
284
|
+
group.stack.some((layerId) => layerId !== exceptId && isBlockingView(_tasks[layerId]?.view));
|
|
285
|
+
|
|
286
|
+
const syncStackIndex = (group: AreaGroup) => {
|
|
287
|
+
group.stack.forEach((layerId, index) => {
|
|
288
|
+
_tasks[layerId]?.setStackIndex?.(index);
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const recomputeRecess = (group: AreaGroup) => {
|
|
293
|
+
const visible = visibleGroupIds(group);
|
|
294
|
+
visible.forEach((layerId, index) => {
|
|
295
|
+
let depth = 0;
|
|
296
|
+
for (let next = index + 1; next < visible.length; next += 1) {
|
|
297
|
+
if (_tasks[visible[next]]?.openMode === 'cascade') depth += 1;
|
|
298
|
+
}
|
|
299
|
+
_tasks[layerId]?.setRecess?.(depth > 2 ? 2 : depth);
|
|
300
|
+
});
|
|
301
|
+
group.stack.forEach((layerId) => {
|
|
302
|
+
if (!visible.includes(layerId)) _tasks[layerId]?.setRecess?.(0);
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const ensureOnStack = (group: AreaGroup, layerId: string) => {
|
|
307
|
+
const index = group.stack.indexOf(layerId);
|
|
308
|
+
if (index !== -1) group.stack.splice(index, 1);
|
|
309
|
+
group.stack.push(layerId);
|
|
310
|
+
syncStackIndex(group);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const removeFromStack = (group: AreaGroup, layerId: string) => {
|
|
314
|
+
const index = group.stack.indexOf(layerId);
|
|
315
|
+
if (index !== -1) group.stack.splice(index, 1);
|
|
316
|
+
syncStackIndex(group);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const removeFromQueue = (group: AreaGroup, layerId: string) => {
|
|
320
|
+
group.queue = group.queue.filter((job) => job.id !== layerId);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const getVisibleTop = (group: AreaGroup, exceptId = '') => {
|
|
324
|
+
for (let index = group.stack.length - 1; index >= 0; index -= 1) {
|
|
325
|
+
const layerId = group.stack[index];
|
|
326
|
+
if (layerId === exceptId) continue;
|
|
327
|
+
const task = _tasks[layerId];
|
|
328
|
+
if (task && (task.view === 2 || task.view === 3)) return task;
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
};
|
|
332
|
+
|
|
122
333
|
const hasInlineProperty = (style: CSSStyleDeclaration, property: string) => {
|
|
123
334
|
for (let index = 0; index < style.length; index += 1) {
|
|
124
335
|
if (style.item(index) === property) return true;
|
|
@@ -307,25 +518,78 @@
|
|
|
307
518
|
|
|
308
519
|
// 状态管理
|
|
309
520
|
const state = reactive({
|
|
310
|
-
|
|
521
|
+
coverDark: '' as string | number,
|
|
522
|
+
immediate: false,
|
|
523
|
+
openMode: 'cover' as LayerMode,
|
|
311
524
|
params: {},
|
|
525
|
+
parkAfterHide: false,
|
|
526
|
+
recess: 0,
|
|
527
|
+
stackIndex: 0,
|
|
528
|
+
view: 0,
|
|
312
529
|
});
|
|
313
530
|
const scrollLockOwner = {};
|
|
314
531
|
|
|
532
|
+
const layerStyle = computed(() => {
|
|
533
|
+
let style = '';
|
|
534
|
+
if (`${props.dark}` !== '') style += `--layer-dark:${props.dark};`;
|
|
535
|
+
if (`${state.coverDark}` !== '') style += `--layer-cover-dark:${state.coverDark};`;
|
|
536
|
+
if (props.zIndex) style += `z-index:${props.zIndex};`;
|
|
537
|
+
else if (state.stackIndex > 0) style += `z-index:${50 + state.stackIndex};`;
|
|
538
|
+
return style;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const syncTaskView = (view: number) => {
|
|
542
|
+
if (!_tasks[props.id]) return;
|
|
543
|
+
_tasks[props.id].view = view;
|
|
544
|
+
_tasks[props.id].openMode = state.openMode;
|
|
545
|
+
_tasks[props.id].parkAfterHide = state.parkAfterHide;
|
|
546
|
+
_tasks[props.id].participates = participatesInGroup(props.dark);
|
|
547
|
+
_tasks[props.id].dark = props.dark;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const syncOwnDarkOccupancy = () => {
|
|
551
|
+
if (!env.client || !participatesInGroup(props.dark) || !props.id) return;
|
|
552
|
+
const group = getAreaGroup(props.area);
|
|
553
|
+
const immediate = state.immediate;
|
|
554
|
+
if (
|
|
555
|
+
occupiesGlobalDark({
|
|
556
|
+
dark: props.dark,
|
|
557
|
+
parkAfterHide: state.parkAfterHide,
|
|
558
|
+
participates: true,
|
|
559
|
+
view: state.view,
|
|
560
|
+
})
|
|
561
|
+
) {
|
|
562
|
+
acquireGlobalDark(props.area, group, props.id, immediate);
|
|
563
|
+
} else {
|
|
564
|
+
releaseGlobalDark(props.area, group, props.id, immediate);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
315
568
|
watch(
|
|
316
569
|
() => state.view,
|
|
317
570
|
(view: any) => {
|
|
318
|
-
|
|
319
|
-
_tasks[props.id].view = view;
|
|
320
|
-
}
|
|
571
|
+
syncTaskView(view);
|
|
321
572
|
if (view === 2 || view === 3) pushEscapeStack(props.id);
|
|
322
573
|
else removeEscapeStack(props.id);
|
|
323
574
|
if (view > 0) acquireScrollLock(scrollLockOwner);
|
|
324
575
|
else releaseScrollLock(scrollLockOwner);
|
|
576
|
+
syncOwnDarkOccupancy();
|
|
577
|
+
state.immediate = false;
|
|
578
|
+
if (participatesInGroup(props.dark) && props.id) {
|
|
579
|
+
nextTick(() => recomputeRecess(getAreaGroup(props.area)));
|
|
580
|
+
}
|
|
325
581
|
},
|
|
326
582
|
{ deep: true, immediate: true },
|
|
327
583
|
);
|
|
328
584
|
|
|
585
|
+
watch(
|
|
586
|
+
() => props.dark,
|
|
587
|
+
() => {
|
|
588
|
+
syncTaskView(state.view);
|
|
589
|
+
syncOwnDarkOccupancy();
|
|
590
|
+
},
|
|
591
|
+
);
|
|
592
|
+
|
|
329
593
|
watch(
|
|
330
594
|
() => props.escClose,
|
|
331
595
|
(escClose) => {
|
|
@@ -350,6 +614,138 @@
|
|
|
350
614
|
none: [],
|
|
351
615
|
};
|
|
352
616
|
|
|
617
|
+
const resolveMode = (params: any): LayerMode => {
|
|
618
|
+
const mode = params?.mode;
|
|
619
|
+
if (mode === 'cascade' || mode === 'displace' || mode === 'replace' || mode === 'queue') return mode;
|
|
620
|
+
return 'cover';
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const bindOpenCallbacks = (params: any = {}, then = () => {}) => {
|
|
624
|
+
if (params?.on?.show) on.show.push(params.on.show);
|
|
625
|
+
if (params?.on?.hide) on.hide.push(params.on.hide);
|
|
626
|
+
on.show.push(then);
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const runCallbacks = (key: 'show' | 'hide' | 'none') => {
|
|
630
|
+
on[key].forEach((fun: any) => {
|
|
631
|
+
if (isFunction(fun)) fun();
|
|
632
|
+
});
|
|
633
|
+
on[key].length = 0;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const flushQueue = (group: AreaGroup) => {
|
|
637
|
+
if (groupHasBlocking(group) || !group.queue.length) return;
|
|
638
|
+
const job = group.queue.shift();
|
|
639
|
+
if (!job) return;
|
|
640
|
+
const task = _tasks[job.id];
|
|
641
|
+
if (!task) {
|
|
642
|
+
flushQueue(group);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const playParams = { ...job.params };
|
|
646
|
+
if (playParams.mode === 'queue') delete playParams.mode;
|
|
647
|
+
if (job.kind === 'open') task.open(playParams, job.then);
|
|
648
|
+
else task.show(playParams, job.then);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const scheduleFlushQueue = (group: AreaGroup) => {
|
|
652
|
+
const flush = () => flushQueue(group);
|
|
653
|
+
if (group.dark.view === 1) {
|
|
654
|
+
const prev = group.dark.afterHide;
|
|
655
|
+
group.dark.afterHide = () => {
|
|
656
|
+
prev?.();
|
|
657
|
+
flush();
|
|
658
|
+
};
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
nextTick(flush);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const onGroupLayerClosed = (layerId: string) => {
|
|
665
|
+
if (!participatesInGroup(props.dark)) return;
|
|
666
|
+
const group = getAreaGroup(props.area);
|
|
667
|
+
removeFromStack(group, layerId);
|
|
668
|
+
const topId = group.stack[group.stack.length - 1];
|
|
669
|
+
if (topId && _tasks[topId]?.view === 4) _tasks[topId].unpark();
|
|
670
|
+
recomputeRecess(group);
|
|
671
|
+
scheduleFlushQueue(group);
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const closeReplacedLayers = (mode: LayerMode, params: any, immediate: boolean) => {
|
|
675
|
+
if (mode !== 'replace' || !participatesInGroup(props.dark) || !props.id) return;
|
|
676
|
+
const group = getAreaGroup(props.area);
|
|
677
|
+
const prev = getVisibleTop(group, props.id);
|
|
678
|
+
const targets = params?.replace === 'all' ? group.stack.filter((layerId) => layerId !== props.id) : prev ? [prev.id] : [];
|
|
679
|
+
targets.forEach((layerId) => {
|
|
680
|
+
const task = _tasks[layerId];
|
|
681
|
+
if (!task) return;
|
|
682
|
+
if (immediate) task.none();
|
|
683
|
+
else task.hide();
|
|
684
|
+
});
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const applyIncomingRelation = (mode: LayerMode, params: any, immediate: boolean) => {
|
|
688
|
+
if (!participatesInGroup(props.dark) || !props.id) {
|
|
689
|
+
state.coverDark = '';
|
|
690
|
+
state.recess = 0;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const group = getAreaGroup(props.area);
|
|
694
|
+
const prev = getVisibleTop(group, props.id);
|
|
695
|
+
state.coverDark = resolveCoverDark(mode, !!prev, params?.coverDark);
|
|
696
|
+
state.recess = 0;
|
|
697
|
+
|
|
698
|
+
if (mode === 'displace' && prev) prev.park({ immediate });
|
|
699
|
+
ensureOnStack(group, props.id);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const syncGroupPresentation = () => {
|
|
703
|
+
if (!participatesInGroup(props.dark) || !props.id) return;
|
|
704
|
+
recomputeRecess(getAreaGroup(props.area));
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const tryEnqueue = (kind: 'open' | 'show', params: any, then: any) => {
|
|
708
|
+
if (!participatesInGroup(props.dark) || resolveMode(params) !== 'queue' || !props.id) return false;
|
|
709
|
+
const group = getAreaGroup(props.area);
|
|
710
|
+
if (!groupHasBlocking(group, props.id)) return false;
|
|
711
|
+
removeFromQueue(group, props.id);
|
|
712
|
+
group.queue.push({ id: props.id, kind, params, then });
|
|
713
|
+
return true;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const setRecess = (depth: number) => {
|
|
717
|
+
state.recess = depth;
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const setStackIndex = (index: number) => {
|
|
721
|
+
state.stackIndex = index;
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const park = (options: any = {}) => {
|
|
725
|
+
if (options.immediate) {
|
|
726
|
+
state.parkAfterHide = false;
|
|
727
|
+
state.immediate = true;
|
|
728
|
+
state.view = 4;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (state.view === 2 || state.view === 3) {
|
|
732
|
+
state.parkAfterHide = true;
|
|
733
|
+
syncTaskView(state.view);
|
|
734
|
+
state.view = 1;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const unpark = (options: any = {}) => {
|
|
739
|
+
if (state.view !== 4) return;
|
|
740
|
+
state.parkAfterHide = false;
|
|
741
|
+
if (options.immediate) {
|
|
742
|
+
state.immediate = true;
|
|
743
|
+
state.view = 3;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
state.view = 2;
|
|
747
|
+
};
|
|
748
|
+
|
|
353
749
|
// 注入参数
|
|
354
750
|
const init = (params: any = {}) => {
|
|
355
751
|
_tasks[id].params = params;
|
|
@@ -364,68 +760,86 @@
|
|
|
364
760
|
|
|
365
761
|
// 显示弹层
|
|
366
762
|
const show = (params: any = {}, then = () => {}) => {
|
|
763
|
+
if (tryEnqueue('show', params, then)) return;
|
|
764
|
+
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
765
|
+
bindOpenCallbacks(params, then);
|
|
766
|
+
state.openMode = resolveMode(params);
|
|
767
|
+
applyIncomingRelation(state.openMode, params, false);
|
|
768
|
+
state.parkAfterHide = false;
|
|
367
769
|
state.view = 2;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
if (params?.on?.hide) {
|
|
372
|
-
on.hide.push(params.on.hide);
|
|
373
|
-
}
|
|
374
|
-
on.show.push(then);
|
|
770
|
+
closeReplacedLayers(state.openMode, params, false);
|
|
771
|
+
syncGroupPresentation();
|
|
375
772
|
};
|
|
376
773
|
|
|
377
774
|
// 隐藏弹层
|
|
378
775
|
const hide = (_params: any = {}, then = () => {}) => {
|
|
379
|
-
if (state.view ===
|
|
776
|
+
if (state.view === 4) {
|
|
777
|
+
on.hide.push(then);
|
|
778
|
+
state.parkAfterHide = false;
|
|
779
|
+
state.immediate = true;
|
|
780
|
+
state.view = 0;
|
|
781
|
+
runCallbacks('hide');
|
|
782
|
+
onGroupLayerClosed(id);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (state.view === 2 || state.view === 3) {
|
|
786
|
+
state.parkAfterHide = false;
|
|
787
|
+
syncTaskView(state.view);
|
|
380
788
|
state.view = 1;
|
|
381
789
|
on.hide.push(then);
|
|
382
790
|
}
|
|
383
791
|
};
|
|
384
792
|
|
|
385
793
|
// 打开弹层(没有动画过程)
|
|
386
|
-
const open = (
|
|
794
|
+
const open = (params: any = {}, then = () => {}) => {
|
|
795
|
+
if (tryEnqueue('open', params, then)) return;
|
|
796
|
+
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
797
|
+
bindOpenCallbacks(params, then);
|
|
798
|
+
state.openMode = resolveMode(params);
|
|
799
|
+
applyIncomingRelation(state.openMode, params, true);
|
|
800
|
+
state.parkAfterHide = false;
|
|
801
|
+
state.immediate = true;
|
|
387
802
|
state.view = 3;
|
|
388
|
-
|
|
803
|
+
closeReplacedLayers(state.openMode, params, true);
|
|
804
|
+
syncGroupPresentation();
|
|
805
|
+
runCallbacks('show');
|
|
389
806
|
};
|
|
390
807
|
// 关闭弹层(没有动画过程)
|
|
391
808
|
const none = (_params: any = {}, then = () => {}) => {
|
|
809
|
+
const wasOpen = state.view > 0;
|
|
810
|
+
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
811
|
+
state.parkAfterHide = false;
|
|
812
|
+
state.immediate = true;
|
|
392
813
|
state.view = 0;
|
|
393
814
|
on.none.push(then);
|
|
394
815
|
// 立即执行回调,因为没有动画
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
fun();
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
on.none.length = 0;
|
|
816
|
+
runCallbacks('none');
|
|
817
|
+
if (wasOpen) onGroupLayerClosed(id);
|
|
401
818
|
};
|
|
402
819
|
|
|
403
820
|
// 点暗层关闭
|
|
404
821
|
const _sys_hide = (ev: any) => {
|
|
405
822
|
if (props.darkClickClose && ev.target.className === 'fekit-layer-wrap' && state.view === 3) {
|
|
406
|
-
|
|
823
|
+
hide();
|
|
407
824
|
}
|
|
408
825
|
};
|
|
409
826
|
|
|
410
827
|
// 监听动画
|
|
411
|
-
const anim = () => {
|
|
828
|
+
const anim = (ev?: AnimationEvent) => {
|
|
829
|
+
if (ev && ev.target !== ev.currentTarget) return;
|
|
412
830
|
if (state.view === 1) {
|
|
831
|
+
if (state.parkAfterHide) {
|
|
832
|
+
state.parkAfterHide = false;
|
|
833
|
+
state.view = 4;
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
413
836
|
state.view = 0;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
fun();
|
|
417
|
-
}
|
|
418
|
-
});
|
|
419
|
-
on.hide.length = 0; // 清空回调数组
|
|
837
|
+
runCallbacks('hide');
|
|
838
|
+
onGroupLayerClosed(id);
|
|
420
839
|
}
|
|
421
840
|
if (state.view === 2) {
|
|
422
841
|
state.view = 3;
|
|
423
|
-
|
|
424
|
-
if (isFunction(fun)) {
|
|
425
|
-
fun();
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
on.show.length = 0; // 清空回调数组
|
|
842
|
+
runCallbacks('show');
|
|
429
843
|
}
|
|
430
844
|
};
|
|
431
845
|
|
|
@@ -435,7 +849,25 @@
|
|
|
435
849
|
console.error('Layer 有重复 id', id);
|
|
436
850
|
}
|
|
437
851
|
if (id) {
|
|
438
|
-
_tasks[id] = {
|
|
852
|
+
_tasks[id] = {
|
|
853
|
+
dark: props.dark,
|
|
854
|
+
darkClickClose: props.darkClickClose,
|
|
855
|
+
escClose: props.escClose,
|
|
856
|
+
hide,
|
|
857
|
+
id,
|
|
858
|
+
init,
|
|
859
|
+
none,
|
|
860
|
+
open,
|
|
861
|
+
openMode: state.openMode,
|
|
862
|
+
park,
|
|
863
|
+
parkAfterHide: state.parkAfterHide,
|
|
864
|
+
participates: participatesInGroup(props.dark),
|
|
865
|
+
setRecess,
|
|
866
|
+
setStackIndex,
|
|
867
|
+
show,
|
|
868
|
+
unpark,
|
|
869
|
+
view: state.view,
|
|
870
|
+
};
|
|
439
871
|
ensureEscapeListener();
|
|
440
872
|
} else {
|
|
441
873
|
console.error('Layer 必须填写全局统一的 id');
|
|
@@ -443,6 +875,17 @@
|
|
|
443
875
|
|
|
444
876
|
// 卸载弹层
|
|
445
877
|
onBeforeUnmount(() => {
|
|
878
|
+
if (participatesInGroup(props.dark) && id) {
|
|
879
|
+
const group = getAreaGroup(props.area);
|
|
880
|
+
removeFromQueue(group, id);
|
|
881
|
+
if (state.view > 0) {
|
|
882
|
+
state.immediate = true;
|
|
883
|
+
state.view = 0;
|
|
884
|
+
onGroupLayerClosed(id);
|
|
885
|
+
} else {
|
|
886
|
+
removeFromStack(group, id);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
446
889
|
removeEscapeStack(id);
|
|
447
890
|
releaseScrollLock(scrollLockOwner);
|
|
448
891
|
delete _tasks[id];
|
|
@@ -453,24 +896,122 @@
|
|
|
453
896
|
@use '@neatui/css' as neatui;
|
|
454
897
|
|
|
455
898
|
// 弹层
|
|
899
|
+
@keyframes am-layer-dark-show {
|
|
900
|
+
0% {
|
|
901
|
+
opacity: 0;
|
|
902
|
+
}
|
|
903
|
+
100% {
|
|
904
|
+
opacity: 1;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
@keyframes am-layer-dark-hide {
|
|
909
|
+
0% {
|
|
910
|
+
opacity: 1;
|
|
911
|
+
}
|
|
912
|
+
100% {
|
|
913
|
+
opacity: 0;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.fekit-layer-dark {
|
|
918
|
+
--layer-dark: 0.75;
|
|
919
|
+
position: fixed;
|
|
920
|
+
inset: 0;
|
|
921
|
+
z-index: 50;
|
|
922
|
+
pointer-events: none;
|
|
923
|
+
background-color: rgba(0, 0, 0, var(--layer-dark));
|
|
924
|
+
opacity: 0;
|
|
925
|
+
transition: background-color 180ms linear;
|
|
926
|
+
|
|
927
|
+
&[view='2'] {
|
|
928
|
+
animation: am-layer-dark-show 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
&[view='3'] {
|
|
932
|
+
opacity: 1;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
&[view='1'] {
|
|
936
|
+
animation: am-layer-dark-hide 260ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
&[view='0'] {
|
|
940
|
+
opacity: 0;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
456
944
|
.fekit-layer {
|
|
945
|
+
--layer-dark: 0.75;
|
|
946
|
+
--layer-cover-dark: 0.32;
|
|
457
947
|
position: fixed;
|
|
458
948
|
width: 100%;
|
|
459
949
|
height: 100%;
|
|
460
950
|
left: 0;
|
|
461
951
|
top: 0;
|
|
462
952
|
z-index: 50;
|
|
463
|
-
background-color:
|
|
953
|
+
background-color: transparent;
|
|
464
954
|
pointer-events: none;
|
|
465
955
|
|
|
956
|
+
.fekit-layer-cover-dark {
|
|
957
|
+
position: absolute;
|
|
958
|
+
inset: 0;
|
|
959
|
+
z-index: 0;
|
|
960
|
+
background-color: rgba(0, 0, 0, var(--layer-cover-dark));
|
|
961
|
+
opacity: 0;
|
|
962
|
+
pointer-events: none;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
&[view='2'] .fekit-layer-cover-dark {
|
|
966
|
+
animation: am-layer-dark-show 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
&[view='3'] .fekit-layer-cover-dark {
|
|
970
|
+
opacity: 1;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
&[view='1'] .fekit-layer-cover-dark {
|
|
974
|
+
animation: am-layer-dark-hide 260ms cubic-bezier(0.4, 0, 0.2, 1) both;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
&[view='4'] {
|
|
978
|
+
pointer-events: none;
|
|
979
|
+
|
|
980
|
+
.fekit-layer-wrap {
|
|
981
|
+
pointer-events: none;
|
|
982
|
+
|
|
983
|
+
& > * {
|
|
984
|
+
opacity: 0;
|
|
985
|
+
visibility: hidden;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
&[view='3'][data-recess='1'] .fekit-layer-wrap > *:not(.fekit-layer-cover-dark) {
|
|
991
|
+
transform: scale(0.94);
|
|
992
|
+
opacity: 0.88;
|
|
993
|
+
transition:
|
|
994
|
+
transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
995
|
+
opacity 280ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
&[view='3'][data-recess='2'] .fekit-layer-wrap > *:not(.fekit-layer-cover-dark) {
|
|
999
|
+
transform: scale(0.88);
|
|
1000
|
+
opacity: 0.76;
|
|
1001
|
+
transition:
|
|
1002
|
+
transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
|
|
1003
|
+
opacity 280ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
466
1006
|
&-wrap {
|
|
467
1007
|
position: relative;
|
|
468
1008
|
width: 100%;
|
|
469
1009
|
height: 100%;
|
|
470
1010
|
pointer-events: auto;
|
|
471
1011
|
|
|
472
|
-
& >
|
|
1012
|
+
& > *:not(.fekit-layer-cover-dark) {
|
|
473
1013
|
position: relative !important;
|
|
1014
|
+
z-index: 1;
|
|
474
1015
|
|
|
475
1016
|
&.com-dialog {
|
|
476
1017
|
max-width: 80%;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
:ui-form="`${theme} sz:${sz} ${state.error.co} type:textarea ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
|
|
3
|
+
:ui-form="`${theme} sz:${sz} ${state.error.co} type:textarea ${isAutoHeight ? 'auto-height' : ''} ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
|
|
4
4
|
@click="emits('click', $event)"
|
|
5
5
|
>
|
|
6
6
|
<div :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
:placeholder="placeholder"
|
|
22
22
|
:disabled="disabled"
|
|
23
23
|
:readonly="readonly"
|
|
24
|
-
:rows="
|
|
25
|
-
ui-scroll=":y"
|
|
24
|
+
:rows="displayRows"
|
|
25
|
+
:ui-scroll="isAutoHeight && !isOverflowing ? undefined : ':y'"
|
|
26
26
|
dir="auto"
|
|
27
27
|
@focus="emits('focus', $event, state)"
|
|
28
28
|
@input="input($event)"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
</template>
|
|
46
46
|
|
|
47
47
|
<script setup lang="ts">
|
|
48
|
-
import { reactive, watch, ref, nextTick, onMounted } from 'vue';
|
|
48
|
+
import { reactive, watch, ref, nextTick, onMounted, onBeforeUnmount, computed } from 'vue';
|
|
49
49
|
import { isObject, isFunction } from '@fekit/utils';
|
|
50
50
|
|
|
51
51
|
const emits = defineEmits(['update:modelValue', 'click', 'blur', 'focus', 'change', 'input', 'clear']);
|
|
@@ -71,8 +71,10 @@
|
|
|
71
71
|
* prefix 前缀内容
|
|
72
72
|
* suffix 后缀内容
|
|
73
73
|
* interacted 用户是否交互
|
|
74
|
-
* autoHeight
|
|
75
|
-
* rows
|
|
74
|
+
* autoHeight 是否自动调整高度;同时传入 minRows 与 maxRows 时默认开启;单独开启时无限增高
|
|
75
|
+
* rows 初始行数(未开启自动高度,或开启但未同时设 min/max 时)
|
|
76
|
+
* minRows 与 maxRows 同时传入时生效的最小行数
|
|
77
|
+
* maxRows 与 minRows 同时传入时生效的最大行数,超出后出现滚动条
|
|
76
78
|
*/
|
|
77
79
|
|
|
78
80
|
// 类型
|
|
@@ -95,8 +97,10 @@
|
|
|
95
97
|
prefix?: Array<any>;
|
|
96
98
|
suffix?: Array<any>;
|
|
97
99
|
interacted?: boolean | number | null;
|
|
98
|
-
autoHeight?: boolean;
|
|
100
|
+
autoHeight?: boolean | null;
|
|
99
101
|
rows?: number;
|
|
102
|
+
minRows?: number;
|
|
103
|
+
maxRows?: number;
|
|
100
104
|
}
|
|
101
105
|
const props: any = withDefaults(defineProps<Props>(), {
|
|
102
106
|
modelValue: '',
|
|
@@ -117,8 +121,9 @@
|
|
|
117
121
|
prefix: () => [],
|
|
118
122
|
suffix: () => [],
|
|
119
123
|
interacted: null,
|
|
124
|
+
// Boolean 未传时 Vue 会落到 false,无法与「显式关闭」区分
|
|
125
|
+
autoHeight: null,
|
|
120
126
|
// textarea 特有属性默认值
|
|
121
|
-
autoHeight: false,
|
|
122
127
|
rows: 3,
|
|
123
128
|
});
|
|
124
129
|
|
|
@@ -132,18 +137,81 @@
|
|
|
132
137
|
});
|
|
133
138
|
|
|
134
139
|
// textarea 元素引用
|
|
135
|
-
const el
|
|
140
|
+
const el = ref<HTMLTextAreaElement | null>(null);
|
|
141
|
+
const isOverflowing = ref(false);
|
|
142
|
+
|
|
143
|
+
const fRowLimit = (value: unknown, fallback: number) => {
|
|
144
|
+
const n = Number(value);
|
|
145
|
+
return Number.isFinite(n) && n > 0 ? Math.max(1, Math.floor(n)) : fallback;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const hasRowRange = computed(() => props.minRows != null && props.maxRows != null);
|
|
149
|
+
const isAutoHeight = computed(() => (props.autoHeight === false ? false : props.autoHeight === true ? true : hasRowRange.value));
|
|
150
|
+
const resolvedMinRows = computed(() => fRowLimit(props.minRows, 1));
|
|
151
|
+
const resolvedMaxRows = computed(() => Math.max(resolvedMinRows.value, fRowLimit(props.maxRows, 10)));
|
|
152
|
+
// 有 min/max 时从最小行起算;仅 autoHeight 时保留 rows,并无限增高
|
|
153
|
+
const displayRows = computed(() => (isAutoHeight.value && hasRowRange.value ? resolvedMinRows.value : props.rows));
|
|
154
|
+
|
|
155
|
+
let measurementQueued = false;
|
|
156
|
+
let resizeObserver: ResizeObserver | undefined;
|
|
157
|
+
let observedWidth = 0;
|
|
158
|
+
|
|
159
|
+
const fMeasureHeight = () => {
|
|
160
|
+
measurementQueued = false;
|
|
161
|
+
const node = el.value;
|
|
162
|
+
if (!node) return;
|
|
163
|
+
|
|
164
|
+
if (!isAutoHeight.value) {
|
|
165
|
+
node.style.height = '';
|
|
166
|
+
node.style.overflowY = '';
|
|
167
|
+
isOverflowing.value = false;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 未同时设 min/max:跟内容无限增高,不封顶
|
|
172
|
+
if (!hasRowRange.value) {
|
|
173
|
+
node.style.height = 'auto';
|
|
174
|
+
node.style.overflowY = 'hidden';
|
|
175
|
+
node.style.height = `${node.scrollHeight + 1}px`;
|
|
176
|
+
isOverflowing.value = false;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
node.style.height = '0px';
|
|
181
|
+
node.style.overflowY = 'hidden';
|
|
182
|
+
|
|
183
|
+
const style = getComputedStyle(node);
|
|
184
|
+
let lineHeight = parseFloat(style.lineHeight);
|
|
185
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
186
|
+
lineHeight = (parseFloat(style.fontSize) || 14) * 1.5;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const paddingBlock = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
|
|
190
|
+
const minHeight = resolvedMinRows.value * lineHeight + paddingBlock;
|
|
191
|
+
const maxHeight = resolvedMaxRows.value * lineHeight + paddingBlock;
|
|
192
|
+
const contentHeight = node.scrollHeight;
|
|
193
|
+
const overflowing = contentHeight > maxHeight + 1;
|
|
194
|
+
const measuredHeight = contentHeight <= minHeight + 1 ? minHeight : contentHeight;
|
|
195
|
+
const targetHeight = Math.min(Math.max(measuredHeight, minHeight), maxHeight);
|
|
196
|
+
|
|
197
|
+
node.style.height = `${Math.ceil(targetHeight)}px`;
|
|
198
|
+
node.style.overflowY = overflowing ? 'auto' : 'hidden';
|
|
199
|
+
isOverflowing.value = overflowing;
|
|
200
|
+
};
|
|
136
201
|
|
|
137
|
-
// 自动高度功能
|
|
138
202
|
const fAutoHeight = () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
el.value.style.height = el.value.scrollHeight + 1 + 'px';
|
|
143
|
-
}
|
|
144
|
-
});
|
|
203
|
+
if (measurementQueued) return;
|
|
204
|
+
measurementQueued = true;
|
|
205
|
+
nextTick(fMeasureHeight);
|
|
145
206
|
};
|
|
146
207
|
|
|
208
|
+
watch(
|
|
209
|
+
[() => isAutoHeight.value, () => resolvedMinRows.value, () => resolvedMaxRows.value, () => props.modelValue, () => props.rows, () => props.sz, () => props.theme],
|
|
210
|
+
() => {
|
|
211
|
+
fAutoHeight();
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
147
215
|
// 外部传入交互标识,主要用于外部没有输入但点击了提交
|
|
148
216
|
watch(
|
|
149
217
|
() => props.interacted,
|
|
@@ -267,7 +335,23 @@
|
|
|
267
335
|
|
|
268
336
|
// 组件挂载后初始化自动高度
|
|
269
337
|
onMounted(() => {
|
|
270
|
-
|
|
338
|
+
const node = el.value;
|
|
339
|
+
if (node && typeof ResizeObserver !== 'undefined') {
|
|
340
|
+
observedWidth = node.getBoundingClientRect().width;
|
|
341
|
+
resizeObserver = new ResizeObserver(() => {
|
|
342
|
+
const width = node.getBoundingClientRect().width;
|
|
343
|
+
if (Math.abs(width - observedWidth) > 0.5) {
|
|
344
|
+
observedWidth = width;
|
|
345
|
+
fAutoHeight();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
resizeObserver.observe(node);
|
|
349
|
+
}
|
|
271
350
|
fAutoHeight();
|
|
272
351
|
});
|
|
352
|
+
|
|
353
|
+
onBeforeUnmount(() => {
|
|
354
|
+
resizeObserver?.disconnect();
|
|
355
|
+
resizeObserver = undefined;
|
|
356
|
+
});
|
|
273
357
|
</script>
|