artalk 2.2.6 → 2.2.9

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.
@@ -1,17 +1,20 @@
1
+ import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
1
2
  import Context from '../context'
2
3
  import Component from '../lib/component'
3
4
  import * as Utils from '../lib/utils'
4
5
  import * as Ui from '../lib/ui'
5
6
  import Api from '../api'
6
7
  import Comment from './comment'
7
- import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
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
- protected comments: Comment[] = []
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 = 'DATE_DESC'
29
+
24
30
  /** 分页 */
25
31
  public pageMode: 'pagination'|'read-more' = 'pagination'
26
32
  public pageSize: number = 20 // 每次请求获取量
@@ -49,6 +55,9 @@ export default class ListLite extends Component {
49
55
  </div>`)
50
56
  this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
51
57
 
58
+ // 评论为空时显示字符
59
+ if (ctx.conf.noComment) this.noCommentText = ctx.conf.noComment
60
+
52
61
  // 评论时间自动更新
53
62
  window.setInterval(() => {
54
63
  this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
@@ -262,7 +271,7 @@ export default class ListLite extends Component {
262
271
  /** 刷新界面 */
263
272
  public refreshUI() {
264
273
  // 无评论
265
- const isNoComment = this.comments.length <= 0
274
+ const isNoComment = this.commentList.length <= 0
266
275
  let $noComment = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
267
276
 
268
277
  if (isNoComment) {
@@ -280,8 +289,11 @@ export default class ListLite extends Component {
280
289
  }
281
290
 
282
291
  /** 创建新评论 */
283
- protected createComment(data: CommentData) {
284
- const comment = new Comment(this.ctx, data)
292
+ protected createComment(cData: CommentData, ctxData?: CommentData[]) {
293
+ if (!ctxData) ctxData = this.commentDataList
294
+
295
+ const comment = new Comment(this.ctx, cData)
296
+ comment.flatMode = this.flatMode
285
297
  comment.afterRender = () => {
286
298
  if (this.renderComment) this.renderComment(comment)
287
299
  }
@@ -290,6 +302,9 @@ export default class ListLite extends Component {
290
302
  this.refreshUI()
291
303
  }
292
304
 
305
+ // 子评论查找回复对象
306
+ comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
307
+
293
308
  return comment
294
309
  }
295
310
 
@@ -300,31 +315,16 @@ export default class ListLite extends Component {
300
315
  this.putCommentFlatMode(commentData, srcData, 'append')
301
316
  })
302
317
  } else {
303
- this.importCommentsNesting(srcData)
318
+ this.importCommentsNest(srcData)
304
319
  }
305
320
  }
306
321
 
307
322
  // 导入评论 · 嵌套模式
308
- private importCommentsNesting(srcData: CommentData[]) {
309
- // 查找并导入所有子评论
310
- const loadChildren = (parentC: Comment) => {
311
- const children = srcData.filter(o => o.rid === parentC.data.id)
312
- children.forEach((childData: CommentData) => {
313
- const childC = this.createComment(childData)
314
- childC.render()
315
-
316
- // 插入到父评论中
317
- parentC.putChild(childC)
318
-
319
- // 递归加载子评论
320
- loadChildren(childC)
321
- })
322
- }
323
-
323
+ private importCommentsNest(srcData: CommentData[]) {
324
324
  // 遍历 root 评论
325
- const rootComments = srcData.filter((o) => o.rid === 0)
326
- rootComments.forEach((rootData: CommentData) => {
327
- const rootC = this.createComment(rootData)
325
+ const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.nestSortBy, this.conf.nestMax)
326
+ rootNodes.forEach((rootNode: ListNest.CommentNode) => {
327
+ const rootC = this.createComment(rootNode.comment, srcData)
328
328
  rootC.render()
329
329
 
330
330
  // 显示并播放渐入动画
@@ -332,10 +332,25 @@ export default class ListLite extends Component {
332
332
  rootC.playFadeInAnim()
333
333
 
334
334
  // 评论放入 comments 总表中
335
- this.comments.push(rootC)
335
+ this.commentList.push(rootC)
336
336
 
337
337
  // 加载子评论
338
- loadChildren(rootC)
338
+ const that = this
339
+ ;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
340
+ parentNode.children.forEach((node: ListNest.CommentNode) => {
341
+ const childD = node.comment
342
+ const childC = that.createComment(childD, srcData)
343
+ childC.render()
344
+
345
+ // 插入到父评论中
346
+ parentC.putChild(childC)
347
+
348
+ that.commentList.push(childC)
349
+
350
+ // 递归加载子评论
351
+ loadChildren(childC, node)
352
+ })
353
+ })(rootC, rootNode)
339
354
 
340
355
  // 限高检测
341
356
  rootC.checkHeightLimit()
@@ -343,18 +358,13 @@ export default class ListLite extends Component {
343
358
  }
344
359
 
345
360
  /** 导入评论 · 平铺模式 */
346
- private putCommentFlatMode(cData: CommentData, srcData: CommentData[], insertMode: 'append'|'prepend') {
361
+ private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
347
362
  if (cData.is_collapsed) cData.is_allow_reply = false
348
- const comment = this.createComment(cData)
349
- if (cData.rid !== 0) {
350
- const rComment = srcData.find(o => o.id === cData.rid)
351
- if (rComment) comment.replyTo = rComment
352
- }
363
+ const comment = this.createComment(cData, ctxData)
353
364
  comment.render()
354
365
 
355
366
  // 将评论导入 comments 总表中
356
- if (insertMode === 'append') this.comments.push(comment)
357
- if (insertMode === 'prepend') this.comments.unshift(comment)
367
+ this.commentList.push(comment)
358
368
 
359
369
  // 可见评论添加到界面
360
370
  // 注:不可见评论用于显示 “引用内容”
@@ -375,17 +385,18 @@ export default class ListLite extends Component {
375
385
  if (!this.flatMode) {
376
386
  // 嵌套模式
377
387
  const comment = this.createComment(commentData)
378
- comment.render()
379
388
 
380
389
  if (commentData.rid === 0) {
381
390
  // root评论 新增
391
+ comment.render()
382
392
  this.$commentsWrap.prepend(comment.getEl())
383
- this.comments.unshift(comment)
393
+ this.commentList.push(comment)
384
394
  } else {
385
395
  // 子评论 新增
386
396
  const parent = this.findComment(commentData.rid)
387
397
  if (parent) {
388
- parent.putChild(comment)
398
+ comment.render()
399
+ parent.putChild(comment, (this.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
389
400
 
390
401
  // 若父评论存在 “子评论部分” 限高,取消限高
391
402
  comment.getParents().forEach((p) => {
@@ -400,7 +411,7 @@ export default class ListLite extends Component {
400
411
  comment.playFadeInAnim() // 播放评论渐出动画
401
412
  } else {
402
413
  // 平铺模式
403
- const comment = this.putCommentFlatMode(commentData, this.comments.map(c => c.data), 'prepend')
414
+ const comment = this.putCommentFlatMode(commentData, this.commentDataList, 'prepend')
404
415
  Ui.scrollIntoView(comment.getEl()) // 滚动到可见
405
416
  }
406
417
 
@@ -412,47 +423,22 @@ export default class ListLite extends Component {
412
423
  this.ctx.trigger('comments-loaded')
413
424
  }
414
425
 
415
- /** 遍历操作 Comment (包括子评论) */
416
- protected eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
417
- if (commentList.length === 0) return
418
- commentList.every((item) => {
419
- if (action(item, commentList) === false) return false
420
- this.eachComment(item.getChildren(), action)
421
- return true
422
- })
423
- }
424
-
425
426
  /** 查找评论项 */
426
- protected findComment(id: number, src?: Comment[]): Comment|null {
427
- if (!src) src = this.comments
428
- let comment: Comment|null = null
429
- this.eachComment(src, (item) => {
430
- if (item.data.id === id) {
431
- comment = item
432
- return false
433
- }
434
- return true
435
- })
436
-
437
- return comment
427
+ protected findComment(id: number): Comment|undefined {
428
+ return this.commentList.find(c => c.data.id === id)
438
429
  }
439
430
 
440
431
  /** 删除评论 */
441
- public deleteComment(comment: number|Comment) {
442
- let findComment: Comment|null
443
- if (typeof comment === 'number') {
444
- findComment = this.findComment(comment)
445
- if (!findComment) throw Error(`未找到评论 ${comment}`)
446
- } else findComment = comment
447
-
448
- findComment.getEl().remove()
449
- this.eachComment(this.comments, (item, levelList) => {
450
- if (item === findComment) {
451
- levelList.splice(levelList.indexOf(item), 1)
452
- return false
453
- }
454
- return true
455
- })
432
+ public deleteComment(_comment: number|Comment) {
433
+ let comment: Comment
434
+ if (typeof _comment === 'number') {
435
+ const findComment = this.findComment(_comment)
436
+ if (!findComment) throw Error(`未找到评论 ${_comment}`)
437
+ comment = findComment
438
+ } else comment = _comment
439
+
440
+ comment.getEl().remove()
441
+ this.commentList.splice(this.commentList.indexOf(comment), 1)
456
442
 
457
443
  if (this.data) this.data.total -= 1 // 评论数减 1
458
444
 
@@ -463,7 +449,7 @@ export default class ListLite extends Component {
463
449
  public clearAllComments() {
464
450
  this.$commentsWrap.innerHTML = ''
465
451
  this.data = undefined
466
- this.comments = []
452
+ this.commentList = []
467
453
  }
468
454
 
469
455
  /** 更新未读数据 */
@@ -472,7 +458,7 @@ export default class ListLite extends Component {
472
458
 
473
459
  // 高亮评论
474
460
  if (this.unreadHighlight === true) {
475
- this.eachComment(this.comments, (comment) => {
461
+ this.commentList.forEach((comment) => {
476
462
  const notify = this.unread.find(o => o.comment_id === comment.data.id)
477
463
  if (notify) {
478
464
  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
+ }
@@ -1,12 +1,12 @@
1
1
  import '../style/list.less'
2
2
 
3
+ import { ListData } from '~/types/artalk-data'
3
4
  import Context from '../context'
4
5
  import * as Utils from '../lib/utils'
5
6
  import * as Ui from '../lib/ui'
6
7
  import Api from '../api'
7
8
  import ListHTML from './html/list.html?raw'
8
9
  import ListLite from './list-lite'
9
- import { ListData } from '~/types/artalk-data'
10
10
 
11
11
  export default class List extends ListLite {
12
12
  private $closeCommentBtn!: HTMLElement
@@ -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 async checkGoToCommentByUrlHash () {
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
- const comment = this.findComment(commentId)
141
- // TODO 若找不到评论的情况
142
- // if (!comment) { // 若找不到评论
143
- // if (this.hasMoreComments) { // 阅读更多,并重试
144
- // await this.fetchComments()
145
- // }
146
- // }
147
-
148
- if (!comment) return
149
-
150
- Ui.scrollIntoView(comment.getEl(), false)
151
- window.setTimeout(() => {
152
- comment.getEl().classList.add('atk-flash-once')
153
- }, 800)
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
  /** 管理员设置页面信息 */
@@ -38,7 +38,7 @@ export default class Pagination {
38
38
  `<div class="atk-pagination-wrap">
39
39
  <div class="atk-pagination">
40
40
  <div class="atk-btn atk-btn-prev">Prev</div>
41
- <input type="text" class="atk-input" />
41
+ <input type="text" class="atk-input" aria-label="Enter the number of page" />
42
42
  <div class="atk-btn atk-btn-next">Next</div>
43
43
  </div>
44
44
  </div>`)
@@ -3,11 +3,11 @@ import '../style/sidebar-layer.less'
3
3
  import Context from '@/context'
4
4
  import Component from '@/lib/component'
5
5
  import * as Utils from '@/lib/utils'
6
- import SidebarHTML from './html/sidebar-layer.html?raw'
7
6
  import * as Ui from '@/lib/ui'
7
+ import { SidebarShowPayload } from '~/types/event'
8
+ import SidebarHTML from './html/sidebar-layer.html?raw'
8
9
  import Layer from './layer'
9
10
  import Api from '../api'
10
- import { SidebarShowPayload } from '~/types/event'
11
11
 
12
12
  export default class SidebarLayer extends Component {
13
13
  public layer?: Layer
package/src/context.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { marked as libMarked } from 'marked'
2
- import User from './lib/user'
3
2
  import ArtalkConfig from '~/types/artalk-config'
4
3
  import { EventPayloadMap, Event, EventScopeType, Handler } from '~/types/event'
4
+ import User from './lib/user'
5
5
 
6
6
  /**
7
7
  * Artalk Context
package/src/defaults.ts CHANGED
@@ -20,7 +20,8 @@ const defaults: ArtalkConfig = {
20
20
  listSort: true,
21
21
  pvEl: '#ArtalkPV',
22
22
  flatMode: 'auto',
23
- maxNesting: 3,
23
+ nestMax: 3,
24
+
24
25
  gravatar: {
25
26
  default: 'mp',
26
27
  mirror: 'https://sdn.geekzu.org/avatar/',
@@ -40,6 +41,7 @@ const defaults: ArtalkConfig = {
40
41
  imgUpload: true,
41
42
  reqTimeout: 15000,
42
43
  versionCheck: true,
44
+ useBackendConf: false,
43
45
  }
44
46
 
45
47
  export default defaults
@@ -1,9 +1,9 @@
1
1
  import Context from '@/context'
2
2
  import Layer from '@/components/layer'
3
3
  import Dialog from '@/components/dialog'
4
+ import { CheckerPayload } from '~/types/event'
4
5
  import * as Utils from '../utils'
5
6
  import * as Ui from '../ui'
6
- import { CheckerPayload } from '~/types/event'
7
7
  import CaptchaChecker from './captcha-checker'
8
8
  import AdminChecker from './admin-checker'
9
9
 
package/src/lib/utils.ts CHANGED
@@ -293,3 +293,33 @@ export function getURLBasedOnApi(ctx: Context, path: string) {
293
293
  export function getURLBasedOn(baseURL: string, path: string) {
294
294
  return `${baseURL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
295
295
  }
296
+
297
+ /**
298
+ * Performs a deep merge of `source` into `target`.
299
+ * Mutates `target` only but not its objects and arrays.
300
+ *
301
+ * @author inspired by [jhildenbiddle](https://stackoverflow.com/a/48218209).
302
+ * @link https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6
303
+ */
304
+ export function mergeDeep(target: any, source: any) {
305
+ const isObject = (obj: any) => obj && typeof obj === 'object'
306
+
307
+ if (!isObject(target) || !isObject(source)) {
308
+ return source
309
+ }
310
+
311
+ Object.keys(source).forEach(key => {
312
+ const targetValue = target[key]
313
+ const sourceValue = source[key]
314
+
315
+ if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
316
+ target[key] = targetValue.concat(sourceValue)
317
+ } else if (isObject(targetValue) && isObject(sourceValue)) {
318
+ target[key] = mergeDeep({ ...targetValue}, sourceValue)
319
+ } else {
320
+ target[key] = sourceValue
321
+ }
322
+ })
323
+
324
+ return target
325
+ }
@@ -2,8 +2,8 @@
2
2
  --at-color-font: #2a2e2e;
3
3
  --at-color-deep: #2a2e2e;
4
4
  --at-color-sub: #757575;
5
- --at-color-grey: #999;
6
- --at-color-meta: #757e91;
5
+ --at-color-grey: #747474;
6
+ --at-color-meta: #697182;
7
7
  --at-color-border: #eceff2;
8
8
  --at-color-light: #4285f4;
9
9
  --at-color-bg: #FFF;
@@ -83,6 +83,27 @@
83
83
  color: var(--at-color-main);
84
84
  text-decoration: none;
85
85
  }
86
+
87
+ &.atk-reply-at {
88
+ margin-left: 2px;
89
+
90
+ & > .atk-arrow:before {
91
+ content: '';
92
+ vertical-align: middle;
93
+ transform: rotate(90deg);
94
+ border-bottom: 4px solid var(--at-color-grey);
95
+ border-left: 3px solid transparent;
96
+ border-right: 3px solid transparent;
97
+ display: inline-block;
98
+ margin-top: -1px;
99
+ }
100
+
101
+ & > .atk-nick {
102
+ color: var(--at-color-main);
103
+ cursor: pointer;
104
+ margin-left: 6px;
105
+ }
106
+ }
86
107
  }
87
108
 
88
109
  .badge() {
@@ -74,8 +74,8 @@ export default interface ArtalkConfig {
74
74
  /** 平铺模式 */
75
75
  flatMode?: boolean|'auto'
76
76
 
77
- /** 最大嵌套数 */
78
- maxNesting?: number
77
+ /** 嵌套模式 · 最大层数 */
78
+ nestMax?: number
79
79
 
80
80
  /** 显示 UA 徽标 */
81
81
  uaBadge?: boolean
@@ -88,6 +88,9 @@ export default interface ArtalkConfig {
88
88
 
89
89
  /** 版本检测 */
90
90
  versionCheck?: boolean
91
+
92
+ /** 复用后端配置 */
93
+ useBackendConf?: boolean
91
94
  }
92
95
 
93
96
  export interface LocalUser {