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,22 @@
1
+ import fullVersionConf from './vite.config'
2
+ import * as Utils from './src/lib/utils'
3
+
4
+ export default Utils.mergeDeep(fullVersionConf, {
5
+ build: {
6
+ lib: {
7
+ fileName: (format) => ((format == "umd") ? 'ArtalkLite.js' : `ArtalkLite.${format}.js`),
8
+ },
9
+ rollupOptions: {
10
+ external: ['marked'],
11
+ output: {
12
+ globals: {
13
+ marked: 'marked',
14
+ },
15
+ assetFileNames: (assetInfo) => (/\.css$/.test(assetInfo.name) ? "ArtalkLite.css" : "[name].[ext]")
16
+ }
17
+ }
18
+ },
19
+ define: {
20
+ ARTALK_LITE: true,
21
+ },
22
+ })
package/vite.config.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { defineConfig } from 'vite'
2
2
  import { resolve } from 'path'
3
- import commonConf from './vite-common.config'
4
- import tsconfigPaths from 'vite-tsconfig-paths'
3
+ import baseConf from './vite-base.config'
4
+ import * as Utils from './src/lib/utils'
5
5
 
6
- export default defineConfig({
6
+ export default Utils.mergeDeep(baseConf, defineConfig({
7
7
  build: {
8
8
  target: 'es2015',
9
9
  outDir: resolve(__dirname, "dist"),
10
10
  minify: 'terser',
11
+ emptyOutDir: false,
11
12
  lib: {
12
13
  entry: resolve(__dirname, 'src/main.ts'),
13
14
  name: 'Artalk',
@@ -16,15 +17,11 @@ export default defineConfig({
16
17
  },
17
18
  rollupOptions: {
18
19
  output: {
19
- assetFileNames: assetInfo => {
20
- if (/\.css$/.test(assetInfo.name)) {
21
- return 'Artalk.css'
22
- }
23
- return "[name].[ext]"
24
- }
20
+ assetFileNames: (assetInfo) => (/\.css$/.test(assetInfo.name) ? "Artalk.css" : "[name].[ext]")
25
21
  }
26
22
  }
27
23
  },
28
- plugins: [tsconfigPaths()],
29
- ...commonConf
30
- })
24
+ define: {
25
+ ARTALK_LITE: false,
26
+ },
27
+ }))
@@ -1,10 +0,0 @@
1
- import Context from '@/context'
2
- import Component from '@/lib/component'
3
- import * as Utils from '@/lib/utils'
4
- import * as Ui from '@/lib/ui'
5
-
6
- export default class Example extends Component {
7
- constructor(ctx: Context) {
8
- super(ctx)
9
- }
10
- }
@@ -1,605 +0,0 @@
1
- import '../style/comment.less'
2
-
3
- import { CommentData } from '~/types/artalk-data'
4
- import Context from '../context'
5
- import Component from '../lib/component'
6
- import * as Utils from '../lib/utils'
7
- import * as Ui from '../lib/ui'
8
- import UADetect from '../lib/detect'
9
- import CommentHTML from './html/comment.html?raw'
10
- import Api from '../api'
11
- import ActionBtn from './action-btn'
12
-
13
- export default class Comment extends Component {
14
- public data: CommentData
15
-
16
- public $main!: HTMLElement
17
- public $header!: HTMLElement
18
- public $headerNick!: HTMLElement
19
- public $headerBadge?: HTMLElement
20
- public $body!: HTMLElement
21
- public $content!: HTMLElement
22
- public $children!: HTMLElement|null
23
- public $actions!: HTMLElement
24
- public voteBtnUp?: ActionBtn
25
- public voteBtnDown?: ActionBtn
26
-
27
- public parent: Comment|null
28
- public nestCurt: number
29
- private nestMax: number // 最大嵌套层数
30
- private children: Comment[] = []
31
-
32
- public flatMode: boolean = false
33
- public replyTo?: CommentData // 回复对象
34
-
35
- public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
36
- public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
37
-
38
- public afterRender?: () => void
39
-
40
- private unread = false
41
- private openable = false
42
- private openURL?: string
43
- public openEvt?: () => void
44
-
45
- public onReplyBtnClick?: Function
46
-
47
- constructor(ctx: Context, data: CommentData) {
48
- super(ctx)
49
-
50
- // 最大嵌套数
51
- this.nestMax = ctx.conf.nestMax || 3
52
-
53
- this.data = { ...data }
54
- this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
55
-
56
- this.parent = null
57
- this.nestCurt = 1 // 现在已嵌套 n 层
58
- }
59
-
60
- /** 渲染 UI */
61
- public render() {
62
- this.$el = Utils.createElement(CommentHTML)
63
- this.$main = this.$el.querySelector('.atk-main')!
64
- this.$header = this.$el.querySelector('.atk-header')!
65
- this.$body = this.$el.querySelector('.atk-body')!
66
- this.$content = this.$body.querySelector('.atk-content')!
67
- this.$actions = this.$el.querySelector('.atk-actions')!
68
- this.$children = null
69
-
70
- this.$el.setAttribute('data-comment-id', `${this.data.id}`)
71
-
72
- this.renderCheckUnread()
73
- this.renderCheckClickable()
74
-
75
- this.renderAvatar()
76
- this.renderHeader()
77
- this.renderContent()
78
- this.renderReplyAt()
79
- this.renderReplyTo()
80
- this.renderPending()
81
- this.renderActionBtn()
82
-
83
- if (this.afterRender) this.afterRender()
84
-
85
- return this.$el
86
- }
87
-
88
- //#region Renders
89
- private renderCheckUnread() {
90
- if (this.unread) this.$el.classList.add('atk-unread')
91
- else this.$el.classList.remove('atk-unread')
92
- }
93
-
94
- private renderCheckClickable() {
95
- if (this.openable) {
96
- this.$el.classList.add('atk-openable')
97
- } else {
98
- this.$el.classList.remove('atk-openable')
99
- }
100
-
101
- this.$el.addEventListener('click', (evt) => {
102
- if (this.openable && this.openURL) {
103
- evt.preventDefault()
104
- window.open(this.openURL)
105
- }
106
- if (this.openEvt)
107
- this.openEvt()
108
- })
109
- }
110
-
111
- private renderAvatar() {
112
- const $avatar = this.$el.querySelector<HTMLElement>('.atk-avatar')!
113
- const $avatarImg = Utils.createElement<HTMLImageElement>('<img />')
114
- $avatarImg.src = this.getGravatarUrl()
115
- if (this.data.link) {
116
- const $avatarA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
117
- $avatarA.href = this.data.link
118
- $avatarA.append($avatarImg)
119
- $avatar.append($avatarA)
120
- } else {
121
- $avatar.append($avatarImg)
122
- }
123
- }
124
-
125
- private renderHeader() {
126
- this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
127
- if (this.data.link) {
128
- const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
129
- $nickA.innerText = this.data.nick
130
- $nickA.href = this.data.link
131
- this.$headerNick.append($nickA)
132
- } else {
133
- this.$headerNick.innerText = this.data.nick
134
- }
135
-
136
- this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
137
- if (this.data.badge_name) {
138
- this.$headerBadge.innerText = this.data.badge_name
139
- if (this.data.badge_color)
140
- this.$headerBadge.style.backgroundColor = this.data.badge_color
141
- } else {
142
- this.$headerBadge.remove()
143
- this.$headerBadge = undefined
144
- }
145
-
146
- if (this.data.is_pinned) {
147
- const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
148
- this.$headerNick.insertAdjacentElement('afterend', $pinnedBadge)
149
- }
150
-
151
- const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
152
- $date.innerText = this.getDateFormatted()
153
- $date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
154
-
155
- if (this.conf.uaBadge) {
156
- const $uaWrap = Utils.createElement(`<span class="atk-ua-wrap"></span>`)
157
- const $uaBrowser = Utils.createElement(`<span class="atk-ua ua-browser"></span>`)
158
- const $usOS = Utils.createElement(`<span class="atk-ua ua-os"></span>`)
159
- $uaBrowser.innerText = this.getUserUaBrowser()
160
- $usOS.innerText = this.getUserUaOS()
161
- $uaWrap.append($uaBrowser)
162
- $uaWrap.append($usOS)
163
- this.$header.append($uaWrap)
164
- }
165
- }
166
-
167
- private renderContent() {
168
- // 内容 & 折叠
169
- if (!this.data.is_collapsed) {
170
- this.$content.innerHTML = this.getContentMarked()
171
- return
172
- }
173
-
174
- this.$content.classList.add('atk-hide', 'atk-type-collapsed')
175
- const collapsedInfoEl = Utils.createElement(`
176
- <div class="atk-collapsed">
177
- <span class="atk-text">该评论已被系统或管理员折叠</span>
178
- <span class="atk-show-btn">查看内容</span>
179
- </div>`)
180
- this.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
181
-
182
- const contentShowBtn = collapsedInfoEl.querySelector('.atk-show-btn')!
183
- contentShowBtn.addEventListener('click', (e) => {
184
- e.stopPropagation() // 防止穿透
185
-
186
- if (this.$content.classList.contains('atk-hide')) {
187
- this.$content.innerHTML = this.getContentMarked()
188
- this.$content.classList.remove('atk-hide')
189
- Ui.playFadeInAnim(this.$content)
190
- contentShowBtn.innerHTML = '收起内容'
191
- } else {
192
- this.$content.innerHTML = ''
193
- this.$content.classList.add('atk-hide')
194
- contentShowBtn.innerHTML = '查看内容'
195
- }
196
- })
197
- }
198
-
199
- // 层级嵌套模式显示 At
200
- private renderReplyAt() {
201
- if (this.flatMode || this.data.rid === 0) return // not 平铺模式 或 根评论
202
- if (this.$replyAt) return // not 关闭显示 或 已经显示
203
- if (!this.replyTo) return
204
-
205
- this.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
206
- this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.replyTo.nick}`
207
- this.$replyAt.onclick = () => { this.goToReplyComment() }
208
-
209
- const $after = this.$headerBadge || this.$headerNick
210
- $after.insertAdjacentElement('afterend', this.$replyAt)
211
- }
212
-
213
- // 回复的对象
214
- private renderReplyTo() {
215
- if (!this.flatMode) return // 仅平铺模式显示
216
- if (!this.replyTo) return
217
-
218
- this.$replyTo = Utils.createElement(`
219
- <div class="atk-reply-to">
220
- <div class="atk-meta">回复 <span class="atk-nick"></span>:</div>
221
- <div class="atk-content"></div>
222
- </div>`)
223
- const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
224
- $nick.innerText = `@${this.replyTo.nick}`
225
- $nick.onclick = () => { this.goToReplyComment() }
226
- let replyContent = Utils.marked(this.ctx, this.replyTo.content)
227
- if (this.replyTo.is_collapsed) replyContent = '[已折叠]'
228
- this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
229
- this.$body.prepend(this.$replyTo)
230
- }
231
-
232
- public goToReplyComment() {
233
- const origHash = window.location.hash
234
- const modifyHash = `#atk-comment-${this.data.rid}`
235
-
236
- window.location.hash = modifyHash
237
- if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
238
- }
239
-
240
- // 待审核状态
241
- private renderPending() {
242
- if (!this.data.is_pending) return
243
-
244
- const pendingEl = Utils.createElement(`<div class="atk-pending">审核中,仅本人可见。</div>`)
245
- this.$body.prepend(pendingEl)
246
- }
247
-
248
- /** 初始化评论操作按钮 */
249
- private renderActionBtn() {
250
- // 投票功能
251
- if (this.ctx.conf.vote) {
252
- // 赞同按钮
253
- this.voteBtnUp = new ActionBtn(() => `赞同 (${this.data.vote_up || 0})`).appendTo(this.$actions)
254
- this.voteBtnUp.setClick(() => {
255
- this.vote('up')
256
- })
257
-
258
- // 反对按钮
259
- if (this.ctx.conf.voteDown) {
260
- this.voteBtnDown = new ActionBtn(() => `反对 (${this.data.vote_down || 0})`).appendTo(this.$actions)
261
- this.voteBtnDown.setClick(() => {
262
- this.vote('down')
263
- })
264
- }
265
- }
266
-
267
- // 绑定回复按钮事件
268
- if (this.data.is_allow_reply) {
269
- const replyBtn = Utils.createElement(`<span>回复</span>`)
270
- this.$actions.append(replyBtn)
271
- replyBtn.addEventListener('click', (e) => {
272
- e.stopPropagation() // 防止穿透
273
- if (!this.onReplyBtnClick) {
274
- this.ctx.trigger('editor-reply', {data: this.data, $el: this.$el})
275
- } else {
276
- this.onReplyBtnClick()
277
- }
278
- })
279
- }
280
-
281
- // 绑定折叠按钮事件
282
- const collapseBtn = new ActionBtn({
283
- text: () => (this.data.is_collapsed ? '取消折叠' : '折叠'),
284
- adminOnly: true
285
- })
286
- collapseBtn.appendTo(this.$actions)
287
- collapseBtn.setClick(() => {
288
- this.adminEdit('collapsed', collapseBtn)
289
- })
290
-
291
- // 绑定待审核按钮事件
292
- const pendingBtn = new ActionBtn({
293
- text: () => (this.data.is_pending ? '待审' : '已审'),
294
- adminOnly: true
295
- })
296
- pendingBtn.appendTo(this.$actions)
297
- pendingBtn.setClick(() => {
298
- this.adminEdit('pending', pendingBtn)
299
- })
300
-
301
- // 绑定删除按钮事件
302
- const delBtn = new ActionBtn({
303
- text: '删除',
304
- confirm: true,
305
- confirmText: '确认删除',
306
- adminOnly: true,
307
- })
308
- delBtn.appendTo(this.$actions)
309
- delBtn.setClick(() => {
310
- this.adminDelete(delBtn)
311
- })
312
-
313
- // 绑定置顶按钮事件
314
- const pinnedBtn = new ActionBtn({
315
- text: () => (this.data.is_pinned ? '取消置顶' : '置顶'),
316
- adminOnly: true
317
- })
318
- pinnedBtn.appendTo(this.$actions)
319
- pinnedBtn.setClick(() => {
320
- this.adminEdit('pinned', pendingBtn)
321
- })
322
- }
323
- //#endregion
324
-
325
- /** 刷新评论 UI */
326
- public refreshUI() {
327
- const originalEl = this.$el
328
- const newEl = this.render()
329
- originalEl.replaceWith(newEl) // 替换 document 中的的 elem
330
- this.playFadeInAnim()
331
-
332
- // 重建子评论元素
333
- this.eachComment(this.children, (child) => {
334
- child.parent?.getChildrenEl().appendChild(child.render())
335
- child.playFadeInAnim()
336
- })
337
-
338
- this.ctx.trigger('comments-loaded')
339
- }
340
-
341
- /** 遍历评论 */
342
- private eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
343
- if (commentList.length === 0) return
344
- commentList.every((item) => {
345
- if (action(item, commentList) === false) return false
346
- this.eachComment(item.getChildren(), action)
347
- return true
348
- })
349
- }
350
-
351
- getIsRoot() {
352
- return this.data.rid === 0
353
- }
354
-
355
- getChildren() {
356
- return this.children
357
- }
358
-
359
- putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
360
- childC.parent = this
361
- childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
362
- this.children.push(childC)
363
-
364
- const $children = this.getChildrenEl()
365
- if (insertMode === 'append') $children.append(childC.getEl())
366
- else if (insertMode === 'prepend') $children.prepend(childC.getEl())
367
-
368
- childC.playFadeInAnim()
369
-
370
- // 内容限高
371
- childC.checkHeightLimitArea('content')
372
- }
373
-
374
- getChildrenEl(): HTMLElement {
375
- if (!this.$children) {
376
- // console.log(this.nestCurt)
377
- if (this.nestCurt < this.nestMax) {
378
- this.$children = Utils.createElement('<div class="atk-comment-children"></div>')
379
- this.$main.append(this.$children)
380
- } else if (this.parent) {
381
- this.$children = this.parent.getChildrenEl()
382
- }
383
- }
384
-
385
- return this.$children!
386
- }
387
-
388
- getParent() {
389
- return this.parent
390
- }
391
-
392
- getParents() {
393
- const parents: Comment[] = []
394
- const once = (c: Comment) => {
395
- if (c.parent) {
396
- parents.push(c.parent)
397
- once(c.parent)
398
- }
399
- }
400
-
401
- once(this)
402
- return parents
403
- }
404
-
405
- getEl() {
406
- return this.$el
407
- }
408
-
409
- getData() {
410
- return this.data
411
- }
412
-
413
- getGravatarUrl() {
414
- return Utils.getGravatarURL(this.ctx, this.data.email_encrypted)
415
- }
416
-
417
- getContentMarked() {
418
- return Utils.marked(this.ctx, this.data.content)
419
- }
420
-
421
- getDateFormatted() {
422
- return Utils.timeAgo(new Date(this.data.date))
423
- }
424
-
425
- getUserUaBrowser() {
426
- const info = UADetect(this.data.ua)
427
- return `${info.browser} ${info.version}`
428
- }
429
-
430
- getUserUaOS() {
431
- const info = UADetect(this.data.ua)
432
- return `${info.os} ${info.osVersion}`
433
- }
434
-
435
- /** 渐出动画 */
436
- playFadeInAnim() {
437
- Ui.playFadeInAnim(this.$el)
438
- }
439
-
440
- /** 投票操作 */
441
- vote(type: 'up'|'down') {
442
- const actionBtn = type === 'up' ? this.voteBtnUp : this.voteBtnDown
443
-
444
- new Api(this.ctx).vote(this.data.id, `comment_${type}`)
445
- .then((v) => {
446
- this.data.vote_up = v.up
447
- this.data.vote_down = v.down
448
- this.voteBtnUp?.updateText()
449
- this.voteBtnDown?.updateText()
450
- })
451
- .catch((err) => {
452
- actionBtn?.setError(`投票失败`)
453
- console.log(err)
454
- })
455
- }
456
-
457
- /** 管理员 - 评论状态修改 */
458
- adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
459
- if (btnElem.isLoading) return // 若正在修改中
460
-
461
- btnElem.setLoading(true, '修改中...')
462
-
463
- // 克隆并修改当前数据
464
- const modify = { ...this.data }
465
- if (type === 'collapsed') {
466
- modify.is_collapsed = !modify.is_collapsed
467
- } else if (type === 'pending') {
468
- modify.is_pending = !modify.is_pending
469
- } else if (type === 'pinned') {
470
- modify.is_pinned = !modify.is_pinned
471
- }
472
-
473
- new Api(this.ctx).commentEdit(modify).then((comment) => {
474
- btnElem.setLoading(false)
475
-
476
- // 刷新当前 Comment UI
477
- this.data = comment
478
- this.refreshUI()
479
- Ui.playFadeInAnim(this.$body)
480
-
481
- // 刷新 List UI
482
- this.ctx.trigger('list-refresh-ui')
483
- }).catch((err) => {
484
- console.error(err)
485
- btnElem.setError('修改失败')
486
- })
487
- }
488
-
489
- public onDelete?: (comment: Comment) => void
490
-
491
- /** 管理员 - 评论删除 */
492
- adminDelete(btnElem: ActionBtn) {
493
- if (btnElem.isLoading) return // 若正在删除中
494
-
495
- btnElem.setLoading(true, '删除中...')
496
- new Api(this.ctx).commentDel(this.data.id, this.data.site_name)
497
- .then(() => {
498
- btnElem.setLoading(false)
499
- if (this.onDelete) this.onDelete(this)
500
- })
501
- .catch((e) => {
502
- console.error(e)
503
- btnElem.setError('删除失败')
504
- })
505
- }
506
-
507
- public setUnread(val: boolean) {
508
- this.unread = val
509
- if (this.unread) this.$el.classList.add('atk-unread')
510
- else this.$el.classList.remove('atk-unread')
511
- }
512
-
513
- public setOpenURL(url: string) {
514
- if (!url) {
515
- this.openable = false
516
- this.$el.classList.remove('atk-openable')
517
- }
518
-
519
- this.openable = true
520
- this.openURL = url
521
- this.$el.classList.add('atk-openable')
522
- }
523
-
524
- /** 内容限高检测 */
525
- checkHeightLimit() {
526
- this.checkHeightLimitArea('content') // 评论内容限高
527
- this.checkHeightLimitArea('children') // 子评论部分限高(嵌套模式)
528
- }
529
-
530
- /** 目标内容限高检测 */
531
- checkHeightLimitArea(area: 'children'|'content') {
532
- // 参数准备
533
- const childrenMaxH = this.ctx.conf.heightLimit?.children
534
- const contentMaxH = this.ctx.conf.heightLimit?.content
535
-
536
- if (area === 'children' && !childrenMaxH) return
537
- if (area === 'content' && !contentMaxH) return
538
-
539
- // 限高
540
- let maxHeight: number
541
- if (area === 'children') maxHeight = childrenMaxH!
542
- if (area === 'content') maxHeight = contentMaxH!
543
-
544
- // 检测指定元素
545
- const checkEl = ($el?: HTMLElement|null) => {
546
- if (!$el) return
547
-
548
- // 是否超过高度
549
- if (Utils.getHeight($el) > maxHeight) {
550
- this.heightLimitAdd($el, maxHeight)
551
- }
552
- }
553
-
554
- // 执行限高检测
555
- if (area === 'children') {
556
- checkEl(this.$children)
557
- } else if (area === 'content') {
558
- checkEl(this.$content)
559
- checkEl(this.$replyTo)
560
-
561
- // 若有图片 · 图片加载完后再检测一次
562
- Utils.onImagesLoaded(this.$content, () => {
563
- checkEl(this.$content)
564
- })
565
- if (this.$replyTo) {
566
- Utils.onImagesLoaded(this.$replyTo, () => {
567
- checkEl(this.$replyTo)
568
- })
569
- }
570
- }
571
- }
572
-
573
- // 操作 · 取消限高
574
- heightLimitRemove($el: HTMLElement) {
575
- if (!$el) return
576
- if (!$el.classList.contains('atk-height-limit')) return
577
-
578
- $el.classList.remove('atk-height-limit')
579
- Array.from($el.children).forEach((e) => {
580
- if (e.classList.contains('atk-height-limit-btn')) e.remove()
581
- })
582
- $el.style.height = ''
583
- $el.style.overflow = ''
584
- }
585
-
586
- // 操作 · 内容限高
587
- heightLimitAdd($el: HTMLElement, maxHeight: number) {
588
- if (!$el) return
589
- if ($el.classList.contains('atk-height-limit')) return
590
-
591
- $el.classList.add('atk-height-limit')
592
- $el.style.height = `${maxHeight}px`
593
- $el.style.overflow = 'hidden'
594
- const $hideMoreOpenBtn = Utils.createElement(`<div class="atk-height-limit-btn">阅读更多</span>`)
595
- $hideMoreOpenBtn.onclick = (e) => {
596
- e.stopPropagation()
597
- this.heightLimitRemove($el)
598
-
599
- // 子评论数等于 1,直接取消限高
600
- const children = this.getChildren()
601
- if (children.length === 1) children[0].heightLimitRemove(children[0].$content)
602
- }
603
- $el.append($hideMoreOpenBtn)
604
- }
605
- }
@@ -1,29 +0,0 @@
1
- import './_PreviewPlug/PreviewPlug.less'
2
- import Editor from '../Editor'
3
- import ArtalkContext from '~/src/ArtalkContext'
4
- import Utils from '~/src/utils'
5
-
6
- export default class PreviewPlug extends ArtalkContext {
7
- public elem: HTMLElement
8
-
9
- constructor (public editor: Editor) {
10
- super(editor.artalk)
11
-
12
- this.initElem()
13
- }
14
-
15
- initElem () {
16
- this.elem = Utils.createElement('<div class="atk-editor-plug-example"></div>')
17
- }
18
-
19
- static Name = 'example'
20
- static BtnHTML = '栗子'
21
-
22
- getEl () {
23
- return this.elem
24
- }
25
-
26
- onShow () {}
27
-
28
- onHide () {}
29
- }