@mptw/skylens-ui 1.4.87 → 1.4.89
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/components/SLContextMenu.vue +191 -44
- package/package.json +1 -1
|
@@ -1,75 +1,205 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
|
-
.sl-context-menu-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.sl-context-menu-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
.sl-context-menu-wrapper(ref='el')
|
|
3
|
+
Teleport(to="#popup")
|
|
4
|
+
.sl-context-menu-container(v-if='isShow')
|
|
5
|
+
.sl-context-menu-container-modal(@click='onClickModal')
|
|
6
|
+
.sl-context-menu(:style='contextMenuStyle')
|
|
7
|
+
a.sl-context-menu-item(
|
|
8
|
+
v-for='action in contextMenuActions',
|
|
9
|
+
href='javascript:;',
|
|
10
|
+
:key='action.key',
|
|
11
|
+
@click='onClickAction(action.key)',
|
|
12
|
+
) {{ action.label }}
|
|
13
|
+
SLToast.copy-msg-toast(v-model:show='isShowCopyMsg', :message='copyMsg')
|
|
14
|
+
template(#message)
|
|
15
|
+
.inline-flex.items-center(class='space-x-2.5')
|
|
16
|
+
SLIcon(:icon='isCopySuccess ? "check-circle" : "exclamation-circle"')
|
|
17
|
+
span {{ copyMsg }}
|
|
13
18
|
</template>
|
|
14
19
|
|
|
15
20
|
<script lang="ts">
|
|
16
21
|
import { fromEvent, Subscription } from 'rxjs';
|
|
22
|
+
import type IDateRange from '~/interface/IDateRange';
|
|
23
|
+
|
|
24
|
+
const WatermarkImage = '/assets/images/commons/watermark-2.svg';
|
|
25
|
+
const keys = { 37: 1, 38: 1, 30: 1, 40: 1 };
|
|
26
|
+
|
|
27
|
+
function preventDefault(e: Event) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function preventDefaultForScrollKeys(e: Event) {
|
|
32
|
+
if (keys[e.keyCode]) {
|
|
33
|
+
preventDefault(e);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
export default defineComponent({
|
|
19
39
|
name: 'SLContextMenu',
|
|
20
40
|
props: {
|
|
21
|
-
|
|
22
|
-
type:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
target: {
|
|
42
|
+
type: HTMLElement,
|
|
43
|
+
default: () => null,
|
|
44
|
+
},
|
|
45
|
+
filename: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: '',
|
|
48
|
+
},
|
|
49
|
+
dateRanges: {
|
|
50
|
+
type: Array as PropType<(IDateRange | null)[]>,
|
|
51
|
+
default: () => [],
|
|
52
|
+
},
|
|
53
|
+
padding: {
|
|
54
|
+
type: Number,
|
|
55
|
+
default: 0,
|
|
56
|
+
},
|
|
57
|
+
watermark: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
default: true,
|
|
60
|
+
},
|
|
61
|
+
customFilter: {
|
|
62
|
+
type: Function,
|
|
63
|
+
default: null,
|
|
64
|
+
},
|
|
65
|
+
customStyle: {
|
|
66
|
+
type: Object as PropType<Partial<CSSStyleDeclaration>>,
|
|
67
|
+
default: () => { },
|
|
33
68
|
},
|
|
34
69
|
},
|
|
35
70
|
emits: ['action'],
|
|
36
71
|
setup(props, { emit }) {
|
|
37
|
-
|
|
72
|
+
let supportsPassive = false;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
window.addEventListener("test", null, Object.defineProperty({}, 'passive', {
|
|
76
|
+
get: function () { supportsPassive = true; }
|
|
77
|
+
}));
|
|
78
|
+
} catch (e) { }
|
|
79
|
+
|
|
80
|
+
const wheelOpt = supportsPassive ? { passive: false } : false;
|
|
81
|
+
const wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
|
|
82
|
+
|
|
83
|
+
const { target, filename, dateRanges, padding, watermark, customFilter, customStyle } = toRefs(props);
|
|
38
84
|
|
|
39
85
|
const el = shallowRef<HTMLDivElement | null>(null);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
86
|
+
const isShow = ref(false);
|
|
87
|
+
const contextMenuX = ref(0);
|
|
88
|
+
const contextMenuY = ref(0);
|
|
89
|
+
const isShowCopyMsg = ref(false);
|
|
90
|
+
const copyMsg = ref('');
|
|
91
|
+
const isCopySuccess = ref(false);
|
|
42
92
|
|
|
43
93
|
let contextMenuSubscriber: Subscription | null = null;
|
|
94
|
+
let windowScrollSubscriber: Subscription | null = null;
|
|
95
|
+
let clearMsgTimeoutId: any = null;
|
|
44
96
|
|
|
45
|
-
|
|
46
|
-
|
|
97
|
+
const contextMenuActions = computed(() => [
|
|
98
|
+
{ key: 'saveAs', label: '另存圖檔(.png)' },
|
|
99
|
+
{ key: 'copy', label: '複製圖檔' },
|
|
100
|
+
]);
|
|
101
|
+
const contextMenuStyle = computed(() => ({
|
|
102
|
+
top: `${contextMenuY.value}px`,
|
|
103
|
+
left: `${contextMenuX.value}px`,
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
async function onClickAction(key: string) {
|
|
47
107
|
hide();
|
|
108
|
+
if (key === 'saveAs') {
|
|
109
|
+
const dataURL = await domToPngDataUrl(target.value, customFilter.value, customStyle.value, padding.value, dateRanges.value, watermark.value, WatermarkImage);
|
|
110
|
+
if (!dataURL) {
|
|
111
|
+
console.log("Can not generate data URL!");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
downloadURL(dataURL, `${filename.value}.png`);
|
|
115
|
+
} else if (key === 'copy') {
|
|
116
|
+
if (!navigator.clipboard?.write || !window.ClipboardItem) {
|
|
117
|
+
console.log("Can not copy image to clipboard!");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const dataURL = await domToPngDataUrl(target.value, customFilter.value, customStyle.value, padding.value, dateRanges.value, watermark.value, WatermarkImage);
|
|
122
|
+
if (!dataURL) {
|
|
123
|
+
console.log("Can not generate data URL!");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const blob = dataURLtoBlob(dataURL);
|
|
128
|
+
const data = [new window.ClipboardItem({ 'image/png': blob })];
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await navigator.clipboard.write(data);
|
|
132
|
+
copyMsg.value = '成功複製圖檔到剪貼簿';
|
|
133
|
+
isShowCopyMsg.value = true;
|
|
134
|
+
isCopySuccess.value = true;
|
|
135
|
+
} catch (e) {
|
|
136
|
+
copyMsg.value = '無法複製圖檔到剪貼簿';
|
|
137
|
+
isShowCopyMsg.value = true;
|
|
138
|
+
isCopySuccess.value = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
48
141
|
}
|
|
49
142
|
|
|
50
143
|
function show() {
|
|
51
|
-
|
|
144
|
+
isShow.value = true;
|
|
52
145
|
}
|
|
53
146
|
|
|
54
147
|
function hide() {
|
|
55
|
-
|
|
148
|
+
isShow.value = false;
|
|
56
149
|
}
|
|
57
150
|
|
|
58
151
|
function onRightClick(e: PointerEvent) {
|
|
59
|
-
|
|
60
|
-
if (!menuEl.value || !actions.value.length) {
|
|
152
|
+
if (!el.value || !el.value.parentElement) {
|
|
61
153
|
return;
|
|
62
154
|
}
|
|
63
155
|
|
|
64
156
|
e.preventDefault();
|
|
65
|
-
|
|
157
|
+
contextMenuX.value = e.clientX;
|
|
158
|
+
contextMenuY.value = e.clientY;
|
|
66
159
|
show();
|
|
67
160
|
}
|
|
68
161
|
|
|
69
|
-
function
|
|
162
|
+
function onClickModal() {
|
|
70
163
|
hide();
|
|
71
164
|
}
|
|
72
165
|
|
|
166
|
+
function timeoutClearMessage() {
|
|
167
|
+
if (clearMsgTimeoutId) {
|
|
168
|
+
clearTimeout(clearMsgTimeoutId);
|
|
169
|
+
}
|
|
170
|
+
clearMsgTimeoutId = setTimeout(() => {
|
|
171
|
+
copyMsg.value = '';
|
|
172
|
+
}, 300);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function subscribeScrollEvents() {
|
|
176
|
+
window.addEventListener('DOMMouseScroll', preventDefault, false); // older FF
|
|
177
|
+
window.addEventListener(wheelEvent, preventDefault, wheelOpt); // modern desktop
|
|
178
|
+
window.addEventListener('touchmove', preventDefault, wheelOpt); // mobile
|
|
179
|
+
window.addEventListener('keydown', preventDefaultForScrollKeys, false);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function unsubscribeScrollEvents() {
|
|
183
|
+
window.removeEventListener('DOMMouseScroll', preventDefault, false);
|
|
184
|
+
window.removeEventListener(wheelEvent, preventDefault, wheelOpt);
|
|
185
|
+
window.removeEventListener('touchmove', preventDefault, wheelOpt);
|
|
186
|
+
window.removeEventListener('keydown', preventDefaultForScrollKeys, false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
watch(isShow, () => {
|
|
190
|
+
if (isShow.value) {
|
|
191
|
+
subscribeScrollEvents();
|
|
192
|
+
} else {
|
|
193
|
+
unsubscribeScrollEvents();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
watch(isShowCopyMsg, () => {
|
|
198
|
+
if (!isShowCopyMsg.value) {
|
|
199
|
+
timeoutClearMessage();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
73
203
|
onMounted(() => {
|
|
74
204
|
if (el.value && el.value.parentElement) {
|
|
75
205
|
contextMenuSubscriber = fromEvent(el.value.parentElement, 'contextmenu').subscribe((e) => {
|
|
@@ -83,27 +213,44 @@ export default defineComponent({
|
|
|
83
213
|
contextMenuSubscriber.unsubscribe();
|
|
84
214
|
}
|
|
85
215
|
contextMenuSubscriber = null;
|
|
216
|
+
|
|
217
|
+
unsubscribeScroll();
|
|
86
218
|
});
|
|
87
219
|
|
|
88
220
|
return {
|
|
89
221
|
el,
|
|
90
|
-
|
|
91
|
-
|
|
222
|
+
isShow,
|
|
223
|
+
copyMsg,
|
|
224
|
+
isCopySuccess,
|
|
225
|
+
isShowCopyMsg,
|
|
226
|
+
contextMenuActions,
|
|
227
|
+
contextMenuStyle,
|
|
92
228
|
onClickAction,
|
|
93
|
-
|
|
229
|
+
onClickModal,
|
|
94
230
|
};
|
|
95
231
|
},
|
|
96
232
|
});
|
|
97
233
|
</script>
|
|
98
234
|
|
|
99
|
-
<style lang="stylus">
|
|
235
|
+
<style lang="stylus" scoped>
|
|
100
236
|
.sl-context-menu-wrapper
|
|
101
|
-
@apply
|
|
237
|
+
@apply hidden
|
|
238
|
+
|
|
239
|
+
.sl-context-menu-container
|
|
240
|
+
@apply fixed top-0 left-0 w-full h-full
|
|
241
|
+
|
|
242
|
+
&-modal
|
|
243
|
+
@apply absolute top-0 left-0 w-full h-full
|
|
244
|
+
|
|
245
|
+
.sl-context-menu
|
|
246
|
+
@apply absolute top-0 left-0 m-0 border border-primary-600 rounded overflow-hidden shadow-popup-sm
|
|
247
|
+
|
|
248
|
+
&-item
|
|
249
|
+
@apply block s('min-w-[160px]') p-3 bg-white text-base text-gray-2
|
|
250
|
+
@apply s('hover:bg-primary-100') s('hover:text-primary')
|
|
102
251
|
|
|
103
|
-
.
|
|
104
|
-
|
|
252
|
+
.toast.copy-msg-toast
|
|
253
|
+
:deep(.toast-wrapper)
|
|
254
|
+
@apply s('bg-black/80') shadow-popup-sm
|
|
105
255
|
|
|
106
|
-
&-item
|
|
107
|
-
@apply block w-auto s('min-w-[160px]') p-3 bg-white text-base text-gray-2
|
|
108
|
-
@apply s('hover:bg-primary-100') s('hover:text-primary')
|
|
109
256
|
</style>
|