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