@mptw/skylens-ui 1.0.0 → 1.1.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/app.vue
CHANGED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.multi-email-input(:class='{ collapsable, collapsed }')
|
|
3
|
+
a.multi-email-input-header(href='javascript:;', @click='onClickedToggleCollapse')
|
|
4
|
+
.left-block
|
|
5
|
+
span 與其他使用者共用
|
|
6
|
+
SLInfoTip(placement="top", :pinnable='false')
|
|
7
|
+
.space-y-3
|
|
8
|
+
.flex.items-center.text-sm.text-gray-2
|
|
9
|
+
SLIcon.text-secondary(icon="check-circle")
|
|
10
|
+
span.whitespace-nowrap :確認帳號存在 SKYLENS
|
|
11
|
+
.flex.items-center.text-sm.text-gray-2
|
|
12
|
+
SLIcon.text-danger(icon='exclamation-circle')
|
|
13
|
+
span.whitespace-nowrap :帳號可能不存在 SKYLENS , 需請帳號管理者新增
|
|
14
|
+
.right-block
|
|
15
|
+
SLIcon(icon='chevron-down')
|
|
16
|
+
.multi-email-input-body
|
|
17
|
+
.multi-email-input-body-wrapper
|
|
18
|
+
.emails-input(ref='inputEl')
|
|
19
|
+
.emails-input-wrapper
|
|
20
|
+
.emails-list.custom-scrollbar-y(
|
|
21
|
+
v-if='modelValue.length > 0'
|
|
22
|
+
ref='listEl'
|
|
23
|
+
)
|
|
24
|
+
.emails-list-wrapper(ref='listWrapperEl')
|
|
25
|
+
.email-item(
|
|
26
|
+
v-for='inputEmail in modelValue'
|
|
27
|
+
:key='inputEmail.email'
|
|
28
|
+
)
|
|
29
|
+
.email-item-status
|
|
30
|
+
SLSpinIcon(
|
|
31
|
+
v-if='inputEmail.status === "loading"'
|
|
32
|
+
icon='loading-circle'
|
|
33
|
+
).icon-loading
|
|
34
|
+
SLIcon(
|
|
35
|
+
v-else-if='inputEmail.status === "success"'
|
|
36
|
+
icon='check-circle'
|
|
37
|
+
).icon-success
|
|
38
|
+
SLIcon(
|
|
39
|
+
v-else-if='inputEmail.status === "warn"'
|
|
40
|
+
icon='exclamation-circle'
|
|
41
|
+
).icon-warn
|
|
42
|
+
.email-item-email {{ inputEmail.email }}
|
|
43
|
+
a.email-item-delete-button(
|
|
44
|
+
href='javascript:;'
|
|
45
|
+
@click='onClickedRemoveInputEmail(inputEmail.email)'
|
|
46
|
+
)
|
|
47
|
+
SLIcon(icon='times')
|
|
48
|
+
textarea(
|
|
49
|
+
v-model.trim="email"
|
|
50
|
+
rows="1"
|
|
51
|
+
placeholder='輸入使用者Email'
|
|
52
|
+
ref='textareaEl'
|
|
53
|
+
@paste.prevent="onPastedEmails"
|
|
54
|
+
@keydown.enter.prevent="onEnteredEmail"
|
|
55
|
+
@focus='onFocusTextarea'
|
|
56
|
+
@blur='onBlurTextarea'
|
|
57
|
+
)
|
|
58
|
+
.multi-email-input-role(v-if='isShowRoleOptions')
|
|
59
|
+
SLDropdown.multi-email-input-role-dropdown(
|
|
60
|
+
v-model:show-dropdown="isShowRoleDropdown"
|
|
61
|
+
:title='roleDropdownTitle'
|
|
62
|
+
toggle-icon='chevron-down'
|
|
63
|
+
placement="bottom"
|
|
64
|
+
menu-class='multi-email-input-role-dropdown-menu'
|
|
65
|
+
)
|
|
66
|
+
SLDropdownItem(
|
|
67
|
+
title='檢視'
|
|
68
|
+
:active='role === "normal"'
|
|
69
|
+
@click='onClickedRole("normal")'
|
|
70
|
+
)
|
|
71
|
+
SLDropdownItem(
|
|
72
|
+
title='編輯'
|
|
73
|
+
:active='role === "editor"'
|
|
74
|
+
@click='onClickedRole("editor")'
|
|
75
|
+
)
|
|
76
|
+
.multi-email-input-actions
|
|
77
|
+
SLButton(
|
|
78
|
+
:title="addButtonTitle"
|
|
79
|
+
color="primary"
|
|
80
|
+
size="md"
|
|
81
|
+
:disabled="!isAddEnable"
|
|
82
|
+
:loading='loading'
|
|
83
|
+
@click="onClickedAdd"
|
|
84
|
+
)
|
|
85
|
+
ClientOnly
|
|
86
|
+
Teleport(to='body')
|
|
87
|
+
.multi-email-input-autocomplete(
|
|
88
|
+
:style='`width: ${autoCompleteWidth}px;`'
|
|
89
|
+
ref='autoCompleteEl'
|
|
90
|
+
)
|
|
91
|
+
.swiper(ref='swiperEl')
|
|
92
|
+
.swiper-wrapper
|
|
93
|
+
.swiper-slide
|
|
94
|
+
a.autotcomplete-item(
|
|
95
|
+
v-for='member, i in autoCompletedMembers'
|
|
96
|
+
href='javascript:;'
|
|
97
|
+
tabindex='-1'
|
|
98
|
+
:class='{ focused: focusedAutoCompleteIndex === i }'
|
|
99
|
+
:key='member'
|
|
100
|
+
@click='onClickedMember(member)'
|
|
101
|
+
ref='autoCompleteItemEls'
|
|
102
|
+
) {{ member }}
|
|
103
|
+
.swiper-scrollbar
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<script lang="ts">
|
|
107
|
+
import type { PropType } from "vue";
|
|
108
|
+
import { createPopper } from "@popperjs/core";
|
|
109
|
+
import type { Instance as PopperInstance } from "@popperjs/core";
|
|
110
|
+
import { fromEvent, Subscription } from "rxjs";
|
|
111
|
+
import Swiper from "swiper";
|
|
112
|
+
import { FreeMode, Scrollbar, Mousewheel } from "swiper/modules";
|
|
113
|
+
import "swiper/css";
|
|
114
|
+
import "swiper/css/scrollbar";
|
|
115
|
+
import { OrgUserRole } from "~ui/interface/IOrganization";
|
|
116
|
+
|
|
117
|
+
export default defineComponent({
|
|
118
|
+
name: "SLMultiEmailInput",
|
|
119
|
+
components: {},
|
|
120
|
+
props: {
|
|
121
|
+
collapsable: {
|
|
122
|
+
type: Boolean,
|
|
123
|
+
default: false,
|
|
124
|
+
},
|
|
125
|
+
collapsed: {
|
|
126
|
+
type: Boolean,
|
|
127
|
+
default: true,
|
|
128
|
+
},
|
|
129
|
+
loading: {
|
|
130
|
+
type: Boolean,
|
|
131
|
+
default: false,
|
|
132
|
+
},
|
|
133
|
+
orgMembers: {
|
|
134
|
+
type: Array as PropType<string[]>,
|
|
135
|
+
default: () => [],
|
|
136
|
+
},
|
|
137
|
+
label: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: "與其他使用者共用",
|
|
140
|
+
},
|
|
141
|
+
modelValue: {
|
|
142
|
+
type: Array as PropType<
|
|
143
|
+
{
|
|
144
|
+
email: string;
|
|
145
|
+
status: "loading" | "success" | "warn";
|
|
146
|
+
}[]
|
|
147
|
+
>,
|
|
148
|
+
default: () => [
|
|
149
|
+
{ email: "aaa@example.com", status: "loading" },
|
|
150
|
+
{ email: "bbb@example.com", status: "success" },
|
|
151
|
+
{ email: "ccc@example.com", status: "warn" },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
role: {
|
|
155
|
+
type: String as PropType<OrgUserRole>,
|
|
156
|
+
default: OrgUserRole.Normal,
|
|
157
|
+
},
|
|
158
|
+
isShowRoleOptions: {
|
|
159
|
+
type: Boolean,
|
|
160
|
+
default: true,
|
|
161
|
+
},
|
|
162
|
+
addButtonTitle: {
|
|
163
|
+
type: String,
|
|
164
|
+
default: "加入",
|
|
165
|
+
},
|
|
166
|
+
recommendEmails: {
|
|
167
|
+
type: Array as PropType<string[]>,
|
|
168
|
+
default: () => [],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
emits: [
|
|
172
|
+
"remove:email",
|
|
173
|
+
"paste:emails",
|
|
174
|
+
"enter:email",
|
|
175
|
+
"update:role",
|
|
176
|
+
"add",
|
|
177
|
+
"toggle:collapse",
|
|
178
|
+
],
|
|
179
|
+
setup(props, { emit }) {
|
|
180
|
+
const { collapsable, orgMembers, modelValue, role, recommendEmails } =
|
|
181
|
+
toRefs(props);
|
|
182
|
+
|
|
183
|
+
const inputEl = shallowRef<HTMLDivElement | null>(null);
|
|
184
|
+
const listEl = shallowRef<HTMLDivElement | null>(null);
|
|
185
|
+
const listWrapperEl = shallowRef<HTMLDivElement | null>(null);
|
|
186
|
+
const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
|
|
187
|
+
const autoCompleteEl = shallowRef<HTMLDivElement | null>(null);
|
|
188
|
+
const swiperEl = shallowRef<HTMLDivElement | null>(null);
|
|
189
|
+
const email = ref("");
|
|
190
|
+
const isShowRoleDropdown = ref(false);
|
|
191
|
+
const isShowAutoComplete = ref(false);
|
|
192
|
+
const autoCompleteWidth = ref(0);
|
|
193
|
+
const isScrollToEnd = ref(false);
|
|
194
|
+
const focusedAutoCompleteIndex = ref(-1);
|
|
195
|
+
const focusedTextarea = ref(false);
|
|
196
|
+
|
|
197
|
+
let popperInstance: PopperInstance | null = null;
|
|
198
|
+
let clickSubscription: Subscription | null = null;
|
|
199
|
+
let keydownSubscription: Subscription | null = null;
|
|
200
|
+
let swiper: Swiper | null = null;
|
|
201
|
+
|
|
202
|
+
const autoCompletedMembers = computed(() => {
|
|
203
|
+
if (!email.value) return recommendEmails.value;
|
|
204
|
+
|
|
205
|
+
const regexp = new RegExp(`.*${email.value}.*`, "i");
|
|
206
|
+
return orgMembers.value.filter((member) => regexp.test(member));
|
|
207
|
+
});
|
|
208
|
+
const roleDropdownTitle = computed(() => {
|
|
209
|
+
switch (role.value) {
|
|
210
|
+
case OrgUserRole.Editor:
|
|
211
|
+
return "編輯";
|
|
212
|
+
default:
|
|
213
|
+
return "檢視";
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const isAddEnable = computed(() => {
|
|
217
|
+
let isEnable =
|
|
218
|
+
modelValue.value.filter((inputEmail) => inputEmail.status === "success")
|
|
219
|
+
.length > 0;
|
|
220
|
+
|
|
221
|
+
modelValue.value.forEach((inputEmail) => {
|
|
222
|
+
if (inputEmail.status === "loading") {
|
|
223
|
+
isEnable = false;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return isEnable;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
function onClickedRemoveInputEmail(email: string) {
|
|
231
|
+
emit("remove:email", email);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onPastedEmails(e: ClipboardEvent) {
|
|
235
|
+
if (!e.clipboardData) return;
|
|
236
|
+
|
|
237
|
+
const pastedText = e.clipboardData.getData("text");
|
|
238
|
+
const pastedEmails = pastedText.replaceAll(/\r\n|\r/g, "\n").split("\n");
|
|
239
|
+
const pastedEmailSet: Set<string> = new Set();
|
|
240
|
+
pastedEmails.forEach((email) => {
|
|
241
|
+
if (
|
|
242
|
+
!modelValue.value.find((inputEmail) => inputEmail.email === email)
|
|
243
|
+
) {
|
|
244
|
+
pastedEmailSet.add(email);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
isScrollToEnd.value = true;
|
|
248
|
+
emit("paste:emails", Array.from(pastedEmailSet));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function onEnteredEmail() {
|
|
252
|
+
if (
|
|
253
|
+
!modelValue.value.find((inputEmail) => inputEmail.email === email.value)
|
|
254
|
+
) {
|
|
255
|
+
isScrollToEnd.value = true;
|
|
256
|
+
emit("enter:email", email.value);
|
|
257
|
+
}
|
|
258
|
+
email.value = "";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function onClickedMember(member: string) {
|
|
262
|
+
if (!modelValue.value.find((inputEmail) => inputEmail.email === member)) {
|
|
263
|
+
emit("enter:email", member);
|
|
264
|
+
}
|
|
265
|
+
email.value = "";
|
|
266
|
+
focusedAutoCompleteIndex.value = -1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function onClickedRole(changeRole: "normal" | "editor") {
|
|
270
|
+
emit("update:role", changeRole);
|
|
271
|
+
isShowRoleDropdown.value = false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function onClickedAdd() {
|
|
275
|
+
emit("add");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function updateSwiper() {
|
|
279
|
+
nextTick(() => {
|
|
280
|
+
if (!swiper) return;
|
|
281
|
+
|
|
282
|
+
swiper.update();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function setupSwiper() {
|
|
287
|
+
nextTick(() => {
|
|
288
|
+
if (!swiperEl.value) return;
|
|
289
|
+
|
|
290
|
+
if (swiper) {
|
|
291
|
+
updateSwiper();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
swiper = new Swiper(swiperEl.value, {
|
|
296
|
+
modules: [FreeMode, Scrollbar, Mousewheel],
|
|
297
|
+
slidesPerView: "auto",
|
|
298
|
+
direction: "vertical",
|
|
299
|
+
freeMode: true,
|
|
300
|
+
mousewheel: true,
|
|
301
|
+
scrollbar: {
|
|
302
|
+
el: ".swiper-scrollbar",
|
|
303
|
+
draggable: true,
|
|
304
|
+
snapOnRelease: false,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function destroySwiper() {
|
|
311
|
+
if (swiper) {
|
|
312
|
+
swiper.destroy();
|
|
313
|
+
}
|
|
314
|
+
swiper = null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function subscribeClick() {
|
|
318
|
+
clickSubscription = fromEvent(window, "click").subscribe((e) => {
|
|
319
|
+
const { target } = e;
|
|
320
|
+
if (
|
|
321
|
+
(!textareaEl.value || !textareaEl.value.contains(target as Node)) &&
|
|
322
|
+
(!autoCompleteEl.value ||
|
|
323
|
+
!autoCompleteEl.value.contains(target as Node))
|
|
324
|
+
) {
|
|
325
|
+
hideAutoComplete();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function unsubscribeClick() {
|
|
331
|
+
if (clickSubscription) {
|
|
332
|
+
clickSubscription.unsubscribe();
|
|
333
|
+
}
|
|
334
|
+
clickSubscription = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function setupPopper() {
|
|
338
|
+
nextTick(() => {
|
|
339
|
+
if (!inputEl.value || !autoCompleteEl.value) return;
|
|
340
|
+
popperInstance = createPopper(inputEl.value, autoCompleteEl.value, {
|
|
341
|
+
placement: "bottom",
|
|
342
|
+
modifiers: [
|
|
343
|
+
{
|
|
344
|
+
name: "offset",
|
|
345
|
+
options: {
|
|
346
|
+
offset: [0, 4],
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
{ name: "eventListeners", enabled: false },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function destroyPopper() {
|
|
356
|
+
destroySwiper();
|
|
357
|
+
unsubscribeClick();
|
|
358
|
+
|
|
359
|
+
if (popperInstance) {
|
|
360
|
+
popperInstance.destroy();
|
|
361
|
+
}
|
|
362
|
+
popperInstance = null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function showAutoComplete() {
|
|
366
|
+
if (
|
|
367
|
+
!autoCompleteEl.value ||
|
|
368
|
+
!inputEl.value ||
|
|
369
|
+
autoCompleteEl.value.hasAttribute("data-show") ||
|
|
370
|
+
!popperInstance ||
|
|
371
|
+
!autoCompletedMembers.value.length
|
|
372
|
+
)
|
|
373
|
+
return;
|
|
374
|
+
|
|
375
|
+
isShowAutoComplete.value = true;
|
|
376
|
+
subscribeClick();
|
|
377
|
+
const inputElRect = inputEl.value.getBoundingClientRect();
|
|
378
|
+
autoCompleteWidth.value = inputElRect.width;
|
|
379
|
+
|
|
380
|
+
autoCompleteEl.value.setAttribute("data-show", "");
|
|
381
|
+
popperInstance.setOptions((options) => ({
|
|
382
|
+
...options,
|
|
383
|
+
modifiers: [
|
|
384
|
+
...(options.modifiers || []),
|
|
385
|
+
{ name: "eventListeners", enabled: true },
|
|
386
|
+
],
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
setupSwiper();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function hideAutoComplete() {
|
|
393
|
+
if (!autoCompleteEl.value || !popperInstance) return;
|
|
394
|
+
|
|
395
|
+
isShowAutoComplete.value = false;
|
|
396
|
+
unsubscribeClick();
|
|
397
|
+
destroySwiper();
|
|
398
|
+
autoCompleteEl.value.removeAttribute("data-show");
|
|
399
|
+
popperInstance.setOptions((options) => ({
|
|
400
|
+
...options,
|
|
401
|
+
modifiers: [
|
|
402
|
+
...(options.modifiers || []),
|
|
403
|
+
{ name: "eventListeners", enabled: false },
|
|
404
|
+
],
|
|
405
|
+
}));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function onFocusTextarea() {
|
|
409
|
+
focusedTextarea.value = true;
|
|
410
|
+
showAutoComplete();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function onBlurTextarea() {
|
|
414
|
+
focusedTextarea.value = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function listScrollToEnd() {
|
|
418
|
+
if (!isScrollToEnd.value) return;
|
|
419
|
+
|
|
420
|
+
nextTick(() => {
|
|
421
|
+
isScrollToEnd.value = false;
|
|
422
|
+
if (!listEl.value || !listWrapperEl.value) return;
|
|
423
|
+
|
|
424
|
+
const listElRect = listEl.value.getBoundingClientRect();
|
|
425
|
+
const listWrapperElRect = listWrapperEl.value.getBoundingClientRect();
|
|
426
|
+
const diff = listWrapperElRect.height - listElRect.height;
|
|
427
|
+
|
|
428
|
+
listEl.value.scrollTo({
|
|
429
|
+
top: diff > 0 ? diff : 0,
|
|
430
|
+
behavior: "smooth",
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function onWindowKeydown(e: Event) {
|
|
436
|
+
if (!isShowAutoComplete.value) return;
|
|
437
|
+
|
|
438
|
+
const keyboardEvent = e as KeyboardEvent;
|
|
439
|
+
if (
|
|
440
|
+
keyboardEvent.key === "ArrowDown" ||
|
|
441
|
+
keyboardEvent.key === "ArrowUp" ||
|
|
442
|
+
keyboardEvent.key === "Enter"
|
|
443
|
+
) {
|
|
444
|
+
switch (keyboardEvent.key) {
|
|
445
|
+
case "ArrowDown":
|
|
446
|
+
focusedAutoCompleteIndex.value = Math.min(
|
|
447
|
+
autoCompletedMembers.value.length - 1,
|
|
448
|
+
focusedAutoCompleteIndex.value + 1
|
|
449
|
+
);
|
|
450
|
+
break;
|
|
451
|
+
case "ArrowUp":
|
|
452
|
+
focusedAutoCompleteIndex.value = Math.max(
|
|
453
|
+
-1,
|
|
454
|
+
focusedAutoCompleteIndex.value - 1
|
|
455
|
+
);
|
|
456
|
+
break;
|
|
457
|
+
default:
|
|
458
|
+
const member =
|
|
459
|
+
autoCompletedMembers.value[focusedAutoCompleteIndex.value];
|
|
460
|
+
onClickedMember(member);
|
|
461
|
+
hideAutoComplete();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (textareaEl.value && keyboardEvent.key !== "Enter") {
|
|
465
|
+
if (focusedAutoCompleteIndex.value > -1) {
|
|
466
|
+
textareaEl.value.blur();
|
|
467
|
+
} else {
|
|
468
|
+
textareaEl.value.focus();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
e.stopPropagation();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function setupAutoCompleteItemsTabindex() {
|
|
479
|
+
nextTick(() => {
|
|
480
|
+
const slideEl = swiperEl.value?.querySelector(".swiper-slide");
|
|
481
|
+
if (!slideEl) return;
|
|
482
|
+
|
|
483
|
+
for (let i = 0; i < slideEl.children.length; i += 1) {
|
|
484
|
+
const autoCompleteItemEl = slideEl.children[i];
|
|
485
|
+
|
|
486
|
+
if (i === focusedAutoCompleteIndex.value) {
|
|
487
|
+
autoCompleteItemEl.setAttribute("tabindex", "0");
|
|
488
|
+
} else {
|
|
489
|
+
autoCompleteItemEl.setAttribute("tabindex", "-1");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function scrollToAutoCompleteItem() {
|
|
496
|
+
nextTick(() => {
|
|
497
|
+
if (!swiperEl.value || !swiper) return;
|
|
498
|
+
|
|
499
|
+
if (focusedAutoCompleteIndex.value <= -1) {
|
|
500
|
+
swiper.translateTo(0, 0, false, false);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const swiperWrapperEl = swiperEl.value.querySelector(".swiper-wrapper");
|
|
505
|
+
const swiperSlideEl = swiperEl.value.querySelector(".swiper-slide");
|
|
506
|
+
const itemEl = swiperSlideEl?.children[focusedAutoCompleteIndex.value];
|
|
507
|
+
|
|
508
|
+
if (!swiperWrapperEl || !swiperSlideEl || !itemEl) return;
|
|
509
|
+
|
|
510
|
+
const swiperRect = swiperEl.value.getBoundingClientRect();
|
|
511
|
+
const wrapperRect = swiperWrapperEl.getBoundingClientRect();
|
|
512
|
+
const slideRect = swiperSlideEl.getBoundingClientRect();
|
|
513
|
+
const itemRect = itemEl.getBoundingClientRect();
|
|
514
|
+
const scrollTop = wrapperRect.y - swiperRect.y;
|
|
515
|
+
const scrollBotttom = scrollTop - swiperRect.height;
|
|
516
|
+
const itemTop = slideRect.y - itemRect.y;
|
|
517
|
+
const itemBottom = itemTop - itemRect.height;
|
|
518
|
+
|
|
519
|
+
if (itemTop > scrollTop) {
|
|
520
|
+
swiper.translateTo(itemTop, 300, false, false);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (itemBottom < scrollBotttom) {
|
|
525
|
+
swiper.translateTo(
|
|
526
|
+
itemBottom - scrollBotttom + scrollTop,
|
|
527
|
+
300,
|
|
528
|
+
false,
|
|
529
|
+
false
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function onClickedToggleCollapse() {
|
|
536
|
+
if (!collapsable.value) return;
|
|
537
|
+
|
|
538
|
+
emit("toggle:collapse");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
watch(autoCompletedMembers, () => {
|
|
542
|
+
focusedAutoCompleteIndex.value = -1;
|
|
543
|
+
|
|
544
|
+
if (autoCompletedMembers.value.length) {
|
|
545
|
+
if (focusedTextarea.value) {
|
|
546
|
+
showAutoComplete();
|
|
547
|
+
}
|
|
548
|
+
updateSwiper();
|
|
549
|
+
} else {
|
|
550
|
+
hideAutoComplete();
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
watch(modelValue, () => {
|
|
555
|
+
listScrollToEnd();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
watch(focusedAutoCompleteIndex, () => {
|
|
559
|
+
setupAutoCompleteItemsTabindex();
|
|
560
|
+
scrollToAutoCompleteItem();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
onMounted(() => {
|
|
564
|
+
setupPopper();
|
|
565
|
+
|
|
566
|
+
keydownSubscription = fromEvent(window, "keydown").subscribe(
|
|
567
|
+
onWindowKeydown
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
onBeforeUnmount(() => {
|
|
572
|
+
destroyPopper();
|
|
573
|
+
|
|
574
|
+
if (keydownSubscription) {
|
|
575
|
+
keydownSubscription.unsubscribe();
|
|
576
|
+
}
|
|
577
|
+
keydownSubscription = null;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
inputEl,
|
|
582
|
+
listEl,
|
|
583
|
+
listWrapperEl,
|
|
584
|
+
textareaEl,
|
|
585
|
+
autoCompleteEl,
|
|
586
|
+
swiperEl,
|
|
587
|
+
email,
|
|
588
|
+
isShowRoleDropdown,
|
|
589
|
+
autoCompleteWidth,
|
|
590
|
+
focusedAutoCompleteIndex,
|
|
591
|
+
autoCompletedMembers,
|
|
592
|
+
roleDropdownTitle,
|
|
593
|
+
isAddEnable,
|
|
594
|
+
onClickedRemoveInputEmail,
|
|
595
|
+
onPastedEmails,
|
|
596
|
+
onEnteredEmail,
|
|
597
|
+
onClickedMember,
|
|
598
|
+
onClickedRole,
|
|
599
|
+
onClickedAdd,
|
|
600
|
+
onFocusTextarea,
|
|
601
|
+
onBlurTextarea,
|
|
602
|
+
onClickedToggleCollapse,
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
</script>
|
|
607
|
+
|
|
608
|
+
<style lang="stylus" scoped>
|
|
609
|
+
.multi-email-input
|
|
610
|
+
@apply flex flex-col
|
|
611
|
+
|
|
612
|
+
&.collapsable
|
|
613
|
+
.multi-email-input-header
|
|
614
|
+
@apply cursor-pointer
|
|
615
|
+
|
|
616
|
+
.right-block
|
|
617
|
+
@apply flex
|
|
618
|
+
|
|
619
|
+
&.collapsed
|
|
620
|
+
.multi-email-input-header
|
|
621
|
+
.right-block
|
|
622
|
+
.icon
|
|
623
|
+
@apply rotate-0
|
|
624
|
+
|
|
625
|
+
&-header
|
|
626
|
+
@apply flex items-center justify-between mb-2 cursor-default
|
|
627
|
+
|
|
628
|
+
.left-block
|
|
629
|
+
@apply flex-grow-0 flex-shrink-0 flex items-center text-base text-gray-2
|
|
630
|
+
|
|
631
|
+
.right-block
|
|
632
|
+
@apply flex-grow-0 flex-shrink-0 text-xs hidden
|
|
633
|
+
.icon
|
|
634
|
+
@apply transition-transform rotate-180
|
|
635
|
+
|
|
636
|
+
&-body
|
|
637
|
+
|
|
638
|
+
&-wrapper
|
|
639
|
+
@apply flex items-start space-x-4
|
|
640
|
+
|
|
641
|
+
.emails-input
|
|
642
|
+
@apply flex-grow flex items-center s('p-1.75') space-x-4 bg-white border border-primary-light rounded overflow-hidden
|
|
643
|
+
|
|
644
|
+
&-wrapper
|
|
645
|
+
@apply flex-grow space-y-2 overflow-hidden
|
|
646
|
+
|
|
647
|
+
.emails-list
|
|
648
|
+
@apply s('max-h-[97px]')
|
|
649
|
+
|
|
650
|
+
&-wrapper
|
|
651
|
+
@apply flex flex-col items-start space-y-2 overflow-hidden
|
|
652
|
+
|
|
653
|
+
.email-item
|
|
654
|
+
@apply inline-flex items-center justify-start max-w-full s('px-1.75') s('py-0.5') space-x-2 bg-white border border-primary-lighter rounded-full text-base text-gray-2
|
|
655
|
+
|
|
656
|
+
&-email
|
|
657
|
+
@apply flex-grow truncate
|
|
658
|
+
|
|
659
|
+
&-status
|
|
660
|
+
@apply flex-grow-0 flex-shrink-0
|
|
661
|
+
|
|
662
|
+
.icon-loading
|
|
663
|
+
@apply text-third
|
|
664
|
+
|
|
665
|
+
.icon-success
|
|
666
|
+
@apply text-secondary
|
|
667
|
+
|
|
668
|
+
.icon-warn
|
|
669
|
+
@apply text-danger
|
|
670
|
+
|
|
671
|
+
&-delete-button
|
|
672
|
+
@apply text-xs
|
|
673
|
+
|
|
674
|
+
textarea
|
|
675
|
+
@apply flex-grow-0 flex-shrink-0 block w-full p-0 border-0 bg-white text-sm text-gray-2 resize-none s('focus:outline-none')
|
|
676
|
+
|
|
677
|
+
.multi-email-input-role
|
|
678
|
+
@apply flex-grow-0 flex-shrink-0 flex
|
|
679
|
+
|
|
680
|
+
:deep(.dropdown-toggle)
|
|
681
|
+
@apply text-sm
|
|
682
|
+
|
|
683
|
+
.multi-email-input-actions
|
|
684
|
+
@apply flex-grow-0 flex-shrink-0
|
|
685
|
+
|
|
686
|
+
.btn
|
|
687
|
+
@apply text-sm
|
|
688
|
+
|
|
689
|
+
.multi-email-input-autocomplete
|
|
690
|
+
@apply z-50 s('max-h-[116px]') p-1 bg-white rounded shadow hidden
|
|
691
|
+
|
|
692
|
+
&[data-show]
|
|
693
|
+
@apply flex
|
|
694
|
+
|
|
695
|
+
.swiper
|
|
696
|
+
@apply w-full
|
|
697
|
+
|
|
698
|
+
.swiper-slide
|
|
699
|
+
@apply h-auto
|
|
700
|
+
.swiper-scrollbar
|
|
701
|
+
@apply top-0 right-0 h-full bg-transparent
|
|
702
|
+
|
|
703
|
+
:deep(.swiper-scrollbar-drag)
|
|
704
|
+
@apply bg-primary-lighter
|
|
705
|
+
.autotcomplete-item
|
|
706
|
+
@apply block w-full py-1 px-2 bg-white text-sm text-gray-2
|
|
707
|
+
&:hover, &.focused
|
|
708
|
+
@apply bg-bg
|
|
709
|
+
</style>
|
|
710
|
+
|
|
711
|
+
<style lang="stylus">
|
|
712
|
+
.dropdown-menu.multi-email-input-role-dropdown-menu
|
|
713
|
+
@apply border-primary
|
|
714
|
+
|
|
715
|
+
.dropdown-list-wrapper
|
|
716
|
+
.dropdown-list
|
|
717
|
+
.dropdown-item
|
|
718
|
+
@apply text-center
|
|
719
|
+
</style>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template lang="pug">
|
|
2
|
+
.project-share-item
|
|
3
|
+
.item-info
|
|
4
|
+
.item-info-email {{ email }}
|
|
5
|
+
.item-actions
|
|
6
|
+
SLDropdown(
|
|
7
|
+
v-model:showDropdown="showDropdown"
|
|
8
|
+
:title="`${modelValue === 'normal' ? '檢視者' : '編輯者'}`"
|
|
9
|
+
toggle-icon='chevron-down'
|
|
10
|
+
placement='bottom'
|
|
11
|
+
)
|
|
12
|
+
SLDrowdownItem(
|
|
13
|
+
title="檢視者"
|
|
14
|
+
:class="{ active: modelValue === 'normal' }"
|
|
15
|
+
@click="onClickedViewer"
|
|
16
|
+
)
|
|
17
|
+
SLDrowdownItem(
|
|
18
|
+
title="編輯者"
|
|
19
|
+
:class="{ active: modelValue === 'editor' }"
|
|
20
|
+
@click="onClickedEditor"
|
|
21
|
+
)
|
|
22
|
+
SLDrowdownItem(title="刪除" @click="onClickedDelete")
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import type { PropType } from "vue";
|
|
27
|
+
import { OrgUserRole } from "~ui/interface/IOrganization";
|
|
28
|
+
|
|
29
|
+
export default defineComponent({
|
|
30
|
+
name: "SLProjectShareItem",
|
|
31
|
+
components: {},
|
|
32
|
+
props: {
|
|
33
|
+
email: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: "",
|
|
36
|
+
},
|
|
37
|
+
modelValue: {
|
|
38
|
+
type: String as PropType<OrgUserRole>,
|
|
39
|
+
default: OrgUserRole.Normal,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
emits: ["update:modelValue", "delete"],
|
|
43
|
+
setup(props, { emit }) {
|
|
44
|
+
const { email } = toRefs(props);
|
|
45
|
+
|
|
46
|
+
const showDropdown = ref(false);
|
|
47
|
+
|
|
48
|
+
function onClickedViewer() {
|
|
49
|
+
emit("update:modelValue", email.value, OrgUserRole.Normal);
|
|
50
|
+
showDropdown.value = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onClickedEditor() {
|
|
54
|
+
emit("update:modelValue", email.value, OrgUserRole.Editor);
|
|
55
|
+
showDropdown.value = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onClickedDelete() {
|
|
59
|
+
emit("delete", email.value);
|
|
60
|
+
showDropdown.value = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
showDropdown,
|
|
65
|
+
onClickedViewer,
|
|
66
|
+
onClickedEditor,
|
|
67
|
+
onClickedDelete,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<style lang="stylus" scoped>
|
|
74
|
+
.project-share-item
|
|
75
|
+
@apply flex items-center justify-between space-x-4
|
|
76
|
+
|
|
77
|
+
.item-info
|
|
78
|
+
@apply flex-grow text-sm text-gray-2 truncate
|
|
79
|
+
|
|
80
|
+
.item-actions
|
|
81
|
+
@apply flex-grow-0 flex-shrink-0
|
|
82
|
+
|
|
83
|
+
:deep(.dropdown-menu)
|
|
84
|
+
@apply border-primary-light
|
|
85
|
+
.dropdown-list-wrapper
|
|
86
|
+
.dropdown-list
|
|
87
|
+
@apply s('min-w-[74px]') divide-y divide-bg
|
|
88
|
+
|
|
89
|
+
:deep(.dropdown-toggle)
|
|
90
|
+
@apply s('py-0.75') border-primary-light bg-white text-sm
|
|
91
|
+
|
|
92
|
+
.dropdown-item
|
|
93
|
+
@apply text-center
|
|
94
|
+
</style>
|
package/components/SLTab.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
li(
|
|
3
3
|
v-if="distributed && distributed.header"
|
|
4
4
|
:class="tabNavItemClass"
|
|
5
|
+
:style="distributed && distributed.style"
|
|
5
6
|
@click="navClick"
|
|
6
7
|
ref='tabPaneEl'
|
|
7
8
|
)
|
|
@@ -68,6 +69,7 @@ export default defineComponent({
|
|
|
68
69
|
|
|
69
70
|
const distributed = inject<{
|
|
70
71
|
header: boolean;
|
|
72
|
+
style: string;
|
|
71
73
|
changeTabTo: (newTab: number) => void;
|
|
72
74
|
}>("distributed");
|
|
73
75
|
const activeTab = inject<Ref<number>>("activeTab");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mptw/skylens-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "nuxi dev .playground",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"rxjs": "^7.8.1",
|
|
44
44
|
"sortablejs": "^1.15.2",
|
|
45
45
|
"stylus": "^0.63.0",
|
|
46
|
+
"swiper": "^11.1.1",
|
|
46
47
|
"tailwindcss-textshadow": "^2.1.3",
|
|
47
48
|
"v-calendar": "^3.1.2",
|
|
48
49
|
"vue": "^3.4.22",
|