artalk 2.2.8 → 2.2.11
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.
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +225 -121
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/package.json +1 -1
- package/src/artalk.ts +6 -0
- package/src/components/comment.ts +60 -25
- package/src/components/editor.ts +15 -2
- package/src/components/list-lite.ts +63 -77
- package/src/components/list-nest.ts +66 -0
- package/src/components/list.ts +36 -16
- package/src/defaults.ts +5 -2
- package/src/style/comment.less +21 -0
- package/types/artalk-config.d.ts +5 -2
package/package.json
CHANGED
package/src/artalk.ts
CHANGED
|
@@ -47,6 +47,11 @@ export default class Artalk {
|
|
|
47
47
|
this.conf.pageTitle = `${document.title}`
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// 列表显示模式
|
|
51
|
+
if (this.conf.nestMax && this.conf.nestMax <= 1) {
|
|
52
|
+
this.conf.flatMode = true
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
// 装载元素
|
|
51
56
|
if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
|
|
52
57
|
this.$root = this.conf.el
|
|
@@ -133,6 +138,7 @@ export default class Artalk {
|
|
|
133
138
|
private initEventBind() {
|
|
134
139
|
// 锚点快速跳转评论
|
|
135
140
|
window.addEventListener('hashchange', () => {
|
|
141
|
+
this.list.goToCommentDelay = false
|
|
136
142
|
this.list.checkGoToCommentByUrlHash()
|
|
137
143
|
})
|
|
138
144
|
|
|
@@ -15,6 +15,8 @@ export default class Comment extends Component {
|
|
|
15
15
|
|
|
16
16
|
public $main!: HTMLElement
|
|
17
17
|
public $header!: HTMLElement
|
|
18
|
+
public $headerNick!: HTMLElement
|
|
19
|
+
public $headerBadge?: HTMLElement
|
|
18
20
|
public $body!: HTMLElement
|
|
19
21
|
public $content!: HTMLElement
|
|
20
22
|
public $children!: HTMLElement|null
|
|
@@ -23,12 +25,15 @@ export default class Comment extends Component {
|
|
|
23
25
|
public voteBtnDown?: ActionBtn
|
|
24
26
|
|
|
25
27
|
public parent: Comment|null
|
|
26
|
-
public
|
|
27
|
-
private
|
|
28
|
-
|
|
28
|
+
public nestCurt: number
|
|
29
|
+
private nestMax: number // 最大嵌套层数
|
|
30
|
+
private children: Comment[] = []
|
|
29
31
|
|
|
30
|
-
public
|
|
31
|
-
public
|
|
32
|
+
public flatMode: boolean = false
|
|
33
|
+
public replyTo?: CommentData // 回复对象
|
|
34
|
+
|
|
35
|
+
public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
|
|
36
|
+
public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
|
|
32
37
|
|
|
33
38
|
public afterRender?: () => void
|
|
34
39
|
|
|
@@ -43,13 +48,13 @@ export default class Comment extends Component {
|
|
|
43
48
|
super(ctx)
|
|
44
49
|
|
|
45
50
|
// 最大嵌套数
|
|
46
|
-
this.
|
|
51
|
+
this.nestMax = ctx.conf.nestMax || 3
|
|
47
52
|
|
|
48
53
|
this.data = { ...data }
|
|
49
54
|
this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
|
|
50
55
|
|
|
51
56
|
this.parent = null
|
|
52
|
-
this.
|
|
57
|
+
this.nestCurt = 1 // 现在已嵌套 n 层
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/** 渲染 UI */
|
|
@@ -70,6 +75,7 @@ export default class Comment extends Component {
|
|
|
70
75
|
this.renderAvatar()
|
|
71
76
|
this.renderHeader()
|
|
72
77
|
this.renderContent()
|
|
78
|
+
this.renderReplyAt()
|
|
73
79
|
this.renderReplyTo()
|
|
74
80
|
this.renderPending()
|
|
75
81
|
this.renderActionBtn()
|
|
@@ -117,28 +123,29 @@ export default class Comment extends Component {
|
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
private renderHeader() {
|
|
120
|
-
|
|
126
|
+
this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
|
|
121
127
|
if (this.data.link) {
|
|
122
128
|
const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
123
129
|
$nickA.innerText = this.data.nick
|
|
124
130
|
$nickA.href = this.data.link
|
|
125
|
-
|
|
131
|
+
this.$headerNick.append($nickA)
|
|
126
132
|
} else {
|
|
127
|
-
|
|
133
|
+
this.$headerNick.innerText = this.data.nick
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+
this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
|
|
131
137
|
if (this.data.badge_name) {
|
|
132
|
-
|
|
138
|
+
this.$headerBadge.innerText = this.data.badge_name
|
|
133
139
|
if (this.data.badge_color)
|
|
134
|
-
|
|
140
|
+
this.$headerBadge.style.backgroundColor = this.data.badge_color
|
|
135
141
|
} else {
|
|
136
|
-
|
|
142
|
+
this.$headerBadge.remove()
|
|
143
|
+
this.$headerBadge = undefined
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
if (this.data.is_pinned) {
|
|
140
147
|
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
|
|
141
|
-
|
|
148
|
+
this.$headerNick.insertAdjacentElement('afterend', $pinnedBadge)
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
@@ -189,8 +196,23 @@ export default class Comment extends Component {
|
|
|
189
196
|
})
|
|
190
197
|
}
|
|
191
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
|
+
|
|
192
213
|
// 回复的对象
|
|
193
214
|
private renderReplyTo() {
|
|
215
|
+
if (!this.flatMode) return // 仅平铺模式显示
|
|
194
216
|
if (!this.replyTo) return
|
|
195
217
|
|
|
196
218
|
this.$replyTo = Utils.createElement(`
|
|
@@ -198,13 +220,23 @@ export default class Comment extends Component {
|
|
|
198
220
|
<div class="atk-meta">回复 <span class="atk-nick"></span>:</div>
|
|
199
221
|
<div class="atk-content"></div>
|
|
200
222
|
</div>`)
|
|
201
|
-
this.$replyTo.querySelector<HTMLElement>('.atk-nick')
|
|
223
|
+
const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
|
|
224
|
+
$nick.innerText = `@${this.replyTo.nick}`
|
|
225
|
+
$nick.onclick = () => { this.goToReplyComment() }
|
|
202
226
|
let replyContent = Utils.marked(this.ctx, this.replyTo.content)
|
|
203
227
|
if (this.replyTo.is_collapsed) replyContent = '[已折叠]'
|
|
204
228
|
this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
|
|
205
229
|
this.$body.prepend(this.$replyTo)
|
|
206
230
|
}
|
|
207
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
|
+
|
|
208
240
|
// 待审核状态
|
|
209
241
|
private renderPending() {
|
|
210
242
|
if (!this.data.is_pending) return
|
|
@@ -324,30 +356,33 @@ export default class Comment extends Component {
|
|
|
324
356
|
return this.children
|
|
325
357
|
}
|
|
326
358
|
|
|
327
|
-
putChild(childC: Comment) {
|
|
359
|
+
putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
|
328
360
|
childC.parent = this
|
|
329
|
-
childC.
|
|
361
|
+
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
330
362
|
this.children.push(childC)
|
|
331
363
|
|
|
332
|
-
this.getChildrenEl()
|
|
364
|
+
const $children = this.getChildrenEl()
|
|
365
|
+
if (insertMode === 'append') $children.append(childC.getEl())
|
|
366
|
+
else if (insertMode === 'prepend') $children.prepend(childC.getEl())
|
|
367
|
+
|
|
333
368
|
childC.playFadeInAnim()
|
|
334
369
|
|
|
335
370
|
// 内容限高
|
|
336
371
|
childC.checkHeightLimitArea('content')
|
|
337
372
|
}
|
|
338
373
|
|
|
339
|
-
getChildrenEl() {
|
|
340
|
-
if (this.$children
|
|
341
|
-
// console.log(this.
|
|
342
|
-
if (this.
|
|
374
|
+
getChildrenEl(): HTMLElement {
|
|
375
|
+
if (!this.$children) {
|
|
376
|
+
// console.log(this.nestCurt)
|
|
377
|
+
if (this.nestCurt < this.nestMax) {
|
|
343
378
|
this.$children = Utils.createElement('<div class="atk-comment-children"></div>')
|
|
344
|
-
this.$main.
|
|
379
|
+
this.$main.append(this.$children)
|
|
345
380
|
} else if (this.parent) {
|
|
346
381
|
this.$children = this.parent.getChildrenEl()
|
|
347
382
|
}
|
|
348
383
|
}
|
|
349
384
|
|
|
350
|
-
return this.$children
|
|
385
|
+
return this.$children!
|
|
351
386
|
}
|
|
352
387
|
|
|
353
388
|
getParent() {
|
package/src/components/editor.ts
CHANGED
|
@@ -85,9 +85,22 @@ export default class Editor extends Component {
|
|
|
85
85
|
if (inputEl && inputEl instanceof HTMLInputElement) {
|
|
86
86
|
inputEl.value = this.user.data[field] || ''
|
|
87
87
|
// 绑定事件
|
|
88
|
-
inputEl.addEventListener('input', () => this.
|
|
88
|
+
inputEl.addEventListener('input', () => this.onHeaderInput(field, inputEl))
|
|
89
89
|
}
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
// Link URL 自动补全协议
|
|
93
|
+
const $linkInput = this.getInputEl('link')
|
|
94
|
+
if ($linkInput) {
|
|
95
|
+
$linkInput.addEventListener('change', () => {
|
|
96
|
+
const link = $linkInput.value.trim()
|
|
97
|
+
if (!!link && !/^(http|https):\/\//.test(link)) {
|
|
98
|
+
$linkInput.value = `https://${link}`
|
|
99
|
+
this.user.data.link = $linkInput.value
|
|
100
|
+
this.saveUser()
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
91
104
|
}
|
|
92
105
|
|
|
93
106
|
getInputEl (field: string) {
|
|
@@ -101,7 +114,7 @@ export default class Editor extends Component {
|
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
/** header 输入框内容变化事件 */
|
|
104
|
-
|
|
117
|
+
onHeaderInput(field: string, inputEl: HTMLInputElement) {
|
|
105
118
|
this.user.data[field] = inputEl.value.trim()
|
|
106
119
|
|
|
107
120
|
// 若修改的是 nick or email
|
|
@@ -7,11 +7,14 @@ import Api from '../api'
|
|
|
7
7
|
import Comment from './comment'
|
|
8
8
|
import Pagination from './pagination'
|
|
9
9
|
import ReadMoreBtn from './read-more-btn'
|
|
10
|
+
import * as ListNest from './list-nest'
|
|
10
11
|
import { backendMinVersion } from '../../package.json'
|
|
11
12
|
|
|
12
13
|
export default class ListLite extends Component {
|
|
13
14
|
protected $commentsWrap: HTMLElement
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
protected commentList: Comment[] = [] // Note: 无层级结构 + 无须排列
|
|
17
|
+
protected get commentDataList() { return this.commentList.map(c => c.data) }
|
|
15
18
|
|
|
16
19
|
protected data?: ListData
|
|
17
20
|
protected isLoading: boolean = false
|
|
@@ -21,6 +24,9 @@ export default class ListLite extends Component {
|
|
|
21
24
|
/** 平铺模式 */
|
|
22
25
|
public flatMode = false
|
|
23
26
|
|
|
27
|
+
/** 嵌套模式 */
|
|
28
|
+
public nestSortBy!: ListNest.SortByType
|
|
29
|
+
|
|
24
30
|
/** 分页 */
|
|
25
31
|
public pageMode: 'pagination'|'read-more' = 'pagination'
|
|
26
32
|
public pageSize: number = 20 // 每次请求获取量
|
|
@@ -52,6 +58,9 @@ export default class ListLite extends Component {
|
|
|
52
58
|
// 评论为空时显示字符
|
|
53
59
|
if (ctx.conf.noComment) this.noCommentText = ctx.conf.noComment
|
|
54
60
|
|
|
61
|
+
// 嵌套排序方式
|
|
62
|
+
this.nestSortBy = this.ctx.conf.nestSort || 'DATE_ASC'
|
|
63
|
+
|
|
55
64
|
// 评论时间自动更新
|
|
56
65
|
window.setInterval(() => {
|
|
57
66
|
this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
|
|
@@ -265,7 +274,7 @@ export default class ListLite extends Component {
|
|
|
265
274
|
/** 刷新界面 */
|
|
266
275
|
public refreshUI() {
|
|
267
276
|
// 无评论
|
|
268
|
-
const isNoComment = this.
|
|
277
|
+
const isNoComment = this.commentList.length <= 0
|
|
269
278
|
let $noComment = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
|
|
270
279
|
|
|
271
280
|
if (isNoComment) {
|
|
@@ -283,8 +292,11 @@ export default class ListLite extends Component {
|
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
/** 创建新评论 */
|
|
286
|
-
protected createComment(
|
|
287
|
-
|
|
295
|
+
protected createComment(cData: CommentData, ctxData?: CommentData[]) {
|
|
296
|
+
if (!ctxData) ctxData = this.commentDataList
|
|
297
|
+
|
|
298
|
+
const comment = new Comment(this.ctx, cData)
|
|
299
|
+
comment.flatMode = this.flatMode
|
|
288
300
|
comment.afterRender = () => {
|
|
289
301
|
if (this.renderComment) this.renderComment(comment)
|
|
290
302
|
}
|
|
@@ -293,6 +305,9 @@ export default class ListLite extends Component {
|
|
|
293
305
|
this.refreshUI()
|
|
294
306
|
}
|
|
295
307
|
|
|
308
|
+
// 子评论查找回复对象
|
|
309
|
+
comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
|
|
310
|
+
|
|
296
311
|
return comment
|
|
297
312
|
}
|
|
298
313
|
|
|
@@ -303,31 +318,16 @@ export default class ListLite extends Component {
|
|
|
303
318
|
this.putCommentFlatMode(commentData, srcData, 'append')
|
|
304
319
|
})
|
|
305
320
|
} else {
|
|
306
|
-
this.
|
|
321
|
+
this.importCommentsNest(srcData)
|
|
307
322
|
}
|
|
308
323
|
}
|
|
309
324
|
|
|
310
325
|
// 导入评论 · 嵌套模式
|
|
311
|
-
private
|
|
312
|
-
// 查找并导入所有子评论
|
|
313
|
-
const loadChildren = (parentC: Comment) => {
|
|
314
|
-
const children = srcData.filter(o => o.rid === parentC.data.id)
|
|
315
|
-
children.forEach((childData: CommentData) => {
|
|
316
|
-
const childC = this.createComment(childData)
|
|
317
|
-
childC.render()
|
|
318
|
-
|
|
319
|
-
// 插入到父评论中
|
|
320
|
-
parentC.putChild(childC)
|
|
321
|
-
|
|
322
|
-
// 递归加载子评论
|
|
323
|
-
loadChildren(childC)
|
|
324
|
-
})
|
|
325
|
-
}
|
|
326
|
-
|
|
326
|
+
private importCommentsNest(srcData: CommentData[]) {
|
|
327
327
|
// 遍历 root 评论
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
const rootC = this.createComment(
|
|
328
|
+
const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.nestSortBy, this.conf.nestMax)
|
|
329
|
+
rootNodes.forEach((rootNode: ListNest.CommentNode) => {
|
|
330
|
+
const rootC = this.createComment(rootNode.comment, srcData)
|
|
331
331
|
rootC.render()
|
|
332
332
|
|
|
333
333
|
// 显示并播放渐入动画
|
|
@@ -335,10 +335,25 @@ export default class ListLite extends Component {
|
|
|
335
335
|
rootC.playFadeInAnim()
|
|
336
336
|
|
|
337
337
|
// 评论放入 comments 总表中
|
|
338
|
-
this.
|
|
338
|
+
this.commentList.push(rootC)
|
|
339
339
|
|
|
340
340
|
// 加载子评论
|
|
341
|
-
|
|
341
|
+
const that = this
|
|
342
|
+
;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
|
|
343
|
+
parentNode.children.forEach((node: ListNest.CommentNode) => {
|
|
344
|
+
const childD = node.comment
|
|
345
|
+
const childC = that.createComment(childD, srcData)
|
|
346
|
+
childC.render()
|
|
347
|
+
|
|
348
|
+
// 插入到父评论中
|
|
349
|
+
parentC.putChild(childC)
|
|
350
|
+
|
|
351
|
+
that.commentList.push(childC)
|
|
352
|
+
|
|
353
|
+
// 递归加载子评论
|
|
354
|
+
loadChildren(childC, node)
|
|
355
|
+
})
|
|
356
|
+
})(rootC, rootNode)
|
|
342
357
|
|
|
343
358
|
// 限高检测
|
|
344
359
|
rootC.checkHeightLimit()
|
|
@@ -346,18 +361,13 @@ export default class ListLite extends Component {
|
|
|
346
361
|
}
|
|
347
362
|
|
|
348
363
|
/** 导入评论 · 平铺模式 */
|
|
349
|
-
private putCommentFlatMode(cData: CommentData,
|
|
364
|
+
private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
|
|
350
365
|
if (cData.is_collapsed) cData.is_allow_reply = false
|
|
351
|
-
const comment = this.createComment(cData)
|
|
352
|
-
if (cData.rid !== 0) {
|
|
353
|
-
const rComment = srcData.find(o => o.id === cData.rid)
|
|
354
|
-
if (rComment) comment.replyTo = rComment
|
|
355
|
-
}
|
|
366
|
+
const comment = this.createComment(cData, ctxData)
|
|
356
367
|
comment.render()
|
|
357
368
|
|
|
358
369
|
// 将评论导入 comments 总表中
|
|
359
|
-
|
|
360
|
-
if (insertMode === 'prepend') this.comments.unshift(comment)
|
|
370
|
+
this.commentList.push(comment)
|
|
361
371
|
|
|
362
372
|
// 可见评论添加到界面
|
|
363
373
|
// 注:不可见评论用于显示 “引用内容”
|
|
@@ -378,17 +388,18 @@ export default class ListLite extends Component {
|
|
|
378
388
|
if (!this.flatMode) {
|
|
379
389
|
// 嵌套模式
|
|
380
390
|
const comment = this.createComment(commentData)
|
|
381
|
-
comment.render()
|
|
382
391
|
|
|
383
392
|
if (commentData.rid === 0) {
|
|
384
393
|
// root评论 新增
|
|
394
|
+
comment.render()
|
|
385
395
|
this.$commentsWrap.prepend(comment.getEl())
|
|
386
|
-
this.
|
|
396
|
+
this.commentList.push(comment)
|
|
387
397
|
} else {
|
|
388
398
|
// 子评论 新增
|
|
389
399
|
const parent = this.findComment(commentData.rid)
|
|
390
400
|
if (parent) {
|
|
391
|
-
|
|
401
|
+
comment.render()
|
|
402
|
+
parent.putChild(comment, (this.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
|
|
392
403
|
|
|
393
404
|
// 若父评论存在 “子评论部分” 限高,取消限高
|
|
394
405
|
comment.getParents().forEach((p) => {
|
|
@@ -403,7 +414,7 @@ export default class ListLite extends Component {
|
|
|
403
414
|
comment.playFadeInAnim() // 播放评论渐出动画
|
|
404
415
|
} else {
|
|
405
416
|
// 平铺模式
|
|
406
|
-
const comment = this.putCommentFlatMode(commentData, this.
|
|
417
|
+
const comment = this.putCommentFlatMode(commentData, this.commentDataList, 'prepend')
|
|
407
418
|
Ui.scrollIntoView(comment.getEl()) // 滚动到可见
|
|
408
419
|
}
|
|
409
420
|
|
|
@@ -415,47 +426,22 @@ export default class ListLite extends Component {
|
|
|
415
426
|
this.ctx.trigger('comments-loaded')
|
|
416
427
|
}
|
|
417
428
|
|
|
418
|
-
/** 遍历操作 Comment (包括子评论) */
|
|
419
|
-
protected eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
|
|
420
|
-
if (commentList.length === 0) return
|
|
421
|
-
commentList.every((item) => {
|
|
422
|
-
if (action(item, commentList) === false) return false
|
|
423
|
-
this.eachComment(item.getChildren(), action)
|
|
424
|
-
return true
|
|
425
|
-
})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
429
|
/** 查找评论项 */
|
|
429
|
-
protected findComment(id: number
|
|
430
|
-
|
|
431
|
-
let comment: Comment|null = null
|
|
432
|
-
this.eachComment(src, (item) => {
|
|
433
|
-
if (item.data.id === id) {
|
|
434
|
-
comment = item
|
|
435
|
-
return false
|
|
436
|
-
}
|
|
437
|
-
return true
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
return comment
|
|
430
|
+
protected findComment(id: number): Comment|undefined {
|
|
431
|
+
return this.commentList.find(c => c.data.id === id)
|
|
441
432
|
}
|
|
442
433
|
|
|
443
434
|
/** 删除评论 */
|
|
444
|
-
public deleteComment(
|
|
445
|
-
let
|
|
446
|
-
if (typeof
|
|
447
|
-
findComment = this.findComment(
|
|
448
|
-
if (!findComment) throw Error(`未找到评论 ${
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
levelList.splice(levelList.indexOf(item), 1)
|
|
455
|
-
return false
|
|
456
|
-
}
|
|
457
|
-
return true
|
|
458
|
-
})
|
|
435
|
+
public deleteComment(_comment: number|Comment) {
|
|
436
|
+
let comment: Comment
|
|
437
|
+
if (typeof _comment === 'number') {
|
|
438
|
+
const findComment = this.findComment(_comment)
|
|
439
|
+
if (!findComment) throw Error(`未找到评论 ${_comment}`)
|
|
440
|
+
comment = findComment
|
|
441
|
+
} else comment = _comment
|
|
442
|
+
|
|
443
|
+
comment.getEl().remove()
|
|
444
|
+
this.commentList.splice(this.commentList.indexOf(comment), 1)
|
|
459
445
|
|
|
460
446
|
if (this.data) this.data.total -= 1 // 评论数减 1
|
|
461
447
|
|
|
@@ -466,7 +452,7 @@ export default class ListLite extends Component {
|
|
|
466
452
|
public clearAllComments() {
|
|
467
453
|
this.$commentsWrap.innerHTML = ''
|
|
468
454
|
this.data = undefined
|
|
469
|
-
this.
|
|
455
|
+
this.commentList = []
|
|
470
456
|
}
|
|
471
457
|
|
|
472
458
|
/** 更新未读数据 */
|
|
@@ -475,7 +461,7 @@ export default class ListLite extends Component {
|
|
|
475
461
|
|
|
476
462
|
// 高亮评论
|
|
477
463
|
if (this.unreadHighlight === true) {
|
|
478
|
-
this.
|
|
464
|
+
this.commentList.forEach((comment) => {
|
|
479
465
|
const notify = this.unread.find(o => o.comment_id === comment.data.id)
|
|
480
466
|
if (notify) {
|
|
481
467
|
comment.setUnread(true)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CommentData } from '~/types/artalk-data'
|
|
2
|
+
|
|
3
|
+
export type CommentNode = {
|
|
4
|
+
id: number
|
|
5
|
+
comment: CommentData
|
|
6
|
+
children: CommentNode[]
|
|
7
|
+
parent?: CommentNode
|
|
8
|
+
level: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SortByType = 'DATE_DESC'|'DATE_ASC'|'SRC_INDEX'|'VOTE_UP_DESC'
|
|
12
|
+
|
|
13
|
+
// 构建树状结构列表
|
|
14
|
+
export function makeNestCommentNodeList(srcData: CommentData[], sortBy: SortByType = 'DATE_DESC', nestMax = 2) {
|
|
15
|
+
const nodeList: CommentNode[] = []
|
|
16
|
+
|
|
17
|
+
const roots = srcData.filter((o) => o.rid === 0)
|
|
18
|
+
roots.forEach((root: CommentData) => {
|
|
19
|
+
const rootNode: CommentNode = {
|
|
20
|
+
id: root.id,
|
|
21
|
+
comment: root,
|
|
22
|
+
children: [],
|
|
23
|
+
level: 1,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
rootNode.parent = rootNode
|
|
27
|
+
nodeList.push(rootNode)
|
|
28
|
+
|
|
29
|
+
;(function loadChildren(parentNode: CommentNode) {
|
|
30
|
+
const children = srcData.filter((o) => o.rid === parentNode.id)
|
|
31
|
+
if (children.length === 0) return
|
|
32
|
+
if (parentNode.level >= nestMax) parentNode = parentNode.parent!
|
|
33
|
+
children.forEach((child: CommentData) => {
|
|
34
|
+
const childNode: CommentNode = {
|
|
35
|
+
id: child.id,
|
|
36
|
+
comment: child,
|
|
37
|
+
children: [],
|
|
38
|
+
parent: parentNode,
|
|
39
|
+
level: parentNode.level + 1,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
parentNode.children.push(childNode)
|
|
43
|
+
loadChildren(childNode)
|
|
44
|
+
})
|
|
45
|
+
})(rootNode)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 排序
|
|
49
|
+
const sortFunc = (a: CommentNode, b: CommentNode): number => {
|
|
50
|
+
let v = a.id - b.id
|
|
51
|
+
if (sortBy === 'DATE_ASC') v = +new Date(a.comment.date) - +new Date(b.comment.date)
|
|
52
|
+
else if (sortBy === 'DATE_DESC') v = +new Date(b.comment.date) - +new Date(a.comment.date)
|
|
53
|
+
else if (sortBy === 'SRC_INDEX') v = srcData.indexOf(a.comment) - srcData.indexOf(b.comment)
|
|
54
|
+
else if (sortBy === 'VOTE_UP_DESC') v = b.comment.vote_up - a.comment.vote_up
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
(function sortLevels(nodes: CommentNode[]) {
|
|
59
|
+
nodes.forEach((node) => {
|
|
60
|
+
node.children = node.children.sort(sortFunc)
|
|
61
|
+
sortLevels(node.children)
|
|
62
|
+
})
|
|
63
|
+
})(nodeList)
|
|
64
|
+
|
|
65
|
+
return nodeList
|
|
66
|
+
}
|
package/src/components/list.ts
CHANGED
|
@@ -112,11 +112,19 @@ export default class List extends ListLite {
|
|
|
112
112
|
super.onLoad(data, offset)
|
|
113
113
|
|
|
114
114
|
// 检测锚点跳转
|
|
115
|
-
this.checkGoToCommentByUrlHash()
|
|
115
|
+
if (!this.goToCommentFounded) this.checkGoToCommentByUrlHash()
|
|
116
|
+
|
|
117
|
+
// 防止评论框被吞
|
|
118
|
+
if (this.ctx.conf.editorTravel === true) {
|
|
119
|
+
this.ctx.trigger('editor-travel-back')
|
|
120
|
+
}
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
private goToCommentFounded = false
|
|
124
|
+
public goToCommentDelay = true
|
|
125
|
+
|
|
118
126
|
/** 跳到评论项位置 - 根据 `location.hash` */
|
|
119
|
-
public
|
|
127
|
+
public checkGoToCommentByUrlHash() {
|
|
120
128
|
let commentId = Number(Utils.getQueryParam('atk_comment')) // same as backend GetReplyLink()
|
|
121
129
|
if (!commentId) {
|
|
122
130
|
const match = window.location.hash.match(/#atk-comment-([0-9]+)/)
|
|
@@ -125,6 +133,14 @@ export default class List extends ListLite {
|
|
|
125
133
|
}
|
|
126
134
|
if (!commentId) return
|
|
127
135
|
|
|
136
|
+
const comment = this.findComment(commentId)
|
|
137
|
+
if (!comment) { // 若找不到评论
|
|
138
|
+
// 自动翻页
|
|
139
|
+
if (this.readMoreBtn) this.readMoreBtn.click()
|
|
140
|
+
else if (this.pagination) this.pagination.next()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
128
144
|
// 已阅 API
|
|
129
145
|
const notifyKey = Utils.getQueryParam('atk_notify_key')
|
|
130
146
|
if (notifyKey) {
|
|
@@ -137,20 +153,24 @@ export default class List extends ListLite {
|
|
|
137
153
|
})
|
|
138
154
|
}
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
// 若父评论存在 “子评论部分” 限高,取消限高
|
|
157
|
+
comment.getParents().forEach((p) => {
|
|
158
|
+
if (p.$children) p.heightLimitRemove(p.$children)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const goTo = () => {
|
|
162
|
+
Ui.scrollIntoView(comment.getEl(), false)
|
|
163
|
+
|
|
164
|
+
comment.getEl().classList.remove('atk-flash-once')
|
|
165
|
+
window.setTimeout(() => {
|
|
166
|
+
comment.getEl().classList.add('atk-flash-once')
|
|
167
|
+
}, 150)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!this.goToCommentDelay) goTo()
|
|
171
|
+
else window.setTimeout(() => goTo(), 350)
|
|
172
|
+
|
|
173
|
+
this.goToCommentFounded = true
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
/** 管理员设置页面信息 */
|
package/src/defaults.ts
CHANGED
|
@@ -12,6 +12,10 @@ const defaults: ArtalkConfig = {
|
|
|
12
12
|
darkMode: false,
|
|
13
13
|
editorTravel: true,
|
|
14
14
|
|
|
15
|
+
flatMode: 'auto',
|
|
16
|
+
nestMax: 2,
|
|
17
|
+
nestSort: 'DATE_ASC',
|
|
18
|
+
|
|
15
19
|
emoticons: "https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json",
|
|
16
20
|
|
|
17
21
|
vote: true,
|
|
@@ -19,8 +23,7 @@ const defaults: ArtalkConfig = {
|
|
|
19
23
|
uaBadge: true,
|
|
20
24
|
listSort: true,
|
|
21
25
|
pvEl: '#ArtalkPV',
|
|
22
|
-
|
|
23
|
-
maxNesting: 3,
|
|
26
|
+
|
|
24
27
|
gravatar: {
|
|
25
28
|
default: 'mp',
|
|
26
29
|
mirror: 'https://sdn.geekzu.org/avatar/',
|