@mptw/skylens-ui 1.4.86 → 1.4.88
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 +168 -46
- package/components/SLDownloadButton.vue +0 -17
- package/components/SLToast.vue +2 -1
- package/package.json +1 -1
- package/utils/index.ts +13 -1
|
@@ -1,75 +1,183 @@
|
|
|
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';
|
|
17
25
|
|
|
18
26
|
export default defineComponent({
|
|
19
27
|
name: 'SLContextMenu',
|
|
20
28
|
props: {
|
|
21
|
-
|
|
22
|
-
type:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
target: {
|
|
30
|
+
type: HTMLElement,
|
|
31
|
+
default: () => null,
|
|
32
|
+
},
|
|
33
|
+
filename: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: '',
|
|
36
|
+
},
|
|
37
|
+
dateRanges: {
|
|
38
|
+
type: Array as PropType<(IDateRange | null)[]>,
|
|
39
|
+
default: () => [],
|
|
40
|
+
},
|
|
41
|
+
padding: {
|
|
42
|
+
type: Number,
|
|
43
|
+
default: 0,
|
|
44
|
+
},
|
|
45
|
+
watermark: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: true,
|
|
48
|
+
},
|
|
49
|
+
customFilter: {
|
|
50
|
+
type: Function,
|
|
51
|
+
default: null,
|
|
52
|
+
},
|
|
53
|
+
customStyle: {
|
|
54
|
+
type: Object as PropType<Partial<CSSStyleDeclaration>>,
|
|
55
|
+
default: () => { },
|
|
33
56
|
},
|
|
34
57
|
},
|
|
35
58
|
emits: ['action'],
|
|
36
59
|
setup(props, { emit }) {
|
|
37
|
-
const {
|
|
60
|
+
const { target, filename, dateRanges, padding, watermark, customFilter, customStyle } = toRefs(props);
|
|
38
61
|
|
|
39
62
|
const el = shallowRef<HTMLDivElement | null>(null);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
63
|
+
const isShow = ref(false);
|
|
64
|
+
const contextMenuX = ref(0);
|
|
65
|
+
const contextMenuY = ref(0);
|
|
66
|
+
const isShowCopyMsg = ref(false);
|
|
67
|
+
const copyMsg = ref('');
|
|
68
|
+
const isCopySuccess = ref(false);
|
|
42
69
|
|
|
43
70
|
let contextMenuSubscriber: Subscription | null = null;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
let windowScrollSubscriber: Subscription | null = null;
|
|
72
|
+
let clearMsgTimeoutId: any = null;
|
|
73
|
+
|
|
74
|
+
const contextMenuActions = computed(() => [
|
|
75
|
+
{ key: 'saveAs', label: '另存圖檔(.png)' },
|
|
76
|
+
{ key: 'copy', label: '複製圖檔' },
|
|
77
|
+
]);
|
|
78
|
+
const contextMenuStyle = computed(() => ({
|
|
79
|
+
top: `${contextMenuY.value}px`,
|
|
80
|
+
left: `${contextMenuX.value}px`,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
async function onClickAction(key: string) {
|
|
47
84
|
hide();
|
|
85
|
+
if (key === 'saveAs') {
|
|
86
|
+
const dataURL = await domToPngDataUrl(target.value, customFilter.value, customStyle.value, padding.value, dateRanges.value, watermark.value, WatermarkImage);
|
|
87
|
+
if (!dataURL) {
|
|
88
|
+
console.log("Can not generate data URL!");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
downloadURL(dataURL, `${filename.value}.png`);
|
|
92
|
+
} else if (key === 'copy') {
|
|
93
|
+
if (!navigator.clipboard?.write || !window.ClipboardItem) {
|
|
94
|
+
console.log("Can not copy image to clipboard!");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const dataURL = await domToPngDataUrl(target.value, customFilter.value, customStyle.value, padding.value, dateRanges.value, watermark.value, WatermarkImage);
|
|
99
|
+
if (!dataURL) {
|
|
100
|
+
console.log("Can not generate data URL!");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const blob = dataURLtoBlob(dataURL);
|
|
105
|
+
const data = [new window.ClipboardItem({ 'image/png': blob })];
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await navigator.clipboard.write(data);
|
|
109
|
+
copyMsg.value = '成功複製圖檔到剪貼簿';
|
|
110
|
+
isShowCopyMsg.value = true;
|
|
111
|
+
isCopySuccess.value = true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
copyMsg.value = '無法複製圖檔到剪貼簿';
|
|
114
|
+
isShowCopyMsg.value = true;
|
|
115
|
+
isCopySuccess.value = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
48
118
|
}
|
|
49
119
|
|
|
50
120
|
function show() {
|
|
51
|
-
|
|
121
|
+
isShow.value = true;
|
|
52
122
|
}
|
|
53
123
|
|
|
54
124
|
function hide() {
|
|
55
|
-
|
|
125
|
+
isShow.value = false;
|
|
56
126
|
}
|
|
57
127
|
|
|
58
128
|
function onRightClick(e: PointerEvent) {
|
|
59
|
-
|
|
60
|
-
if (!menuEl.value || !actions.value.length) {
|
|
129
|
+
if (!el.value || !el.value.parentElement) {
|
|
61
130
|
return;
|
|
62
131
|
}
|
|
63
132
|
|
|
64
133
|
e.preventDefault();
|
|
65
|
-
|
|
134
|
+
contextMenuX.value = e.clientX;
|
|
135
|
+
contextMenuY.value = e.clientY;
|
|
66
136
|
show();
|
|
67
137
|
}
|
|
68
138
|
|
|
69
|
-
function
|
|
139
|
+
function onClickModal() {
|
|
70
140
|
hide();
|
|
71
141
|
}
|
|
72
142
|
|
|
143
|
+
function timeoutClearMessage() {
|
|
144
|
+
if (clearMsgTimeoutId) {
|
|
145
|
+
clearTimeout(clearMsgTimeoutId);
|
|
146
|
+
}
|
|
147
|
+
clearMsgTimeoutId = setTimeout(() => {
|
|
148
|
+
copyMsg.value = '';
|
|
149
|
+
}, 300);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function subscribeScroll() {
|
|
153
|
+
if (window) {
|
|
154
|
+
windowScrollSubscriber = fromEvent(window, 'scroll').subscribe((e: Event) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function unsubscribeScroll() {
|
|
161
|
+
if (windowScrollSubscriber) {
|
|
162
|
+
windowScrollSubscriber.unsubscribe();
|
|
163
|
+
windowScrollSubscriber = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
watch(isShow, () => {
|
|
168
|
+
if (isShow.value) {
|
|
169
|
+
subscribeScroll();
|
|
170
|
+
} else {
|
|
171
|
+
unsubscribeScroll();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
watch(isShowCopyMsg, () => {
|
|
176
|
+
if (!isShowCopyMsg.value) {
|
|
177
|
+
timeoutClearMessage();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
73
181
|
onMounted(() => {
|
|
74
182
|
if (el.value && el.value.parentElement) {
|
|
75
183
|
contextMenuSubscriber = fromEvent(el.value.parentElement, 'contextmenu').subscribe((e) => {
|
|
@@ -83,27 +191,41 @@ export default defineComponent({
|
|
|
83
191
|
contextMenuSubscriber.unsubscribe();
|
|
84
192
|
}
|
|
85
193
|
contextMenuSubscriber = null;
|
|
194
|
+
|
|
195
|
+
unsubscribeScroll();
|
|
86
196
|
});
|
|
87
197
|
|
|
88
198
|
return {
|
|
89
199
|
el,
|
|
90
|
-
|
|
91
|
-
|
|
200
|
+
isShow,
|
|
201
|
+
copyMsg,
|
|
202
|
+
isCopySuccess,
|
|
203
|
+
isShowCopyMsg,
|
|
204
|
+
contextMenuActions,
|
|
205
|
+
contextMenuStyle,
|
|
92
206
|
onClickAction,
|
|
93
|
-
|
|
207
|
+
onClickModal,
|
|
94
208
|
};
|
|
95
209
|
},
|
|
96
210
|
});
|
|
97
211
|
</script>
|
|
98
212
|
|
|
99
|
-
<style lang="stylus">
|
|
100
|
-
.sl-context-menu-
|
|
101
|
-
@apply
|
|
213
|
+
<style lang="stylus" scoped>
|
|
214
|
+
.sl-context-menu-container
|
|
215
|
+
@apply fixed top-0 left-0 w-full h-full
|
|
216
|
+
|
|
217
|
+
&-modal
|
|
218
|
+
@apply absolute top-0 left-0 w-full h-full
|
|
219
|
+
|
|
220
|
+
.sl-context-menu
|
|
221
|
+
@apply absolute top-0 left-0 m-0 border border-primary-600 rounded overflow-hidden shadow-popup-sm
|
|
222
|
+
|
|
223
|
+
&-item
|
|
224
|
+
@apply block s('min-w-[160px]') p-3 bg-white text-base text-gray-2
|
|
225
|
+
@apply s('hover:bg-primary-100') s('hover:text-primary')
|
|
102
226
|
|
|
103
|
-
.
|
|
104
|
-
|
|
227
|
+
.toast.copy-msg-toast
|
|
228
|
+
:deep(.toast-wrapper)
|
|
229
|
+
@apply s('bg-black/80') shadow-popup-sm
|
|
105
230
|
|
|
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
231
|
</style>
|
|
@@ -30,10 +30,8 @@ import type { PropType } from "vue";
|
|
|
30
30
|
import { fromEvent, Subscription } from "rxjs";
|
|
31
31
|
import { createPopper } from "@popperjs/core";
|
|
32
32
|
import type { Instance as PopperInstance, Placement } from "@popperjs/core";
|
|
33
|
-
import * as htmlToImage from "html-to-image";
|
|
34
33
|
import ExcelJS from "exceljs";
|
|
35
34
|
import {
|
|
36
|
-
formatDateRange,
|
|
37
35
|
downloadURL,
|
|
38
36
|
downloadBlob,
|
|
39
37
|
gaev,
|
|
@@ -243,20 +241,6 @@ export default defineComponent({
|
|
|
243
241
|
}
|
|
244
242
|
}
|
|
245
243
|
|
|
246
|
-
async function copyImageToClipboard() {
|
|
247
|
-
if (!navigator.clipboard) {
|
|
248
|
-
console.log("Can not copy image to clipboard!");
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
const dataURL = await domToPngDataUrl(imageTarget.value, customFilter.value, customStyle.value, padding.value, dateRanges.value, watermark.value, WatermarkImage);
|
|
252
|
-
if (!dataURL) {
|
|
253
|
-
console.log("Can not generate data URL!");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
await navigator.clipboard.writeText(dataURL);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
244
|
onMounted(() => {
|
|
261
245
|
nextTick(() => {
|
|
262
246
|
if (buttonEl.value && popupEl.value) {
|
|
@@ -280,7 +264,6 @@ export default defineComponent({
|
|
|
280
264
|
popupEl,
|
|
281
265
|
onClickedToggle,
|
|
282
266
|
onClickedOption,
|
|
283
|
-
copyImageToClipboard,
|
|
284
267
|
};
|
|
285
268
|
},
|
|
286
269
|
});
|
package/components/SLToast.vue
CHANGED
package/package.json
CHANGED
package/utils/index.ts
CHANGED
|
@@ -702,4 +702,16 @@ export const domToPngDataUrl = async (
|
|
|
702
702
|
dataURL = await addDateRanges(dataURL, [...dateRanges].reverse());
|
|
703
703
|
if (watermark) dataURL = await addWatermark(dataURL, watermarkImg);
|
|
704
704
|
return dataURL;
|
|
705
|
-
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export const dataURLtoBlob = (dataurl: string) => {
|
|
708
|
+
const arr = dataurl.split(',');
|
|
709
|
+
const mime = arr[0].match(/:(.*?);/)[1];
|
|
710
|
+
const bstr = atob(arr[1]);
|
|
711
|
+
let n = bstr.length;
|
|
712
|
+
const u8arr = new Uint8Array(n);
|
|
713
|
+
while (n--) {
|
|
714
|
+
u8arr[n] = bstr.charCodeAt(n);
|
|
715
|
+
}
|
|
716
|
+
return new Blob([u8arr], { type: mime });
|
|
717
|
+
}
|