@waline/client 2.14.0 → 2.14.2

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.
Files changed (62) hide show
  1. package/dist/comment.cjs +1 -1
  2. package/dist/comment.cjs.map +1 -1
  3. package/dist/comment.js +1 -68
  4. package/dist/comment.js.map +1 -1
  5. package/dist/comment.mjs +1 -1
  6. package/dist/comment.mjs.map +1 -1
  7. package/dist/component.mjs +1 -1
  8. package/dist/component.mjs.map +1 -1
  9. package/dist/legacy.umd.d.ts +14 -5
  10. package/dist/legacy.umd.js +1 -1
  11. package/dist/legacy.umd.js.map +1 -1
  12. package/dist/pageview.cjs +1 -1
  13. package/dist/pageview.cjs.map +1 -1
  14. package/dist/pageview.js +1 -121
  15. package/dist/pageview.js.map +1 -1
  16. package/dist/pageview.mjs +1 -1
  17. package/dist/pageview.mjs.map +1 -1
  18. package/dist/shim.cjs +1 -1
  19. package/dist/shim.cjs.map +1 -1
  20. package/dist/shim.d.cts +15 -6
  21. package/dist/shim.d.mts +15 -6
  22. package/dist/shim.mjs +1 -1
  23. package/dist/shim.mjs.map +1 -1
  24. package/dist/waline.cjs +1 -1
  25. package/dist/waline.cjs.map +1 -1
  26. package/dist/waline.css +1 -1
  27. package/dist/waline.css.map +1 -1
  28. package/dist/waline.d.cts +15 -6
  29. package/dist/waline.d.mts +15 -6
  30. package/dist/waline.d.ts +15 -6
  31. package/dist/waline.js +1 -6787
  32. package/dist/waline.js.map +1 -1
  33. package/dist/waline.mjs +1 -1
  34. package/dist/waline.mjs.map +1 -1
  35. package/package.json +18 -18
  36. package/src/comment.ts +1 -2
  37. package/src/components/ArticleReaction.vue +120 -117
  38. package/src/components/CommentBox.vue +451 -488
  39. package/src/components/CommentCard.vue +109 -98
  40. package/src/components/ImageWall.vue +132 -131
  41. package/src/components/WalineComment.vue +683 -0
  42. package/src/composables/index.ts +1 -2
  43. package/src/composables/reaction.ts +16 -0
  44. package/src/composables/recaptchaV3.ts +4 -6
  45. package/src/config/default.ts +5 -0
  46. package/src/{entrys → entries}/api.ts +0 -0
  47. package/src/{entrys → entries}/comment.ts +0 -0
  48. package/src/entries/components.ts +2 -0
  49. package/src/{entrys → entries}/full.ts +0 -0
  50. package/src/{entrys → entries}/init.ts +0 -0
  51. package/src/{entrys → entries}/legacy.ts +0 -0
  52. package/src/{entrys → entries}/pageview.ts +0 -0
  53. package/src/init.ts +1 -1
  54. package/src/styles/reaction.scss +27 -16
  55. package/src/typings/base.ts +5 -0
  56. package/src/typings/waline.ts +14 -5
  57. package/src/utils/config.ts +27 -5
  58. package/src/utils/image.ts +1 -1
  59. package/src/components/Waline.vue +0 -509
  60. package/src/composables/timeAgo.ts +0 -15
  61. package/src/composables/vote.ts +0 -20
  62. package/src/entrys/components.ts +0 -2
@@ -5,7 +5,12 @@
5
5
  class="wl-login-info"
6
6
  >
7
7
  <div class="wl-avatar">
8
- <button class="wl-logout-btn" :title="locale.logout" @click="onLogout">
8
+ <button
9
+ type="submit"
10
+ class="wl-logout-btn"
11
+ :title="locale.logout"
12
+ @click="onLogout"
13
+ >
9
14
  <CloseIcon :size="14" />
10
15
  </button>
11
16
 
@@ -19,6 +24,7 @@
19
24
  <img :src="userInfo.avatar" alt="avatar" />
20
25
  </a>
21
26
  </div>
27
+
22
28
  <a
23
29
  href="#"
24
30
  class="wl-login-nick"
@@ -32,7 +38,8 @@
32
38
  <div class="wl-panel">
33
39
  <div
34
40
  v-if="config.login !== 'force' && config.meta.length && !isLogin"
35
- :class="['wl-header', `item${config.meta.length}`]"
41
+ class="wl-header"
42
+ :class="`item${config.meta.length}`"
36
43
  >
37
44
  <div v-for="kind in config.meta" :key="kind" class="wl-header-item">
38
45
  <label
@@ -44,6 +51,7 @@
44
51
  : `(${locale.optional})`)
45
52
  "
46
53
  />
54
+
47
55
  <input
48
56
  :id="`wl-${kind}`"
49
57
  :ref="
@@ -52,7 +60,8 @@
52
60
  }
53
61
  "
54
62
  v-model="userMeta[kind]"
55
- :class="['wl-input', `wl-${kind}`]"
63
+ class="wl-input"
64
+ :class="`wl-${kind}`"
56
65
  :name="kind"
57
66
  :type="kind === 'mail' ? 'email' : 'text'"
58
67
  />
@@ -72,6 +81,7 @@
72
81
 
73
82
  <div v-show="showPreview" class="wl-preview">
74
83
  <hr />
84
+
75
85
  <h4>{{ locale.preview }}:</h4>
76
86
  <!-- eslint-disable-next-line vue/no-v-html -->
77
87
  <div class="wl-content" v-html="previewText" />
@@ -85,7 +95,7 @@
85
95
  aria-label="Markdown is supported"
86
96
  class="wl-action"
87
97
  target="_blank"
88
- rel="noreferrer"
98
+ rel="noopener noreferrer"
89
99
  >
90
100
  <MarkdownIcon />
91
101
  </a>
@@ -93,6 +103,7 @@
93
103
  <button
94
104
  v-show="emoji.tabs.length"
95
105
  ref="emojiButtonRef"
106
+ type="button"
96
107
  class="wl-action"
97
108
  :class="{ active: showEmoji }"
98
109
  :title="locale.emoji"
@@ -104,6 +115,7 @@
104
115
  <button
105
116
  v-if="config.search"
106
117
  ref="gifButtonRef"
118
+ type="button"
107
119
  class="wl-action"
108
120
  :class="{ active: showGif }"
109
121
  :title="locale.gif"
@@ -131,6 +143,7 @@
131
143
  </label>
132
144
 
133
145
  <button
146
+ type="button"
134
147
  class="wl-action"
135
148
  :class="{ active: showPreview }"
136
149
  :title="locale.preview"
@@ -157,6 +170,7 @@
157
170
 
158
171
  <button
159
172
  v-if="config.login !== 'disable' && !isLogin"
173
+ type="button"
160
174
  class="wl-btn"
161
175
  @click="onLogin"
162
176
  v-text="locale.login"
@@ -164,12 +178,14 @@
164
178
 
165
179
  <button
166
180
  v-if="config.login !== 'force' || isLogin"
167
- class="wl-btn primary"
181
+ type="submit"
182
+ class="primary wl-btn"
168
183
  title="Cmd|Ctrl + Enter"
169
184
  :disabled="isSubmitting"
170
185
  @click="submitComment"
171
186
  >
172
187
  <LoadingIcon v-if="isSubmitting" :size="16" />
188
+
173
189
  <template v-else>
174
190
  {{ locale.submit }}
175
191
  </template>
@@ -189,7 +205,7 @@
189
205
  />
190
206
 
191
207
  <ImageWall
192
- :items="gifData.list"
208
+ :items="searchResults.list"
193
209
  :column-width="200"
194
210
  :gap="6"
195
211
  @insert="insert($event)"
@@ -197,10 +213,11 @@
197
213
  >
198
214
  </ImageWall>
199
215
 
200
- <div v-if="gifData.loading" class="wl-loading">
216
+ <div v-if="searchResults.loading" class="wl-loading">
201
217
  <LoadingIcon :size="30" />
202
218
  </div>
203
219
  </div>
220
+
204
221
  <div
205
222
  ref="emojiPopupRef"
206
223
  class="wl-emoji-popup"
@@ -214,6 +231,7 @@
214
231
  <button
215
232
  v-for="key in emojiItem.items"
216
233
  :key="key"
234
+ type="button"
217
235
  :title="key"
218
236
  @click="insert(`:${key}:`)"
219
237
  >
@@ -228,10 +246,12 @@
228
246
  </button>
229
247
  </div>
230
248
  </template>
249
+
231
250
  <div v-if="emoji.tabs.length > 1" class="wl-tabs">
232
251
  <button
233
252
  v-for="(emojiItem, index) in emoji.tabs"
234
253
  :key="emojiItem.name"
254
+ type="button"
235
255
  class="wl-tab"
236
256
  :class="{ active: emojiTabIndex === index }"
237
257
  @click="emojiTabIndex = index"
@@ -252,26 +272,24 @@
252
272
 
253
273
  <button
254
274
  v-if="replyId || edit?.objectId"
275
+ type="button"
255
276
  class="wl-close"
256
277
  :title="locale.cancelReply"
257
- @click="$emit(replyId ? 'cancel-reply' : 'cancel-edit')"
278
+ @click="$emit(replyId ? 'cancelReply' : 'cancelEdit')"
258
279
  >
259
280
  <CloseIcon :size="24" />
260
281
  </button>
261
282
  </div>
262
283
  </template>
263
284
 
264
- <script lang="ts">
285
+ <script setup lang="ts">
265
286
  import { useDebounceFn } from '@vueuse/core';
266
- import { useReCaptcha } from '../composables';
267
287
  import autosize from 'autosize';
268
288
  import {
269
289
  computed,
270
- defineComponent,
271
290
  inject,
272
291
  onMounted,
273
292
  onUnmounted,
274
- PropType,
275
293
  reactive,
276
294
  ref,
277
295
  watch,
@@ -280,22 +298,27 @@ import {
280
298
  import {
281
299
  CloseIcon,
282
300
  EmojiIcon,
301
+ GifIcon,
283
302
  ImageIcon,
303
+ LoadingIcon,
284
304
  MarkdownIcon,
285
305
  PreviewIcon,
286
- LoadingIcon,
287
- GifIcon,
288
306
  } from './Icons';
289
307
  import ImageWall from './ImageWall.vue';
290
- import { addComment, login, updateComment } from '../api';
291
- import { useEditor, useUserMeta, useUserInfo } from '../composables';
308
+ import { addComment, login, updateComment, UserInfo } from '../api/index.js';
309
+ import {
310
+ useEditor,
311
+ useReCaptcha,
312
+ useUserInfo,
313
+ useUserMeta,
314
+ } from '../composables/index.js';
292
315
  import {
293
316
  getEmojis,
294
- getImagefromDataTransfer,
317
+ getImageFromDataTransfer,
295
318
  getWordNumber,
296
319
  parseEmoji,
297
320
  parseMarkdown,
298
- } from '../utils';
321
+ } from '../utils/index.js';
299
322
 
300
323
  import type { ComputedRef, DeepReadonly } from 'vue';
301
324
  import type {
@@ -304,510 +327,450 @@ import type {
304
327
  WalineImageUploader,
305
328
  WalineSearchOptions,
306
329
  WalineSearchResult,
307
- } from '../typings';
308
- import type { WalineConfig, WalineEmojiConfig } from '../utils';
309
-
310
- export default defineComponent({
311
- name: 'CommentBox',
312
-
313
- components: {
314
- CloseIcon,
315
- EmojiIcon,
316
- ImageIcon,
317
- ImageWall,
318
- MarkdownIcon,
319
- PreviewIcon,
320
- LoadingIcon,
321
- GifIcon,
322
- },
323
-
324
- props: {
325
- rootId: {
326
- type: String,
327
- default: '',
328
- },
329
- replyId: {
330
- type: String,
331
- default: '',
332
- },
333
- replyUser: {
334
- type: String,
335
- default: '',
336
- },
337
- edit: {
338
- type: Object as PropType<WalineComment | null>,
339
- default: null,
340
- },
341
- },
342
-
343
- emits: ['submit', 'cancel-reply', 'cancel-edit'],
344
-
345
- setup(props, { emit }) {
346
- const config = inject<ComputedRef<WalineConfig>>('config')!;
347
-
348
- const editor = useEditor();
349
- const userMeta = useUserMeta();
350
- const userInfo = useUserInfo();
351
-
352
- const inputRefs = ref<Record<string, HTMLInputElement>>({});
353
- const editorRef = ref<HTMLTextAreaElement | null>(null);
354
- const imageUploadRef = ref<HTMLInputElement | null>(null);
355
- const emojiButtonRef = ref<HTMLDivElement | null>(null);
356
- const emojiPopupRef = ref<HTMLDivElement | null>(null);
357
- const gifButtonRef = ref<HTMLDivElement | null>(null);
358
- const gifPopupRef = ref<HTMLDivElement | null>(null);
359
- const gifSearchInputRef = ref<HTMLInputElement | null>(null);
360
-
361
- const emoji = ref<DeepReadonly<WalineEmojiConfig>>({ tabs: [], map: {} });
362
- const emojiTabIndex = ref(0);
363
- const showEmoji = ref(false);
364
- const showGif = ref(false);
365
- const showPreview = ref(false);
366
- const previewText = ref('');
367
- const wordNumber = ref(0);
368
-
369
- const searchResults = reactive({
370
- loading: true,
371
- list: [] as WalineSearchResult,
372
- });
373
-
374
- const wordLimit = ref(0);
375
- const isWordNumberLegal = ref(false);
376
-
377
- const content = ref('');
378
-
379
- const isSubmitting = ref(false);
380
-
381
- const locale = computed(() => config.value.locale);
382
-
383
- const isLogin = computed(() => Boolean(userInfo.value?.token));
384
-
385
- const canUploadImage = computed(() => config.value.imageUploader !== false);
386
-
387
- const insert = (content: string): void => {
388
- const textArea = editorRef.value!;
389
- const startPosition = textArea.selectionStart;
390
- const endPosition = textArea.selectionEnd || 0;
391
- const scrollTop = textArea.scrollTop;
392
-
393
- editor.value =
394
- textArea.value.substring(0, startPosition) +
395
- content +
396
- textArea.value.substring(endPosition, textArea.value.length);
397
- textArea.focus();
398
- textArea.selectionStart = startPosition + content.length;
399
- textArea.selectionEnd = startPosition + content.length;
400
- textArea.scrollTop = scrollTop;
401
- };
402
-
403
- const onKeyDown = (event: KeyboardEvent): void => {
404
- const key = event.key;
405
-
406
- // Shortcut key
407
- if ((event.ctrlKey || event.metaKey) && key === 'Enter') submitComment();
408
- };
409
-
410
- const uploadImage = (file: File): Promise<void> => {
411
- const uploadText = `![${config.value.locale.uploading} ${file.name}]()`;
412
-
413
- insert(uploadText);
414
-
415
- return Promise.resolve()
416
- .then(() => (config.value.imageUploader as WalineImageUploader)(file))
417
- .then((url) => {
418
- editor.value = editor.value.replace(
419
- uploadText,
420
- `\r\n![${file.name}](${url})`
421
- );
422
- })
423
- .catch((e) => {
424
- alert(e.message);
425
- editor.value = editor.value.replace(uploadText, '');
426
- });
427
- };
428
-
429
- const onDrop = (event: DragEvent): void => {
430
- if (event.dataTransfer?.items) {
431
- const file = getImagefromDataTransfer(event.dataTransfer.items);
432
-
433
- if (file && canUploadImage.value) {
434
- uploadImage(file);
435
- event.preventDefault();
436
- }
437
- }
438
- };
439
-
440
- const onPaste = (event: ClipboardEvent): void => {
441
- if (event.clipboardData) {
442
- const file = getImagefromDataTransfer(event.clipboardData.items);
443
-
444
- if (file && canUploadImage.value) uploadImage(file);
445
- }
446
- };
447
-
448
- const onChange = (): void => {
449
- const inputElement = imageUploadRef.value!;
450
-
451
- if (inputElement.files && canUploadImage.value)
452
- uploadImage(inputElement.files[0]).then(() => {
453
- // clear input so a same image can be uploaded later
454
- inputElement.value = '';
455
- });
456
- };
457
-
458
- const submitComment = async (): Promise<void> => {
459
- const { serverURL, lang, login, wordLimit, requiredMeta } = config.value;
460
-
461
- let token = '';
462
-
463
- if (config.value.recaptchaV3Key)
464
- token = await useReCaptcha(config.value.recaptchaV3Key).execute(
465
- 'social'
466
- );
467
-
468
- const comment: WalineCommentData = {
469
- comment: content.value,
470
- nick: userMeta.value.nick,
471
- mail: userMeta.value.mail,
472
- link: userMeta.value.link,
473
- ua: navigator.userAgent,
474
- url: config.value.path,
475
- recaptchaV3: token,
476
- };
477
-
478
- if (userInfo.value?.token) {
479
- // login user
480
-
481
- comment.nick = userInfo.value.display_name;
482
- comment.mail = userInfo.value.email;
483
- comment.link = userInfo.value.url;
484
- } else {
485
- if (login === 'force') return;
486
-
487
- // check nick
488
- if (requiredMeta.indexOf('nick') > -1 && !comment.nick) {
489
- inputRefs.value.nick?.focus();
490
-
491
- return alert(locale.value.nickError);
492
- }
493
-
494
- // check mail
495
- if (
496
- (requiredMeta.indexOf('mail') > -1 && !comment.mail) ||
497
- (comment.mail &&
498
- !/^\w(?:[\w._-]*\w)?@(?:\w(?:[\w-]*\w)?\.)*\w+$/.exec(comment.mail))
499
- ) {
500
- inputRefs.value.mail?.focus();
330
+ } from '../typings/index.js';
331
+ import type { WalineConfig, WalineEmojiConfig } from '../utils/index.js';
332
+
333
+ const props = withDefaults(
334
+ defineProps<{
335
+ /**
336
+ * Current comment to be edited
337
+ */
338
+ edit?: WalineComment | null;
339
+ /**
340
+ * Root comment id
341
+ */
342
+ rootId?: string;
343
+ /**
344
+ * Comment id to be replied
345
+ */
346
+ replyId?: string;
347
+ /**
348
+ * User name to be replied
349
+ */
350
+ replyUser?: string;
351
+ }>(),
352
+ {
353
+ edit: null,
354
+ rootId: '',
355
+ replyId: '',
356
+ replyUser: '',
357
+ }
358
+ );
359
+
360
+ const emit = defineEmits<{
361
+ (event: 'log'): void;
362
+ (event: 'cancelEdit'): void;
363
+ (event: 'cancelReply'): void;
364
+ (event: 'submit', comment: WalineComment): void;
365
+ }>();
366
+
367
+ defineExpose();
368
+
369
+ const config = inject<ComputedRef<WalineConfig>>('config')!;
370
+
371
+ const editor = useEditor();
372
+ const userMeta = useUserMeta();
373
+ const userInfo = useUserInfo();
374
+
375
+ const inputRefs = ref<Record<string, HTMLInputElement>>({});
376
+ const editorRef = ref<HTMLTextAreaElement | null>(null);
377
+ const imageUploadRef = ref<HTMLInputElement | null>(null);
378
+ const emojiButtonRef = ref<HTMLDivElement | null>(null);
379
+ const emojiPopupRef = ref<HTMLDivElement | null>(null);
380
+ const gifButtonRef = ref<HTMLDivElement | null>(null);
381
+ const gifPopupRef = ref<HTMLDivElement | null>(null);
382
+ const gifSearchInputRef = ref<HTMLInputElement | null>(null);
383
+
384
+ const emoji = ref<DeepReadonly<WalineEmojiConfig>>({ tabs: [], map: {} });
385
+ const emojiTabIndex = ref(0);
386
+ const showEmoji = ref(false);
387
+ const showGif = ref(false);
388
+ const showPreview = ref(false);
389
+ const previewText = ref('');
390
+ const wordNumber = ref(0);
391
+
392
+ const searchResults = reactive({
393
+ loading: true,
394
+ list: [] as WalineSearchResult,
395
+ });
501
396
 
502
- return alert(locale.value.mailError);
503
- }
397
+ const wordLimit = ref(0);
398
+ const isWordNumberLegal = ref(false);
504
399
 
505
- // check comment
506
- if (!comment.comment) {
507
- editorRef.value?.focus();
400
+ const content = ref('');
508
401
 
509
- return;
510
- }
402
+ const isSubmitting = ref(false);
511
403
 
512
- if (!comment.nick) comment.nick = locale.value.anonymous;
513
- }
404
+ const locale = computed(() => config.value.locale);
514
405
 
515
- if (!isWordNumberLegal.value)
516
- return alert(
517
- locale.value.wordHint
518
- .replace('$0', (wordLimit as [number, number])[0].toString())
519
- .replace('$1', (wordLimit as [number, number])[1].toString())
520
- .replace('$2', wordNumber.value.toString())
521
- );
406
+ const isLogin = computed(() => Boolean(userInfo.value?.token));
522
407
 
523
- comment.comment = parseEmoji(comment.comment, emoji.value.map);
408
+ const canUploadImage = computed(() => config.value.imageUploader !== false);
524
409
 
525
- if (props.replyId && props.rootId) {
526
- comment.pid = props.replyId;
527
- comment.rid = props.rootId;
528
- comment.at = props.replyUser;
529
- }
410
+ const insert = (content: string): void => {
411
+ const textArea = editorRef.value!;
412
+ const startPosition = textArea.selectionStart;
413
+ const endPosition = textArea.selectionEnd || 0;
414
+ const scrollTop = textArea.scrollTop;
530
415
 
531
- isSubmitting.value = true;
416
+ editor.value =
417
+ textArea.value.substring(0, startPosition) +
418
+ content +
419
+ textArea.value.substring(endPosition, textArea.value.length);
420
+ textArea.focus();
421
+ textArea.selectionStart = startPosition + content.length;
422
+ textArea.selectionEnd = startPosition + content.length;
423
+ textArea.scrollTop = scrollTop;
424
+ };
532
425
 
533
- const options = {
534
- serverURL,
535
- lang,
536
- token: userInfo.value?.token,
537
- comment,
538
- };
426
+ const onKeyDown = (event: KeyboardEvent): void => {
427
+ const key = event.key;
539
428
 
540
- (props.edit
541
- ? updateComment({ objectId: props.edit.objectId, ...options })
542
- : addComment(options)
543
- )
544
- .then((resp) => {
545
- isSubmitting.value = false;
429
+ // Shortcut key
430
+ if ((event.ctrlKey || event.metaKey) && key === 'Enter') void submitComment();
431
+ };
546
432
 
547
- if (resp.errmsg) return alert(resp.errmsg);
433
+ const uploadImage = (file: File): Promise<void> => {
434
+ const uploadText = `![${config.value.locale.uploading} ${file.name}]()`;
548
435
 
549
- emit('submit', resp.data!);
436
+ insert(uploadText);
550
437
 
551
- editor.value = '';
552
-
553
- previewText.value = '';
554
-
555
- if (props.replyId) emit('cancel-reply');
556
- if (props.edit?.objectId) emit('cancel-edit');
557
- })
558
- .catch((err: TypeError) => {
559
- isSubmitting.value = false;
438
+ return Promise.resolve()
439
+ .then(() => (config.value.imageUploader as WalineImageUploader)(file))
440
+ .then((url) => {
441
+ editor.value = editor.value.replace(
442
+ uploadText,
443
+ `\r\n![${file.name}](${url})`
444
+ );
445
+ })
446
+ .catch((err: Error) => {
447
+ alert(err.message);
448
+ editor.value = editor.value.replace(uploadText, '');
449
+ });
450
+ };
560
451
 
561
- alert(err.message);
562
- });
563
- };
452
+ const onDrop = (event: DragEvent): void => {
453
+ if (event.dataTransfer?.items) {
454
+ const file = getImageFromDataTransfer(event.dataTransfer.items);
564
455
 
565
- const onLogin = (event: Event): void => {
456
+ if (file && canUploadImage.value) {
457
+ void uploadImage(file);
566
458
  event.preventDefault();
567
- const { lang, serverURL } = config.value;
568
-
569
- login({
570
- serverURL,
571
- lang,
572
- }).then((data) => {
573
- userInfo.value = data;
574
- (data.remember ? localStorage : sessionStorage).setItem(
575
- 'WALINE_USER',
576
- JSON.stringify(data)
577
- );
578
- });
579
- };
459
+ }
460
+ }
461
+ };
580
462
 
581
- const onLogout = (): void => {
582
- userInfo.value = {};
583
- localStorage.setItem('WALINE_USER', 'null');
584
- sessionStorage.setItem('WALINE_USER', 'null');
585
- };
463
+ const onPaste = (event: ClipboardEvent): void => {
464
+ if (event.clipboardData) {
465
+ const file = getImageFromDataTransfer(event.clipboardData.items);
586
466
 
587
- const onProfile = (event: Event): void => {
588
- event.preventDefault();
467
+ if (file && canUploadImage.value) void uploadImage(file);
468
+ }
469
+ };
589
470
 
590
- const { lang, serverURL } = config.value;
471
+ const onChange = (): void => {
472
+ const inputElement = imageUploadRef.value!;
591
473
 
592
- const width = 800;
593
- const height = 800;
594
- const left = (window.innerWidth - width) / 2;
595
- const top = (window.innerHeight - height) / 2;
596
- const query = new URLSearchParams({
597
- lng: lang,
598
- token: userInfo.value!.token,
599
- });
600
- const handler = window.open(
601
- `${serverURL}/ui/profile?${query.toString()}`,
602
- '_blank',
603
- `width=${width},height=${height},left=${left},top=${top},scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no`
604
- );
605
-
606
- handler?.postMessage({ type: 'TOKEN', data: userInfo.value!.token }, '*');
607
- };
608
-
609
- const popupHandler = (event: MouseEvent): void => {
610
- if (
611
- !emojiButtonRef.value!.contains(event.target as Node) &&
612
- !emojiPopupRef.value!.contains(event.target as Node)
613
- )
614
- showEmoji.value = false;
615
-
616
- if (
617
- !gifButtonRef.value!.contains(event.target as Node) &&
618
- !gifPopupRef.value!.contains(event.target as Node)
619
- )
620
- showGif.value = false;
621
- };
622
-
623
- const onImageWallScroll = async (event: Event): Promise<void> => {
624
- const { scrollTop, clientHeight, scrollHeight } =
625
- event.target as HTMLDivElement;
626
- const percent = (clientHeight + scrollTop) / scrollHeight;
627
- const searchOptions = config.value.search as WalineSearchOptions;
628
- const keyword = gifSearchInputRef.value?.value || '';
629
-
630
- if (percent < 0.9 || searchResults.loading) return;
631
-
632
- searchResults.loading = true;
633
-
634
- searchResults.list = [
635
- ...searchResults.list,
636
- ...(searchOptions.more && searchResults.list.length
637
- ? await searchOptions.more(keyword, searchResults.list.length)
638
- : await searchOptions.search(keyword)),
639
- ];
640
-
641
- searchResults.loading = false;
642
-
643
- setTimeout(() => {
644
- (event.target as HTMLDivElement).scrollTop = scrollTop;
645
- }, 50);
646
- };
647
-
648
- const onGifSearch = useDebounceFn((event: Event) => {
649
- searchResults.list = [];
650
- onImageWallScroll(event);
651
- }, 300);
652
-
653
- // update wordNumber
654
- watch(
655
- [config, wordNumber],
656
- ([config, wordNumber]) => {
657
- const { wordLimit: limit } = config;
658
-
659
- if (limit) {
660
- if (wordNumber < limit[0] && limit[0] !== 0) {
661
- wordLimit.value = limit[0];
662
- isWordNumberLegal.value = false;
663
- } else if (wordNumber > limit[1]) {
664
- wordLimit.value = limit[1];
665
- isWordNumberLegal.value = false;
666
- } else {
667
- wordLimit.value = limit[1];
668
- isWordNumberLegal.value = true;
669
- }
670
- } else {
671
- wordLimit.value = 0;
672
- isWordNumberLegal.value = true;
673
- }
674
- },
675
- { immediate: true }
474
+ if (inputElement.files && canUploadImage.value)
475
+ void uploadImage(inputElement.files[0]).then(() => {
476
+ // clear input so a same image can be uploaded later
477
+ inputElement.value = '';
478
+ });
479
+ };
480
+
481
+ const submitComment = async (): Promise<void> => {
482
+ const { serverURL, lang, login, wordLimit, requiredMeta } = config.value;
483
+
484
+ let token = '';
485
+
486
+ if (config.value.recaptchaV3Key)
487
+ token = await useReCaptcha(config.value.recaptchaV3Key).execute('social');
488
+
489
+ const comment: WalineCommentData = {
490
+ comment: content.value,
491
+ nick: userMeta.value.nick,
492
+ mail: userMeta.value.mail,
493
+ link: userMeta.value.link,
494
+ ua: navigator.userAgent,
495
+ url: config.value.path,
496
+ recaptchaV3: token,
497
+ };
498
+
499
+ if (userInfo.value?.token) {
500
+ // login user
501
+
502
+ comment.nick = userInfo.value.display_name;
503
+ comment.mail = userInfo.value.email;
504
+ comment.link = userInfo.value.url;
505
+ } else {
506
+ if (login === 'force') return;
507
+
508
+ // check nick
509
+ if (requiredMeta.indexOf('nick') > -1 && !comment.nick) {
510
+ inputRefs.value.nick?.focus();
511
+
512
+ return alert(locale.value.nickError);
513
+ }
514
+
515
+ // check mail
516
+ if (
517
+ (requiredMeta.indexOf('mail') > -1 && !comment.mail) ||
518
+ (comment.mail &&
519
+ !/^\w(?:[\w._-]*\w)?@(?:\w(?:[\w-]*\w)?\.)*\w+$/.exec(comment.mail))
520
+ ) {
521
+ inputRefs.value.mail?.focus();
522
+
523
+ return alert(locale.value.mailError);
524
+ }
525
+
526
+ // check comment
527
+ if (!comment.comment) {
528
+ editorRef.value?.focus();
529
+
530
+ return;
531
+ }
532
+
533
+ if (!comment.nick) comment.nick = locale.value.anonymous;
534
+ }
535
+
536
+ if (!isWordNumberLegal.value)
537
+ return alert(
538
+ locale.value.wordHint
539
+ .replace('$0', (wordLimit as [number, number])[0].toString())
540
+ .replace('$1', (wordLimit as [number, number])[1].toString())
541
+ .replace('$2', wordNumber.value.toString())
676
542
  );
677
543
 
678
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
679
- const onMessageReceive = ({ data }: any): void => {
680
- if (!data || data.type !== 'profile') return;
544
+ comment.comment = parseEmoji(comment.comment, emoji.value.map);
681
545
 
682
- userInfo.value = { ...userInfo.value, ...data.data };
546
+ if (props.replyId && props.rootId) {
547
+ comment.pid = props.replyId;
548
+ comment.rid = props.rootId;
549
+ comment.at = props.replyUser;
550
+ }
683
551
 
684
- [localStorage, sessionStorage]
685
- .filter((store) => store.getItem('WALINE_USER'))
686
- .forEach((store) =>
687
- store.setItem('WALINE_USER', JSON.stringify(userInfo))
688
- );
689
- };
552
+ isSubmitting.value = true;
690
553
 
691
- onMounted(() => {
692
- document.body.addEventListener('click', popupHandler);
693
- window.addEventListener('message', onMessageReceive);
694
- if (props.edit?.objectId) {
695
- editor.value = props.edit.orig;
696
- }
697
-
698
- // watch gif
699
- watch(showGif, async (showGif) => {
700
- if (!showGif) return;
554
+ const options = {
555
+ serverURL,
556
+ lang,
557
+ token: userInfo.value?.token,
558
+ comment,
559
+ };
701
560
 
702
- const searchOptions = config.value.search as WalineSearchOptions;
561
+ void (
562
+ props.edit
563
+ ? updateComment({ objectId: props.edit.objectId, ...options })
564
+ : addComment(options)
565
+ )
566
+ .then((resp) => {
567
+ isSubmitting.value = false;
703
568
 
704
- // clear input
705
- if (gifSearchInputRef.value) gifSearchInputRef.value.value = '';
569
+ if (resp.errmsg) return alert(resp.errmsg);
706
570
 
707
- searchResults.loading = true;
571
+ emit('submit', resp.data!);
708
572
 
709
- searchResults.list = searchOptions.default
710
- ? await searchOptions.default()
711
- : await searchOptions.search('');
573
+ editor.value = '';
712
574
 
713
- searchResults.loading = false;
714
- });
575
+ previewText.value = '';
715
576
 
716
- // watch editor
717
- watch(
718
- () => editor.value,
719
- (value) => {
720
- const { highlighter, texRenderer } = config.value;
721
-
722
- content.value = value;
723
- previewText.value = parseMarkdown(value, {
724
- emojiMap: emoji.value.map,
725
- highlighter,
726
- texRenderer,
727
- });
728
- wordNumber.value = getWordNumber(value);
729
-
730
- if (value) autosize(editorRef.value!);
731
- else autosize.destroy(editorRef.value!);
732
- },
733
- { immediate: true }
734
- );
577
+ if (props.replyId) emit('cancelReply');
578
+ if (props.edit?.objectId) emit('cancelEdit');
579
+ })
580
+ .catch((err: TypeError) => {
581
+ isSubmitting.value = false;
735
582
 
736
- // watch emoji value change
737
- watch(
738
- () => config.value.emoji,
739
- (emojiConfig) =>
740
- getEmojis(Array.isArray(emojiConfig) ? emojiConfig : []).then(
741
- (config) => {
742
- emoji.value = config;
743
- }
744
- ),
745
- { immediate: true }
746
- );
583
+ alert(err.message);
747
584
  });
585
+ };
586
+
587
+ const onLogin = (event: Event): void => {
588
+ event.preventDefault();
589
+ const { lang, serverURL } = config.value;
590
+
591
+ void login({
592
+ serverURL,
593
+ lang,
594
+ }).then((data) => {
595
+ userInfo.value = data;
596
+ (data.remember ? localStorage : sessionStorage).setItem(
597
+ 'WALINE_USER',
598
+ JSON.stringify(data)
599
+ );
600
+ emit('log');
601
+ });
602
+ };
603
+
604
+ const onLogout = (): void => {
605
+ userInfo.value = {};
606
+ localStorage.setItem('WALINE_USER', 'null');
607
+ sessionStorage.setItem('WALINE_USER', 'null');
608
+ emit('log');
609
+ };
610
+
611
+ const onProfile = (event: Event): void => {
612
+ event.preventDefault();
613
+
614
+ const { lang, serverURL } = config.value;
615
+
616
+ const width = 800;
617
+ const height = 800;
618
+ const left = (window.innerWidth - width) / 2;
619
+ const top = (window.innerHeight - height) / 2;
620
+ const query = new URLSearchParams({
621
+ lng: lang,
622
+ token: userInfo.value.token,
623
+ });
624
+ const handler = window.open(
625
+ `${serverURL}/ui/profile?${query.toString()}`,
626
+ '_blank',
627
+ `width=${width},height=${height},left=${left},top=${top},scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no`
628
+ );
629
+
630
+ handler?.postMessage({ type: 'TOKEN', data: userInfo.value.token }, '*');
631
+ };
632
+
633
+ const popupHandler = (event: MouseEvent): void => {
634
+ if (
635
+ !emojiButtonRef.value?.contains(event.target as Node) &&
636
+ !emojiPopupRef.value?.contains(event.target as Node)
637
+ )
638
+ showEmoji.value = false;
639
+
640
+ if (
641
+ !gifButtonRef.value?.contains(event.target as Node) &&
642
+ !gifPopupRef.value?.contains(event.target as Node)
643
+ )
644
+ showGif.value = false;
645
+ };
646
+
647
+ const onImageWallScroll = async (event: Event): Promise<void> => {
648
+ const { scrollTop, clientHeight, scrollHeight } =
649
+ event.target as HTMLDivElement;
650
+ const percent = (clientHeight + scrollTop) / scrollHeight;
651
+ const searchOptions = config.value.search as WalineSearchOptions;
652
+ const keyword = gifSearchInputRef.value?.value || '';
653
+
654
+ if (percent < 0.9 || searchResults.loading) return;
655
+
656
+ searchResults.loading = true;
657
+
658
+ searchResults.list = [
659
+ ...searchResults.list,
660
+ ...(searchOptions.more && searchResults.list.length
661
+ ? await searchOptions.more(keyword, searchResults.list.length)
662
+ : await searchOptions.search(keyword)),
663
+ ];
664
+
665
+ searchResults.loading = false;
666
+
667
+ setTimeout(() => {
668
+ (event.target as HTMLDivElement).scrollTop = scrollTop;
669
+ }, 50);
670
+ };
671
+
672
+ const onGifSearch = useDebounceFn((event: Event) => {
673
+ searchResults.list = [];
674
+ void onImageWallScroll(event);
675
+ }, 300);
676
+
677
+ // update wordNumber
678
+ watch(
679
+ [config, wordNumber],
680
+ ([config, wordNumber]) => {
681
+ const { wordLimit: limit } = config;
682
+
683
+ if (limit) {
684
+ if (wordNumber < limit[0] && limit[0] !== 0) {
685
+ wordLimit.value = limit[0];
686
+ isWordNumberLegal.value = false;
687
+ } else if (wordNumber > limit[1]) {
688
+ wordLimit.value = limit[1];
689
+ isWordNumberLegal.value = false;
690
+ } else {
691
+ wordLimit.value = limit[1];
692
+ isWordNumberLegal.value = true;
693
+ }
694
+ } else {
695
+ wordLimit.value = 0;
696
+ isWordNumberLegal.value = true;
697
+ }
698
+ },
699
+ { immediate: true }
700
+ );
701
+
702
+ const onMessageReceive = ({
703
+ data,
704
+ }: {
705
+ data: { type: 'profile'; data: UserInfo };
706
+ }): void => {
707
+ if (!data || data.type !== 'profile') return;
708
+
709
+ userInfo.value = { ...userInfo.value, ...data.data };
710
+
711
+ [localStorage, sessionStorage]
712
+ .filter((store) => store.getItem('WALINE_USER'))
713
+ .forEach((store) => store.setItem('WALINE_USER', JSON.stringify(userInfo)));
714
+ };
715
+
716
+ onMounted(() => {
717
+ document.body.addEventListener('click', popupHandler);
718
+ window.addEventListener('message', onMessageReceive);
719
+ if (props.edit?.objectId) {
720
+ editor.value = props.edit.orig;
721
+ }
722
+
723
+ // watch gif
724
+ watch(showGif, async (showGif) => {
725
+ if (!showGif) return;
726
+
727
+ const searchOptions = config.value.search as WalineSearchOptions;
728
+
729
+ // clear input
730
+ if (gifSearchInputRef.value) gifSearchInputRef.value.value = '';
731
+
732
+ searchResults.loading = true;
733
+
734
+ searchResults.list = searchOptions.default
735
+ ? await searchOptions.default()
736
+ : await searchOptions.search('');
737
+
738
+ searchResults.loading = false;
739
+ });
740
+
741
+ // watch editor
742
+ watch(
743
+ () => editor.value,
744
+ (value) => {
745
+ const { highlighter, texRenderer } = config.value;
746
+
747
+ content.value = value;
748
+ previewText.value = parseMarkdown(value, {
749
+ emojiMap: emoji.value.map,
750
+ highlighter,
751
+ texRenderer,
752
+ });
753
+ wordNumber.value = getWordNumber(value);
748
754
 
749
- onUnmounted(() => {
750
- document.body.removeEventListener('click', popupHandler);
751
- window.removeEventListener('message', onMessageReceive);
752
- });
755
+ if (value) autosize(editorRef.value!);
756
+ else autosize.destroy(editorRef.value!);
757
+ },
758
+ { immediate: true }
759
+ );
760
+
761
+ // watch emoji value change
762
+ watch(
763
+ () => config.value.emoji,
764
+ (emojiConfig) =>
765
+ getEmojis(emojiConfig).then((config) => {
766
+ emoji.value = config;
767
+ }),
768
+ { immediate: true }
769
+ );
770
+ });
753
771
 
754
- return {
755
- // config
756
- config,
757
- locale,
758
-
759
- // events
760
- insert,
761
- onChange,
762
- onDrop,
763
- onKeyDown,
764
- onPaste,
765
- onLogin,
766
- onLogout,
767
- onProfile,
768
- submitComment,
769
- onImageWallScroll,
770
- onGifSearch,
771
-
772
- isLogin,
773
- userInfo,
774
- isSubmitting,
775
-
776
- // word
777
- wordNumber,
778
- wordLimit,
779
- isWordNumberLegal,
780
-
781
- // inputs
782
- editor,
783
- userMeta,
784
-
785
- // emoji
786
- emoji,
787
- emojiTabIndex,
788
- showEmoji,
789
-
790
- // gif
791
- gifData: searchResults,
792
- showGif,
793
-
794
- // image
795
- canUploadImage,
796
-
797
- // preview
798
- previewText,
799
- showPreview,
800
-
801
- // ref
802
- inputRefs,
803
- editorRef,
804
- emojiButtonRef,
805
- emojiPopupRef,
806
- gifButtonRef,
807
- gifPopupRef,
808
- imageUploadRef,
809
- gifSearchInputRef,
810
- };
811
- },
772
+ onUnmounted(() => {
773
+ document.body.removeEventListener('click', popupHandler);
774
+ window.removeEventListener('message', onMessageReceive);
812
775
  });
813
776
  </script>