@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.
@@ -1,75 +1,205 @@
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';
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
- 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
- }],
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
- const { actions } = toRefs(props);
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 menuWrapperEl = shallowRef<HTMLDivElement | null>(null);
41
- const menuEl = shallowRef<HTMLDivElement | null>(null);
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
- function onClickAction(key: string) {
46
- emit('action', key);
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
- menuWrapperEl.value?.classList.remove('hidden');
144
+ isShow.value = true;
52
145
  }
53
146
 
54
147
  function hide() {
55
- menuWrapperEl.value?.classList.add('hidden');
148
+ isShow.value = false;
56
149
  }
57
150
 
58
151
  function onRightClick(e: PointerEvent) {
59
- console.log(menuEl.value, actions.value);
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
- menuEl.value.setAttribute('style', `top: ${e.clientY}px; left: ${e.clientX}px;`);
157
+ contextMenuX.value = e.clientX;
158
+ contextMenuY.value = e.clientY;
66
159
  show();
67
160
  }
68
161
 
69
- function onMouseDown() {
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
- menuWrapperEl,
91
- menuEl,
222
+ isShow,
223
+ copyMsg,
224
+ isCopySuccess,
225
+ isShowCopyMsg,
226
+ contextMenuActions,
227
+ contextMenuStyle,
92
228
  onClickAction,
93
- onMouseDown,
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 s('z-[9999]') fixed top-0 left-0 w-full h-full
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
- .sl-context-menu
104
- @apply absolute border border-primary-600 rounded overflow-hidden shadow-popup-sm
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>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mptw/skylens-ui",
3
3
  "type": "module",
4
- "version": "1.4.87",
4
+ "version": "1.4.89",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",