@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.
@@ -1,75 +1,183 @@
1
1
  <template lang="pug">
2
- .sl-context-menu-avatar(ref='el')
3
- ClientOnly
4
- Teleport(to='body')
5
- .sl-context-menu-wrapper.hidden(ref='menuWrapperEl', @mousedown='onMouseDown')
6
- .sl-context-menu(ref='menuEl')
7
- a.sl-context-menu-item(
8
- v-for='action in actions',
9
- href='javascript:;',
10
- :key='action.key',
11
- @click='onClickAction(action.key)',
12
- ) {{ action.label }}
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
- actions: {
22
- type: Array as PropType<{
23
- key: string,
24
- label: string,
25
- }[]>,
26
- default: () => [{
27
- key: 'saveAs',
28
- label: '另存圖檔 (.png)',
29
- }, {
30
- key: 'copy',
31
- label: '複製圖檔',
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 { actions } = toRefs(props);
60
+ const { target, filename, dateRanges, padding, watermark, customFilter, customStyle } = toRefs(props);
38
61
 
39
62
  const el = shallowRef<HTMLDivElement | null>(null);
40
- const menuWrapperEl = shallowRef<HTMLDivElement | null>(null);
41
- const menuEl = shallowRef<HTMLDivElement | null>(null);
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
- function onClickAction(key: string) {
46
- emit('action', key);
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
- menuWrapperEl.value?.classList.remove('hidden');
121
+ isShow.value = true;
52
122
  }
53
123
 
54
124
  function hide() {
55
- menuWrapperEl.value?.classList.add('hidden');
125
+ isShow.value = false;
56
126
  }
57
127
 
58
128
  function onRightClick(e: PointerEvent) {
59
- console.log(menuEl.value, actions.value);
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
- menuEl.value.setAttribute('style', `top: ${e.clientY}px; left: ${e.clientX}px;`);
134
+ contextMenuX.value = e.clientX;
135
+ contextMenuY.value = e.clientY;
66
136
  show();
67
137
  }
68
138
 
69
- function onMouseDown() {
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
- menuWrapperEl,
91
- menuEl,
200
+ isShow,
201
+ copyMsg,
202
+ isCopySuccess,
203
+ isShowCopyMsg,
204
+ contextMenuActions,
205
+ contextMenuStyle,
92
206
  onClickAction,
93
- onMouseDown,
207
+ onClickModal,
94
208
  };
95
209
  },
96
210
  });
97
211
  </script>
98
212
 
99
- <style lang="stylus">
100
- .sl-context-menu-wrapper
101
- @apply s('z-[9999]') fixed top-0 left-0 w-full h-full
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
- .sl-context-menu
104
- @apply absolute border border-primary-600 rounded overflow-hidden shadow-popup-sm
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
  });
@@ -1,7 +1,8 @@
1
1
  <template lang="pug">
2
2
  Transition(name='toast-fade')
3
3
  .toast(v-if='show')
4
- .toast-wrapper {{ message }}
4
+ .toast-wrapper
5
+ slot(name='message') {{ message }}
5
6
  </template>
6
7
 
7
8
  <script lang="ts">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mptw/skylens-ui",
3
3
  "type": "module",
4
- "version": "1.4.86",
4
+ "version": "1.4.88",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",
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
+ }