artalk 2.2.12 → 2.3.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 (78) hide show
  1. package/dist/Artalk.css +1 -1
  2. package/dist/Artalk.es.js +1535 -1175
  3. package/dist/Artalk.iife.js +1 -1
  4. package/dist/Artalk.js +1 -1
  5. package/dist/ArtalkLite.css +1 -0
  6. package/dist/ArtalkLite.es.js +5123 -0
  7. package/dist/ArtalkLite.iife.js +1 -0
  8. package/dist/ArtalkLite.js +1 -0
  9. package/index.html +2 -2
  10. package/package.json +6 -5
  11. package/src/api/index.ts +20 -1
  12. package/src/api/request.ts +7 -7
  13. package/src/artalk.ts +117 -121
  14. package/src/comment/actions.ts +86 -0
  15. package/src/{components/html → comment}/comment.html +1 -1
  16. package/src/comment/comment.ts +191 -0
  17. package/src/comment/index.ts +3 -0
  18. package/src/comment/render.ts +425 -0
  19. package/src/components/action-btn.ts +6 -3
  20. package/src/components/dialog.ts +7 -3
  21. package/src/components/read-more-btn.ts +6 -2
  22. package/src/context.ts +204 -13
  23. package/src/defaults.ts +16 -5
  24. package/src/editor/editor.html +18 -0
  25. package/src/editor/editor.ts +419 -0
  26. package/src/editor/index.ts +3 -0
  27. package/src/editor/plugs/editor-plug.ts +51 -0
  28. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.less +2 -1
  29. package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
  30. package/src/editor/plugs/header-input-plug.ts +78 -0
  31. package/src/{components/editor-plugs → editor/plugs}/preview-plug.less +0 -0
  32. package/src/editor/plugs/preview-plug.ts +40 -0
  33. package/src/editor/plugs/upload-plug.ts +124 -0
  34. package/src/i18n/en.ts +78 -0
  35. package/src/i18n/index.ts +21 -0
  36. package/src/i18n/zh-CN.ts +82 -0
  37. package/src/layer/index.ts +4 -0
  38. package/src/{components → layer}/layer.ts +9 -9
  39. package/src/{components/html → layer}/sidebar-layer.html +0 -0
  40. package/src/{components → layer}/sidebar-layer.ts +17 -47
  41. package/src/lib/checker/admin-checker.ts +7 -7
  42. package/src/lib/checker/captcha-checker.ts +5 -6
  43. package/src/lib/checker/index.ts +22 -17
  44. package/src/lib/component.ts +7 -2
  45. package/src/lib/stat.ts +63 -0
  46. package/src/lib/ui.ts +6 -12
  47. package/src/lib/user.ts +20 -3
  48. package/src/lib/utils.ts +23 -9
  49. package/src/list/index.ts +3 -0
  50. package/src/{components → list}/list-lite.ts +80 -100
  51. package/src/{components → list}/list-nest.ts +0 -0
  52. package/src/{components/html → list}/list.html +3 -6
  53. package/src/{components → list}/list.ts +44 -42
  54. package/src/style/_variables.less +1 -2
  55. package/src/style/comment.less +26 -13
  56. package/src/style/editor.less +22 -14
  57. package/src/style/list.less +6 -9
  58. package/src/style/main.less +9 -8
  59. package/src/style/pagination.less +1 -1
  60. package/test/ArtalkCount.html +16 -0
  61. package/test/ArtalkI18n.html +15 -0
  62. package/test/ArtalkLite.html +15 -0
  63. package/types/artalk-config.d.ts +50 -32
  64. package/types/context.d.ts +128 -0
  65. package/types/event.d.ts +6 -66
  66. package/types/global.d.ts +1 -0
  67. package/types/plug.d.ts +4 -0
  68. package/{vite-common.config.ts → vite-base.config.ts} +6 -4
  69. package/vite-demo.config.ts +2 -4
  70. package/vite-lite.config.ts +22 -0
  71. package/vite.config.ts +9 -12
  72. package/src/components/_component.ts.txt +0 -10
  73. package/src/components/comment.ts +0 -605
  74. package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
  75. package/src/components/editor-plugs/editor-plug.ts +0 -20
  76. package/src/components/editor-plugs/preview-plug.ts +0 -48
  77. package/src/components/editor.ts +0 -607
  78. package/src/components/html/editor.html +0 -18
@@ -0,0 +1,419 @@
1
+ import '@/style/editor.less'
2
+
3
+ import { CommentData } from '~/types/artalk-data'
4
+ import Context from '~/types/context'
5
+ import Component from '../lib/component'
6
+ import * as Utils from '../lib/utils'
7
+ import * as Ui from '../lib/ui'
8
+
9
+ import EditorHTML from './editor.html?raw'
10
+
11
+ import EmoticonsPlug from './plugs/emoticons-plug'
12
+ import UploadPlug from './plugs/upload-plug'
13
+ import PreviewPlug from './plugs/preview-plug'
14
+ import EditorPlug from './plugs/editor-plug'
15
+ import HeaderInputPlug from './plugs/header-input-plug'
16
+
17
+ export default class Editor extends Component {
18
+ private get user() { return this.ctx.user }
19
+
20
+ public $header: HTMLElement
21
+ public $nick: HTMLInputElement
22
+ public $email: HTMLInputElement
23
+ public $link: HTMLInputElement
24
+ public get $inputs() {
25
+ return { nick: this.$nick, email: this.$email, link: this.$link }
26
+ }
27
+
28
+ public $textareaWrap: HTMLElement
29
+ public $textarea: HTMLTextAreaElement
30
+ public $bottom: HTMLElement
31
+ public $submitBtn: HTMLButtonElement
32
+ public $notifyWrap: HTMLElement
33
+
34
+ private replyComment: CommentData|null = null
35
+ private $sendReply: HTMLElement|null = null
36
+
37
+ private isTraveling = false
38
+
39
+ /** 启用的插件 */
40
+ private readonly ENABLED_PLUGS = [ EmoticonsPlug, UploadPlug, PreviewPlug, HeaderInputPlug ]
41
+ public plugList: { [name: string]: EditorPlug } = {}
42
+ private openedPlugName: string|null = null
43
+ public $plugPanelWrap: HTMLElement
44
+ public $plugBtnWrap: HTMLElement
45
+
46
+ public constructor(ctx: Context) {
47
+ super(ctx)
48
+
49
+ this.$el = Utils.createElement(EditorHTML)
50
+
51
+ this.$header = this.$el.querySelector('.atk-header')!
52
+ this.$nick = this.$header.querySelector('[name="nick"]')!
53
+ this.$email = this.$header.querySelector('[name="email"]')!
54
+ this.$link = this.$header.querySelector('[name="link"]')!
55
+
56
+ this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
57
+ this.$textarea = this.$el.querySelector('.atk-textarea')!
58
+ this.$bottom = this.$el.querySelector('.atk-bottom')!
59
+ this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
60
+ this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
61
+
62
+ this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
63
+ this.$plugPanelWrap = this.$el.querySelector('.atk-plug-panel-wrap')!
64
+
65
+ // 执行初始化
66
+ this.initLocalStorage()
67
+ this.initHeader()
68
+ this.initTextarea()
69
+ this.initPlugs()
70
+ this.initSubmitBtn()
71
+
72
+ // 监听事件
73
+ this.ctx.on('conf-updated', () => {})
74
+ }
75
+
76
+ private initLocalStorage() {
77
+ const localContent = window.localStorage.getItem('ArtalkContent') || ''
78
+ if (localContent.trim() !== '') {
79
+ this.showNotify(this.$t('restoredMsg'), 'i')
80
+ this.setContent(localContent)
81
+ }
82
+
83
+ this.$textarea.addEventListener('input', () => (this.saveToLocalStorage()))
84
+ }
85
+
86
+ private saveToLocalStorage() {
87
+ window.localStorage.setItem('ArtalkContent', this.getContentOriginal().trim())
88
+ }
89
+
90
+ private initHeader() {
91
+ Object.entries(this.$inputs).forEach(([key, $input]) => {
92
+ $input.value = this.user.data[key] || ''
93
+ $input.addEventListener('input', () => this.onHeaderInput(key, $input))
94
+
95
+ // 设置 Placeholder
96
+ $input.placeholder = `${this.$t(key as any)}`
97
+ })
98
+ }
99
+
100
+ private onHeaderInput(key: string, $input: HTMLInputElement) {
101
+ this.user.update({
102
+ [key]: $input.value.trim()
103
+ })
104
+
105
+ // 插件监听事件响应
106
+ Object.entries(this.plugList).forEach(([plugName, plug]) => {
107
+ if (plug.onHeaderInput) plug.onHeaderInput(key, $input)
108
+ })
109
+ }
110
+
111
+ private initTextarea() {
112
+ // 占位符
113
+ this.$textarea.placeholder = this.ctx.conf.placeholder || this.$t('placeholder')
114
+
115
+ // 修复按下 Tab 输入的内容
116
+ this.$textarea.addEventListener('keydown', (e) => {
117
+ const keyCode = e.keyCode || e.which
118
+
119
+ if (keyCode === 9) {
120
+ e.preventDefault()
121
+ this.insertContent('\t')
122
+ }
123
+ })
124
+
125
+ // 输入框高度随内容而变化
126
+ this.$textarea.addEventListener('input', () => {
127
+ this.adjustTextareaHeight()
128
+ })
129
+ }
130
+
131
+ private initSubmitBtn() {
132
+ this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
133
+ this.$submitBtn.addEventListener('click', () => (this.submit()))
134
+ }
135
+
136
+ /** 最终用于 submit 的数据 */
137
+ public getFinalContent() {
138
+ let content = this.getContentOriginal()
139
+
140
+ // 表情包处理
141
+ if (this.plugList.emoticons) {
142
+ content = (this.plugList.emoticons as EmoticonsPlug).transEmoticonImageText(content)
143
+ }
144
+
145
+ return content
146
+ }
147
+
148
+ public getContentOriginal() {
149
+ return this.$textarea.value || '' // Tip: !!"0" === true
150
+ }
151
+
152
+ public getContentMarked() {
153
+ return Utils.marked(this.ctx, this.getFinalContent())
154
+ }
155
+
156
+ public setContent(val: string) {
157
+ this.$textarea.value = val
158
+ this.saveToLocalStorage()
159
+ if (this.plugList.preview) {
160
+ ;(this.plugList.preview as PreviewPlug).updateContent()
161
+ }
162
+
163
+ // 延迟执行防止无效
164
+ window.setTimeout(() => {
165
+ this.adjustTextareaHeight()
166
+ }, 80)
167
+ }
168
+
169
+ public insertContent(val: string) {
170
+ if ((document as any).selection) {
171
+ this.$textarea.focus();
172
+ (document as any).selection.createRange().text = val
173
+ this.$textarea.focus()
174
+ } else if (this.$textarea.selectionStart || this.$textarea.selectionStart === 0) {
175
+ const sStart = this.$textarea.selectionStart
176
+ const sEnd = this.$textarea.selectionEnd
177
+ const sT = this.$textarea.scrollTop
178
+ this.setContent(this.$textarea.value.substring(0, sStart) + val + this.$textarea.value.substring(sEnd, this.$textarea.value.length))
179
+ this.$textarea.focus()
180
+ this.$textarea.selectionStart = sStart + val.length
181
+ this.$textarea.selectionEnd = sStart + val.length
182
+ this.$textarea.scrollTop = sT
183
+ } else {
184
+ this.$textarea.focus()
185
+ this.$textarea.value += val
186
+ }
187
+ }
188
+
189
+ public clearEditor() {
190
+ this.setContent('')
191
+ this.cancelReply()
192
+ }
193
+
194
+ private adjustTextareaHeight() {
195
+ const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
196
+ this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
197
+ this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
198
+ }
199
+
200
+ public setReply(commentData: CommentData, $comment: HTMLElement, scroll = true) {
201
+ if (this.replyComment !== null) {
202
+ this.cancelReply()
203
+ }
204
+
205
+ if (this.$sendReply === null) {
206
+ this.$sendReply = Utils.createElement(`<div class="atk-send-reply">${this.$t('reply')} <span class="atk-text"></span><span class="atk-cancel">×</span></div>`);
207
+ this.$sendReply.querySelector<HTMLElement>('.atk-text')!.innerText = `@${commentData.nick}`
208
+ this.$sendReply.addEventListener('click', () => {
209
+ this.cancelReply()
210
+ })
211
+ this.$textareaWrap.append(this.$sendReply)
212
+ }
213
+ this.replyComment = commentData
214
+
215
+ if (this.ctx.conf.editorTravel === true) {
216
+ this.travel($comment)
217
+ }
218
+
219
+ if (scroll) Ui.scrollIntoView(this.$el)
220
+
221
+ this.$textarea.focus()
222
+ }
223
+
224
+ public cancelReply() {
225
+ if (this.$sendReply !== null) {
226
+ this.$sendReply.remove()
227
+ this.$sendReply = null
228
+ }
229
+ this.replyComment = null
230
+
231
+ if (this.ctx.conf.editorTravel === true) {
232
+ this.travelBack()
233
+ }
234
+ }
235
+
236
+ public showNotify(msg: string, type: "i"|"s"|"w"|"e") {
237
+ Ui.showNotify(this.$notifyWrap, msg, type)
238
+ }
239
+
240
+ public showLoading() {
241
+ Ui.showLoading(this.$el)
242
+ }
243
+
244
+ public hideLoading() {
245
+ Ui.hideLoading(this.$el)
246
+ }
247
+
248
+ /** 提交评论 */
249
+ public async submit() {
250
+ if (this.getFinalContent().trim() === '') {
251
+ this.$textarea.focus()
252
+ return
253
+ }
254
+
255
+ this.ctx.trigger('editor-submit')
256
+
257
+ this.showLoading()
258
+
259
+ try {
260
+ const nComment = await this.ctx.getApi().add({
261
+ content: this.getFinalContent(),
262
+ nick: this.user.data.nick,
263
+ email: this.user.data.email,
264
+ link: this.user.data.link,
265
+ rid: (this.replyComment === null) ? 0 : this.replyComment.id,
266
+ page_key: (this.replyComment === null) ? this.ctx.conf.pageKey : this.replyComment.page_key,
267
+ page_title: (this.replyComment === null) ? this.ctx.conf.pageTitle : undefined,
268
+ site_name: (this.replyComment === null) ? this.ctx.conf.site : this.replyComment.site_name
269
+ })
270
+
271
+ // 回复不同页面的评论
272
+ if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
273
+ window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
274
+ }
275
+
276
+ this.ctx.insertComment(nComment)
277
+ this.clearEditor() // 清空编辑器
278
+ this.ctx.trigger('editor-submitted')
279
+ } catch (err: any) {
280
+ console.error(err)
281
+ this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
282
+ return
283
+ } finally {
284
+ this.hideLoading()
285
+ }
286
+ }
287
+
288
+ /** 关闭评论 */
289
+ public close() {
290
+ if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
291
+ this.$textareaWrap.prepend(Utils.createElement(`<div class="atk-comment-closed">${this.$t('onlyAdminCanReply')}</div>`))
292
+
293
+ if (!this.user.data.isAdmin) {
294
+ this.$textarea.style.display = 'none'
295
+ this.closePlugPanel()
296
+ this.$bottom.style.display = 'none'
297
+ } else {
298
+ // 管理员一直打开评论
299
+ this.$textarea.style.display = ''
300
+ this.$bottom.style.display = ''
301
+ }
302
+ }
303
+
304
+ /** 打开评论 */
305
+ public open() {
306
+ this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
307
+ this.$textarea.style.display = ''
308
+ this.$bottom.style.display = ''
309
+ }
310
+
311
+ /** 移动评论框到置顶元素之后 */
312
+ public travel($afterEl: HTMLElement) {
313
+ if (this.isTraveling) return
314
+ this.isTraveling = true
315
+ this.$el.after(Utils.createElement('<div class="atk-editor-travel-placeholder"></div>'))
316
+
317
+ const $travelPlace = Utils.createElement('<div></div>')
318
+ $afterEl.after($travelPlace)
319
+ $travelPlace.replaceWith(this.$el)
320
+
321
+ this.$el.classList.add('atk-fade-in') // 添加渐入动画
322
+ }
323
+
324
+ /** 评论框归位 */
325
+ public travelBack() {
326
+ if (!this.isTraveling) return
327
+ this.isTraveling = false
328
+ this.ctx.$root.querySelector('.atk-editor-travel-placeholder')?.replaceWith(this.$el)
329
+
330
+ // 取消回复
331
+ if (this.replyComment !== null) this.cancelReply()
332
+ }
333
+
334
+ /** 插件初始化 */
335
+ private initPlugs() {
336
+ this.plugList = {}
337
+ this.$plugPanelWrap.innerHTML = ''
338
+ this.$plugPanelWrap.style.display = 'none'
339
+ this.openedPlugName = null
340
+ this.$plugBtnWrap.innerHTML = ''
341
+
342
+ const disabledPlugs: string[] = []
343
+ if (!this.conf.emoticons) disabledPlugs.push('emoticons')
344
+ if (!this.conf.preview) disabledPlugs.push('preview')
345
+
346
+ // 初始化 Editor 插件
347
+ this.ENABLED_PLUGS.forEach((Plug) => {
348
+ const plugName = Plug.Name
349
+
350
+ // 禁用的插件
351
+ if (disabledPlugs.includes(plugName)) return
352
+
353
+ // 插件对象实例化
354
+ const plug = new Plug(this)
355
+ this.plugList[plugName] = plug
356
+
357
+ // 插件按钮
358
+ const $btn = plug.getBtn()
359
+ if ($btn) {
360
+ this.$plugBtnWrap.appendChild($btn)
361
+ $btn.onclick = $btn.onclick || (() => {
362
+ // 其他按钮去除 Active
363
+ this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
364
+
365
+ // 若点击已打开的,则关闭打开的面板
366
+ if (plugName === this.openedPlugName) {
367
+ this.closePlugPanel()
368
+ return
369
+ }
370
+
371
+ this.openPlugPanel(plugName)
372
+
373
+ // 当前按钮添加 Active
374
+ $btn.classList.add('active')
375
+ })
376
+
377
+ // 插件面板初始化
378
+ const $panel = plug.getPanel()
379
+ if ($panel) {
380
+ $panel.setAttribute('data-plug-name', plugName)
381
+ $panel.style.display = 'none'
382
+ this.$plugPanelWrap.appendChild($panel)
383
+ }
384
+ }
385
+ })
386
+ }
387
+
388
+ /** 展开插件面板 */
389
+ public openPlugPanel(plugName: string) {
390
+ Object.entries(this.plugList).forEach(([aPlugName, plug]) => {
391
+ const plugPanel = plug.getPanel()
392
+ if (!plugPanel) return
393
+
394
+ if (aPlugName === plugName) {
395
+ plugPanel.style.display = ''
396
+ if (plug.onPanelShow) plug.onPanelShow()
397
+ } else {
398
+ plugPanel.style.display = 'none'
399
+ if (plug.onPanelHide) plug.onPanelHide()
400
+ }
401
+ })
402
+
403
+ this.$plugPanelWrap.style.display = ''
404
+ this.openedPlugName = plugName
405
+ }
406
+
407
+ /** 收起插件面板 */
408
+ public closePlugPanel() {
409
+ if (!this.openedPlugName) return
410
+
411
+ const plug = this.plugList[this.openedPlugName]
412
+ if (!plug) return
413
+
414
+ if (plug.onPanelHide) plug.onPanelHide()
415
+
416
+ this.$plugPanelWrap.style.display = 'none'
417
+ this.openedPlugName = null
418
+ }
419
+ }
@@ -0,0 +1,3 @@
1
+ import Editor from './editor'
2
+
3
+ export default Editor
@@ -0,0 +1,51 @@
1
+ import * as Utils from '@/lib/utils'
2
+ import Editor from '../editor'
3
+
4
+ /**
5
+ * Editor 插件
6
+ *
7
+ * @desc 使用 Interface x Abstract 合并声明
8
+ * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
9
+ */
10
+ interface EditorPlug {
11
+ onPanelShow?(): void
12
+ onPanelHide?(): void
13
+ }
14
+
15
+ abstract class EditorPlug {
16
+ protected editor: Editor
17
+ protected get ctx() { return this.editor.ctx }
18
+ protected $panel?: HTMLElement
19
+ protected $btn?: HTMLElement
20
+ public onHeaderInput?: (key: string, $input: HTMLElement) => void
21
+
22
+ public constructor(editor: Editor) {
23
+ this.editor = editor
24
+ }
25
+
26
+ public static Name: string
27
+
28
+ protected registerPanel(html: string = '<div></div>') {
29
+ this.$panel = Utils.createElement(html)
30
+ return this.$panel
31
+ }
32
+
33
+ protected registerBtn(html: string) {
34
+ this.$btn = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${this.constructor.name}">${html}</span>`)
35
+ return this.$btn
36
+ }
37
+
38
+ protected registerHeaderInputEvt(action: (key: string, $input: HTMLElement) => void) {
39
+ this.onHeaderInput = action
40
+ }
41
+
42
+ public getPanel() {
43
+ return this.$panel
44
+ }
45
+
46
+ public getBtn() {
47
+ return this.$btn
48
+ }
49
+ }
50
+
51
+ export default EditorPlug
@@ -27,7 +27,7 @@
27
27
  padding: 5px;
28
28
  cursor: pointer;
29
29
  user-select: none;
30
- border-radius: 2px;
30
+ border-radius: (@block-radius / 2);
31
31
  font-size: 16px;
32
32
  min-width: 35px;
33
33
 
@@ -51,6 +51,7 @@
51
51
  background: var(--at-color-bg-transl);
52
52
  height: 30px;
53
53
  border-top: 1px solid var(--at-color-border);
54
+ border-bottom: 1px solid var(--at-color-border);
54
55
 
55
56
  & > span {
56
57
  user-select: none;