@waline/client 2.4.2 → 2.5.0

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 (47) hide show
  1. package/dist/component.esm.js +1 -1
  2. package/dist/component.esm.js.map +1 -1
  3. package/dist/component.js +1 -1
  4. package/dist/component.js.map +1 -1
  5. package/dist/legacy.d.ts +2 -0
  6. package/dist/legacy.js +1 -1
  7. package/dist/legacy.js.map +1 -1
  8. package/dist/pageview.cjs.js +1 -1
  9. package/dist/pageview.esm.js +1 -1
  10. package/dist/pageview.js +1 -1
  11. package/dist/shim.d.ts +2 -0
  12. package/dist/shim.esm.d.ts +2 -0
  13. package/dist/shim.esm.js +1 -1
  14. package/dist/shim.esm.js.map +1 -1
  15. package/dist/shim.js +1 -1
  16. package/dist/shim.js.map +1 -1
  17. package/dist/waline.cjs.d.ts +2 -0
  18. package/dist/waline.cjs.js +1 -1
  19. package/dist/waline.cjs.js.map +1 -1
  20. package/dist/waline.css +1 -1
  21. package/dist/waline.css.map +1 -1
  22. package/dist/waline.d.ts +2 -0
  23. package/dist/waline.esm.d.ts +2 -0
  24. package/dist/waline.esm.js +1 -1
  25. package/dist/waline.esm.js.map +1 -1
  26. package/dist/waline.js +1 -1
  27. package/dist/waline.js.map +1 -1
  28. package/package.json +16 -15
  29. package/src/components/CommentBox.vue +123 -0
  30. package/src/components/Icons.ts +20 -0
  31. package/src/config/i18n/en.ts +2 -0
  32. package/src/config/i18n/generate.ts +2 -0
  33. package/src/config/i18n/jp.ts +8 -0
  34. package/src/config/i18n/pt-BR.ts +8 -0
  35. package/src/config/i18n/ru.ts +8 -0
  36. package/src/config/i18n/vi-VN.ts +8 -0
  37. package/src/config/i18n/zh-CN.ts +2 -0
  38. package/src/config/i18n/zh-TW.ts +2 -0
  39. package/src/init.ts +7 -2
  40. package/src/styles/card.scss +1 -0
  41. package/src/styles/gif.scss +151 -0
  42. package/src/styles/panel.scss +2 -0
  43. package/src/typings/locale.ts +2 -0
  44. package/src/utils/fetchGif.ts +62 -0
  45. package/src/utils/index.ts +2 -0
  46. package/src/utils/throttle.ts +16 -0
  47. package/LICENSE +0 -339
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/client",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "client for waline comment system",
5
5
  "keywords": [
6
6
  "valine",
@@ -56,6 +56,15 @@
56
56
  "dist",
57
57
  "src"
58
58
  ],
59
+ "scripts": {
60
+ "build": "pnpm rollup && pnpm style",
61
+ "clean": "rimraf ./dist",
62
+ "dev": "vite -c config/vite.config.js",
63
+ "lint": "eslint --ext .ts,.vue .",
64
+ "prepublish": "pnpm clean && pnpm build",
65
+ "rollup": "rollup -c ./config/rollup.config.js",
66
+ "style": "sass ./src/styles/index.scss ./dist/waline.css --style=compressed"
67
+ },
59
68
  "browserslist": {
60
69
  "production": [
61
70
  ">0.5%",
@@ -85,27 +94,19 @@
85
94
  "@rollup/plugin-replace": "^4.0.0",
86
95
  "@types/autosize": "^4.0.1",
87
96
  "@types/marked": "^4.0.3",
88
- "@types/node": "^17.0.31",
89
- "@vitejs/plugin-vue": "^2.3.2",
97
+ "@types/node": "^17.0.33",
98
+ "@vitejs/plugin-vue": "^2.3.3",
90
99
  "@vue/compiler-sfc": "^3.2.33",
100
+ "@yeger/vue-masonry-wall": "^3.0.31",
91
101
  "rimraf": "^3.0.2",
92
- "rollup": "^2.72.1",
102
+ "rollup": "^2.73.0",
93
103
  "rollup-plugin-dts": "^4.2.1",
94
104
  "rollup-plugin-terser": "^7.0.2",
95
105
  "rollup-plugin-ts": "^2.0.7",
96
106
  "typescript": "^4.6.4",
97
- "vite": "^2.9.8"
107
+ "vite": "^2.9.9"
98
108
  },
99
109
  "engines": {
100
110
  "node": ">=14"
101
- },
102
- "scripts": {
103
- "build": "pnpm rollup && pnpm style",
104
- "clean": "rimraf ./dist",
105
- "dev": "vite -c config/vite.config.js",
106
- "lint": "eslint --ext .ts,.vue .",
107
- "prepublish": "pnpm clean && pnpm build",
108
- "rollup": "rollup -c ./config/rollup.config.js",
109
- "style": "sass ./src/styles/index.scss ./dist/waline.css --style=compressed"
110
111
  }
111
- }
112
+ }
@@ -88,6 +88,16 @@
88
88
  <EmojiIcon />
89
89
  </button>
90
90
 
91
+ <button
92
+ ref="gifButtonRef"
93
+ class="wl-action"
94
+ :class="{ actived: showGif }"
95
+ :title="locale.gif"
96
+ @click="showGif = !showGif"
97
+ >
98
+ <GifIcon />
99
+ </button>
100
+
91
101
  <input
92
102
  ref="imageUploadRef"
93
103
  class="upload"
@@ -152,6 +162,47 @@
152
162
  </button>
153
163
  </div>
154
164
 
165
+ <div
166
+ ref="gifPopupRef"
167
+ class="wl-gif-popup"
168
+ :class="{ display: showGif }"
169
+ >
170
+ <input
171
+ type="text"
172
+ :placeholder="locale.gifSearchPlaceholder"
173
+ ref="gifSearchInputRef"
174
+ @input="onGifSearch"
175
+ />
176
+
177
+ <masonry-wall
178
+ class="wl-gif-waterfall"
179
+ :items="gifData.list"
180
+ :ssr-columns="2"
181
+ :column-width="200"
182
+ :gap="6"
183
+ @scroll="onGifMasonryScroll"
184
+ >
185
+ <template #default="{ item }">
186
+ <img
187
+ @click="insert(`![](${item.media[0].tinygif.url})`)"
188
+ :src="item.media[0].tinygif.url"
189
+ :title="item.title"
190
+ loading="lazy"
191
+ :style="{
192
+ width: '200px',
193
+ height:
194
+ (200 * item.media[0].tinygif.dims[1]) /
195
+ item.media[0].tinygif.dims[0] +
196
+ 'px',
197
+ }"
198
+ />
199
+ </template>
200
+ </masonry-wall>
201
+
202
+ <div v-if="gifData.loading" class="wl-loading">
203
+ <LoadingIcon :size="30" />
204
+ </div>
205
+ </div>
155
206
  <div
156
207
  ref="emojiPopupRef"
157
208
  class="wl-emoji-popup"
@@ -228,6 +279,7 @@ import {
228
279
  MarkdownIcon,
229
280
  PreviewIcon,
230
281
  LoadingIcon,
282
+ GifIcon,
231
283
  } from './Icons';
232
284
  import { useEditor, useUserMeta, useUserInfo } from '../composables';
233
285
  import {
@@ -237,6 +289,9 @@ import {
237
289
  parseEmoji,
238
290
  postComment,
239
291
  getEmojis,
292
+ fetchGif,
293
+ FetchGifResponse,
294
+ throttle,
240
295
  } from '../utils';
241
296
 
242
297
  import type { ComputedRef, DeepReadonly } from 'vue';
@@ -253,6 +308,7 @@ export default defineComponent({
253
308
  MarkdownIcon,
254
309
  PreviewIcon,
255
310
  LoadingIcon,
311
+ GifIcon,
256
312
  },
257
313
 
258
314
  props: {
@@ -286,13 +342,22 @@ export default defineComponent({
286
342
  const imageUploadRef = ref<HTMLInputElement | null>(null);
287
343
  const emojiButtonRef = ref<HTMLDivElement | null>(null);
288
344
  const emojiPopupRef = ref<HTMLDivElement | null>(null);
345
+ const gifButtonRef = ref<HTMLDivElement | null>(null);
346
+ const gifPopupRef = ref<HTMLDivElement | null>(null);
347
+ const gifSearchInputRef = ref<HTMLInputElement | null>(null);
289
348
 
290
349
  const emoji = ref<DeepReadonly<WalineEmojiConfig>>({ tabs: [], map: {} });
291
350
  const emojiTabIndex = ref(0);
292
351
  const showEmoji = ref(false);
352
+ const showGif = ref(false);
293
353
  const showPreview = ref(false);
294
354
  const previewText = ref('');
295
355
  const wordNumber = ref(0);
356
+ const gifData = ref<{
357
+ cursor: string;
358
+ loading: boolean;
359
+ list: FetchGifResponse['results'];
360
+ }>({ cursor: '', loading: true, list: [] });
296
361
 
297
362
  const wordLimit = ref(0);
298
363
  const isWordNumberLegal = ref(false);
@@ -548,6 +613,41 @@ export default defineComponent({
548
613
  !(emojiPopupRef.value as HTMLElement).contains(event.target as Node)
549
614
  )
550
615
  showEmoji.value = false;
616
+
617
+ if (
618
+ !(gifButtonRef.value as HTMLElement).contains(event.target as Node) &&
619
+ !(gifPopupRef.value as HTMLElement).contains(event.target as Node)
620
+ )
621
+ showGif.value = false;
622
+ };
623
+
624
+ const onGifSearch = throttle(async (event: Event) => {
625
+ gifData.value.cursor = '';
626
+ gifData.value.list = [];
627
+ onGifMasonryScroll(event);
628
+ });
629
+
630
+ const onGifMasonryScroll = async (event: Event): Promise<void> => {
631
+ const { scrollTop, clientHeight, scrollHeight } =
632
+ event.target as HTMLDivElement;
633
+ const percent = (clientHeight + scrollTop) / scrollHeight;
634
+ if (percent < 0.9 || gifData.value.loading) {
635
+ return;
636
+ }
637
+
638
+ gifData.value.loading = true;
639
+ const data = await fetchGif({
640
+ keyword: gifSearchInputRef.value?.value || '',
641
+ pos: gifData.value.cursor,
642
+ }).finally(() => {
643
+ gifData.value.loading = false;
644
+ });
645
+
646
+ gifData.value.cursor = data.next;
647
+ gifData.value.list = gifData.value.list.concat(data.results);
648
+ setTimeout(() => {
649
+ (event.target as HTMLDivElement).scrollTop = scrollTop;
650
+ }, 50);
551
651
  };
552
652
 
553
653
  // update wordNumber
@@ -575,6 +675,20 @@ export default defineComponent({
575
675
  { immediate: true }
576
676
  );
577
677
 
678
+ watch([showGif], async ([showGif]) => {
679
+ if (!showGif) {
680
+ return;
681
+ }
682
+
683
+ gifData.value.loading = true;
684
+ const data = await fetchGif({ keyword: '' }).finally(() => {
685
+ gifData.value.loading = false;
686
+ });
687
+
688
+ gifData.value.cursor = data.next;
689
+ gifData.value.list = gifData.value.list.concat(data.results);
690
+ });
691
+
578
692
  onMounted(() => {
579
693
  document.body.addEventListener('click', popupHandler);
580
694
 
@@ -632,6 +746,8 @@ export default defineComponent({
632
746
  onLogout,
633
747
  onProfile,
634
748
  submitComment,
749
+ onGifMasonryScroll,
750
+ onGifSearch,
635
751
 
636
752
  isLogin,
637
753
  userInfo,
@@ -651,6 +767,9 @@ export default defineComponent({
651
767
  emojiTabIndex,
652
768
  showEmoji,
653
769
 
770
+ // gif
771
+ showGif,
772
+
654
773
  // image
655
774
  canUploadImage,
656
775
 
@@ -663,7 +782,11 @@ export default defineComponent({
663
782
  editorRef,
664
783
  emojiButtonRef,
665
784
  emojiPopupRef,
785
+ gifButtonRef,
786
+ gifPopupRef,
666
787
  imageUploadRef,
788
+ gifSearchInputRef,
789
+ gifData,
667
790
  };
668
791
  },
669
792
  });
@@ -148,3 +148,23 @@ export const LoadingIcon: FunctionalComponent<{ size: number }> = ({ size }) =>
148
148
  })
149
149
  )
150
150
  );
151
+
152
+ export const GifIcon: FunctionalComponent = () =>
153
+ h(
154
+ 'svg',
155
+ {
156
+ width: 24,
157
+ height: 24,
158
+ fill: 'currentcolor',
159
+ viewBox: '0 0 24 24',
160
+ },
161
+ [
162
+ h('path', {
163
+ style: 'transform: translateY(0.5px)',
164
+ d: 'M18.968 10.5H15.968V11.484H17.984V12.984H15.968V15H14.468V9H18.968V10.5V10.5ZM8.984 9C9.26533 9 9.49967 9.09367 9.687 9.281C9.87433 9.46833 9.968 9.70267 9.968 9.984V10.5H6.499V13.5H8.468V12H9.968V14.016C9.968 14.2973 9.87433 14.5317 9.687 14.719C9.49967 14.9063 9.26533 15 8.984 15H5.984C5.70267 15 5.46833 14.9063 5.281 14.719C5.09367 14.5317 5 14.2973 5 14.016V9.985C5 9.70367 5.09367 9.46933 5.281 9.282C5.46833 9.09467 5.70267 9.001 5.984 9.001H8.984V9ZM11.468 9H12.968V15H11.468V9V9Z',
165
+ }),
166
+ h('path', {
167
+ d: 'M18.5 3H5.75C3.6875 3 2 4.6875 2 6.75V18C2 20.0625 3.6875 21.75 5.75 21.75H18.5C20.5625 21.75 22.25 20.0625 22.25 18V6.75C22.25 4.6875 20.5625 3 18.5 3ZM20.75 18C20.75 19.2375 19.7375 20.25 18.5 20.25H5.75C4.5125 20.25 3.5 19.2375 3.5 18V6.75C3.5 5.5125 4.5125 4.5 5.75 4.5H18.5C19.7375 4.5 20.75 5.5125 20.75 6.75V18Z',
168
+ }),
169
+ ]
170
+ );
@@ -39,4 +39,6 @@ export default generateLocale([
39
39
  'Wizards',
40
40
  'Elves',
41
41
  'Maiar',
42
+ 'GIF',
43
+ 'Search GIF',
42
44
  ]);
@@ -39,6 +39,8 @@ const localeKeys = [
39
39
  'level3',
40
40
  'level4',
41
41
  'level5',
42
+ 'gif',
43
+ 'gifSearchPlaceholder',
42
44
  ];
43
45
 
44
46
  export const generateLocale = (locale: string[]): WalineLocale =>
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'ワード',
34
34
  'コメントは $0 から $1 ワードの間でなければなりません!\n 現在の単語番号: $2',
35
35
  '匿名',
36
+ 'うえにん',
37
+ 'なかにん',
38
+ 'しもおし',
39
+ '特にしもおし',
40
+ 'かげ',
41
+ 'なぬし',
42
+ 'GIF',
43
+ '探す GIF',
36
44
  ]);
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'Palavras',
34
34
  'Favor enviar comentário com $0 a $1 palavras!\n Número de palavras atuais: $2',
35
35
  'Anônimo',
36
+ 'Dwarves',
37
+ 'Hobbits',
38
+ 'Ents',
39
+ 'Wizards',
40
+ 'Elves',
41
+ 'Maiar',
42
+ 'GIF',
43
+ 'Pesquisar GIF',
36
44
  ]);
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'Слова',
34
34
  'Пожалуйста, введите комментарии от $0 до $1 слов!\nНомер текущего слова: $2',
35
35
  'Анонимный',
36
+ 'Dwarves',
37
+ 'Hobbits',
38
+ 'Ents',
39
+ 'Wizards',
40
+ 'Elves',
41
+ 'Maiar',
42
+ 'GIF',
43
+ 'Поиск GIF',
36
44
  ]);
@@ -33,4 +33,12 @@ export default generateLocale([
33
33
  'từ',
34
34
  'Please input comments between $0 and $1 words!\n Current word number: $2',
35
35
  'Vô danh',
36
+ 'Dwarves',
37
+ 'Hobbits',
38
+ 'Ents',
39
+ 'Wizards',
40
+ 'Elves',
41
+ 'Maiar',
42
+ 'GIF',
43
+ 'Tìm kiếm GIF',
36
44
  ]);
@@ -39,4 +39,6 @@ export default generateLocale([
39
39
  '活跃',
40
40
  '话痨',
41
41
  '传说',
42
+ '表情包',
43
+ '搜索表情包',
42
44
  ]);
@@ -39,4 +39,6 @@ export default generateLocale([
39
39
  '活躍',
40
40
  '話癆',
41
41
  '傳說',
42
+ '表情包',
43
+ '搜索表情包',
42
44
  ]);
package/src/init.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createApp, h, reactive, watchEffect } from 'vue';
2
+ import MasonryWall from '@yeger/vue-masonry-wall';
2
3
 
3
4
  import Waline from './components/Waline.vue';
4
5
  import { commentCount } from './comment';
@@ -80,8 +81,12 @@ export const init = ({
80
81
  ? createApp(() => h(Waline, { path: state.path, ...props }))
81
82
  : null;
82
83
 
83
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
84
- if (app) app.mount(root!);
84
+ if (app) {
85
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
86
+ app.use(MasonryWall);
87
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
88
+ app.mount(root!);
89
+ }
85
90
 
86
91
  updateCommentCount();
87
92
  updatePageviewCount();
@@ -137,6 +137,7 @@
137
137
  .wl-reply {
138
138
  display: inline-flex;
139
139
  align-items: center;
140
+
140
141
  padding: 4px;
141
142
  border: none;
142
143
 
@@ -0,0 +1,151 @@
1
+ @mixin gif() {
2
+ .wl-gif-popup {
3
+ position: absolute;
4
+ top: 100%;
5
+ left: 1.25em;
6
+ z-index: 10;
7
+
8
+ width: 436px;
9
+ padding: 12px 12px 4px;
10
+ border: var(--waline-border);
11
+ border-radius: 6px;
12
+
13
+ background: var(--waline-bgcolor);
14
+ box-shadow: var(--waline-box-shadow);
15
+
16
+ opacity: 0;
17
+ visibility: hidden;
18
+
19
+ transition: transform 0.2s ease-out, opacity 0.2s ease-out;
20
+ transform: scale(0.9, 0.9);
21
+ transform-origin: 0 0;
22
+
23
+ &.display {
24
+ opacity: 1;
25
+ visibility: visible;
26
+ transform: none;
27
+ }
28
+
29
+ input {
30
+ box-sizing: border-box;
31
+
32
+ width: 100%;
33
+ margin-bottom: 10px;
34
+ padding: 3px 5px;
35
+ border: var(--waline-border);
36
+ }
37
+
38
+ img {
39
+ display: block;
40
+
41
+ box-sizing: border-box;
42
+
43
+ width: 100%;
44
+ border-width: 2px;
45
+ border-style: solid;
46
+ border-color: #fff;
47
+
48
+ cursor: pointer;
49
+
50
+ &:hover {
51
+ border-color: var(--waline-theme-color);
52
+ border-radius: 2px;
53
+ }
54
+ }
55
+
56
+ .wl-gif-waterfall {
57
+ overflow-y: auto;
58
+ max-height: 800px;
59
+ }
60
+
61
+ button {
62
+ display: inline-block;
63
+ vertical-align: middle;
64
+
65
+ width: 2em;
66
+ margin: 0.125em;
67
+ padding: 0;
68
+ border-width: 0;
69
+
70
+ background: transparent;
71
+
72
+ font-size: inherit;
73
+ line-height: 2;
74
+ text-align: center;
75
+
76
+ cursor: pointer;
77
+
78
+ &:hover {
79
+ background: var(--waline-bgcolor-hover);
80
+ }
81
+ }
82
+
83
+ .wl-gif {
84
+ display: inline-block;
85
+ vertical-align: middle;
86
+ max-width: 1.5em;
87
+ max-height: 1.5em;
88
+ }
89
+
90
+ .wl-tab-wrapper {
91
+ overflow-y: auto;
92
+ max-height: 145px;
93
+ padding: 0.5em;
94
+
95
+ &::-webkit-scrollbar {
96
+ width: 6px;
97
+ height: 6px;
98
+ }
99
+
100
+ &::-webkit-scrollbar-track-piece:vertical {
101
+ -webkit-border-radius: 6px;
102
+ border-radius: 6px;
103
+ background: rgb(0 0 0 / 10%);
104
+ }
105
+
106
+ &::-webkit-scrollbar-thumb:vertical {
107
+ width: 6px;
108
+ -webkit-border-radius: 6px;
109
+ border-radius: 6px;
110
+ background: var(--waline-theme-color);
111
+ }
112
+ }
113
+
114
+ .wl-tabs {
115
+ position: relative;
116
+ height: 2em;
117
+ padding: 0 6px 1px;
118
+
119
+ &::before {
120
+ content: ' ';
121
+
122
+ position: absolute;
123
+ top: 0;
124
+ right: 0;
125
+ left: 0;
126
+ z-index: 2;
127
+
128
+ height: 1px;
129
+
130
+ background: var(--waline-border-color);
131
+ }
132
+ }
133
+
134
+ .wl-tab {
135
+ position: relative;
136
+ margin: 0;
137
+ padding: 0 0.5em;
138
+
139
+ &.active {
140
+ z-index: 3;
141
+
142
+ border: 1px solid var(--waline-border-color);
143
+ border-top-width: 0;
144
+ border-bottom-right-radius: 6px;
145
+ border-bottom-left-radius: 6px;
146
+
147
+ background: var(--waline-bgcolor);
148
+ }
149
+ }
150
+ }
151
+ }
@@ -1,4 +1,5 @@
1
1
  @use 'emoji';
2
+ @use 'gif';
2
3
 
3
4
  .wl-comment {
4
5
  position: relative;
@@ -288,3 +289,4 @@
288
289
  }
289
290
 
290
291
  @include emoji.emoji;
292
+ @include gif.gif;
@@ -36,4 +36,6 @@ export interface WalineLocale extends WalineDateLocale, WalineLevelLocale {
36
36
  word: string;
37
37
  wordHint: string;
38
38
  anonymous: string;
39
+ gif: string;
40
+ gifSearchPlaceholder: string;
39
41
  }
@@ -0,0 +1,62 @@
1
+ export type GifFormat =
2
+ | 'gif'
3
+ | 'mediumgif'
4
+ | 'tinygif'
5
+ | 'nanogif'
6
+ | 'mp4'
7
+ | 'loopedmp4'
8
+ | 'tinymp4'
9
+ | 'nanomp4'
10
+ | 'webm'
11
+ | 'tinywebm'
12
+ | 'nanowebm';
13
+ export interface MediaObject {
14
+ preview: string;
15
+ url: string;
16
+ dims: number[];
17
+ size: number;
18
+ }
19
+
20
+ export interface FetchGifRequest {
21
+ key?: string;
22
+ keyword: string;
23
+ pos?: string;
24
+ limit?: number;
25
+ }
26
+
27
+ export interface GifObject {
28
+ created: number;
29
+ hasaudio: boolean;
30
+ id: string;
31
+ media: Record<GifFormat, MediaObject>[];
32
+ tags: string[];
33
+ title: string;
34
+ itemurl: string;
35
+ hascaption: boolean;
36
+ url: string;
37
+ }
38
+
39
+ export interface FetchGifResponse {
40
+ next: string;
41
+ results: GifObject[];
42
+ }
43
+
44
+ export const fetchGif = ({
45
+ key,
46
+ keyword,
47
+ pos,
48
+ limit,
49
+ }: FetchGifRequest): Promise<FetchGifResponse> => {
50
+ const baseUrl = `https://g.tenor.com/v1/search`;
51
+ const query = new URLSearchParams('media_filter=minimal');
52
+ query.set('key', key || 'PAY5JLFIH6V6');
53
+ query.set('limit', (limit || 20).toString());
54
+ query.set('pos', pos || '');
55
+ query.set('q', keyword);
56
+
57
+ return fetch(`${baseUrl}?${query.toString()}`, {
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ }).then((resp) => resp.json() as Promise<FetchGifResponse>);
62
+ };
@@ -10,3 +10,5 @@ export * from './markdown';
10
10
  export * from './path';
11
11
  export * from './query';
12
12
  export * from './wordCount';
13
+ export * from './fetchGif';
14
+ export * from './throttle';
@@ -0,0 +1,16 @@
1
+ export function throttle(
2
+ func: (...args: any[]) => void,
3
+ timeout = 300
4
+ ): (...args: []) => void {
5
+ let timer: number | null;
6
+ return (...args: []): void => {
7
+ if (timer) {
8
+ clearTimeout(timer);
9
+ }
10
+
11
+ timer = window.setTimeout(() => {
12
+ func(...args);
13
+ timer = null;
14
+ }, timeout);
15
+ };
16
+ }