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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.12",
4
- "backendMinVersion": "2.1.6",
3
+ "version": "2.3.0",
4
+ "backendMinVersion": "2.1.7",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -24,17 +24,18 @@
24
24
  "module": "./dist/Artalk.es.js",
25
25
  "types": "./types/index.d.ts",
26
26
  "devDependencies": {
27
- "@types/marked": "^4.0.3"
27
+ "@types/marked": "4.0.3"
28
28
  },
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.14",
32
+ "marked": "^4.0.16",
33
33
  "abortcontroller-polyfill": "^1.7.3"
34
34
  },
35
35
  "scripts": {
36
36
  "dev": "vite",
37
- "build": "pnpm lint && vite build",
37
+ "build": "pnpm lint && vite build && pnpm build:lite",
38
+ "build:lite": "vite build --config vite-lite.config.ts",
38
39
  "build:demo": "vite build --config vite-demo.config.ts",
39
40
  "serve": "vite preview",
40
41
  "deploy": "gh-pages -d deploy",
package/src/api/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
2
2
  import ArtalkConfig from '~/types/artalk-config'
3
- import Context from '../context'
3
+ import Context from '~/types/context'
4
4
  import { Fetch, ToFormData, POST, GET } from './request'
5
5
  import * as Utils from '../lib/utils'
6
6
  import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
@@ -274,6 +274,25 @@ export default class Api {
274
274
  return p.pv as number
275
275
  }
276
276
 
277
+ /** 统计 */
278
+ public async stat(
279
+ type: 'latest_comments'|'latest_pages'|'pv_most_pages'|'comment_most_pages'|
280
+ 'page_pv'|'site_pv'|'page_comment'|'site_comment',
281
+ pageKeys?: string|string[],
282
+ limit?: number
283
+ ) {
284
+ const params: any = {
285
+ type,
286
+ site_name: this.ctx.conf.site || '',
287
+ }
288
+
289
+ if (pageKeys) params.page_keys = Array.isArray(pageKeys) ? pageKeys.join(',') : pageKeys
290
+ if (limit) params.limit = limit
291
+
292
+ const data = await POST<PageData[]|CommentData[]|object|number>(this.ctx, `${this.baseURL}/stat`, params)
293
+ return data
294
+ }
295
+
277
296
  /** 图片上传 */
278
297
  public async imgUpload(file: File) {
279
298
  const params: any = {
@@ -1,4 +1,4 @@
1
- import Context from '../context'
1
+ import Context from '~/types/context'
2
2
 
3
3
  /** 公共请求函数 */
4
4
  export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit, timeout?: number): Promise<any> {
@@ -10,14 +10,14 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
10
10
  }
11
11
 
12
12
  // 请求操作
13
- const resp = await timeoutFetch(input, timeout || ctx.conf.reqTimeout || 15000, init)
13
+ const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
14
14
 
15
15
  const respHttpCode = resp.status
16
16
  const noAccessCodes = [401, 400]
17
17
  const isNoAccess = noAccessCodes.includes(respHttpCode)
18
18
 
19
19
  if (!resp.ok && !isNoAccess)
20
- throw new Error(`请求响应 ${respHttpCode}`)
20
+ throw new Error(`${ctx.$t('reqGot')} ${respHttpCode}`)
21
21
 
22
22
  // 解析获取响应的 json
23
23
  let json: any = await resp.json()
@@ -33,7 +33,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
33
33
  if (json.data?.need_captcha) {
34
34
  // 请求需要验证码
35
35
  json = await (new Promise<any>((resolve, reject) => {
36
- ctx.trigger('checker-captcha', {
36
+ ctx.checkCaptcha({
37
37
  imgData: json.data.img_data,
38
38
  iframe: json.data.iframe,
39
39
  onSuccess: () => { recall(resolve, reject) },
@@ -43,7 +43,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
43
43
  } else if (json.data?.need_login || isNoAccess) {
44
44
  // 请求需要管理员权限
45
45
  json = await (new Promise<any>((resolve, reject) => {
46
- ctx.trigger('checker-admin', {
46
+ ctx.checkAdmin({
47
47
  onSuccess: () => { recall(resolve, reject) },
48
48
  onCancel: () => { reject(json) }
49
49
  })
@@ -82,7 +82,7 @@ export function ToFormData(object: {[key: string]: any}): FormData {
82
82
  }
83
83
 
84
84
  /** 我靠,fetch 一个 timeout,都要丑陋的实现 */
85
- function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
85
+ function timeoutFetch(ctx: Context, url: RequestInfo, ms: number, opts: RequestInit) {
86
86
  const controller = new AbortController()
87
87
  opts.signal?.addEventListener('abort', () => controller.abort()) // 保留原有 signal 功能
88
88
  let promise = fetch(url, { ...opts, signal: controller.signal })
@@ -91,7 +91,7 @@ function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
91
91
  promise.finally(() => { clearTimeout(timer) })
92
92
  }
93
93
  promise = promise.catch((err) => {
94
- throw ((err || {}).name === 'AbortError') ? new Error('请求超时或意外终止') : err
94
+ throw ((err || {}).name === 'AbortError') ? new Error(ctx.$t('reqAborted')) : err
95
95
  })
96
96
  return promise
97
97
  }
package/src/artalk.ts CHANGED
@@ -2,22 +2,24 @@ import './style/main.less'
2
2
 
3
3
  import ArtalkConfig from '~/types/artalk-config'
4
4
  import { EventPayloadMap, Handler } from '~/types/event'
5
- import Context from './context'
5
+ import Context from '~/types/context'
6
+ import ConcreteContext from './context'
6
7
  import defaults from './defaults'
7
8
 
8
9
  import CheckerLauncher from './lib/checker'
9
- import Editor from './components/editor'
10
- import List from './components/list'
11
- import SidebarLayer from './components/sidebar-layer'
10
+ import Editor from './editor'
11
+ import List from './list'
12
+ import SidebarLayer from './layer/sidebar-layer'
12
13
 
13
- import Layer, { GetLayerWrap } from './components/layer'
14
+ import Layer, { GetLayerWrap } from './layer'
14
15
  import Api from './api'
15
16
  import * as Utils from './lib/utils'
16
17
  import * as Ui from './lib/ui'
17
18
 
18
19
  /**
19
20
  * Artalk
20
- * @link https://artalk.js.org
21
+ *
22
+ * @website https://artalk.js.org
21
23
  */
22
24
  export default class Artalk {
23
25
  public static readonly defaults: ArtalkConfig = defaults
@@ -26,12 +28,13 @@ export default class Artalk {
26
28
  public conf!: ArtalkConfig
27
29
  public $root!: HTMLElement
28
30
 
31
+ public api!: Api
29
32
  public checkerLauncher!: CheckerLauncher
30
33
  public editor!: Editor
31
34
  public list!: List
32
35
  public sidebarLayer!: SidebarLayer
33
36
 
34
- constructor (customConf: ArtalkConfig) {
37
+ constructor(customConf: Partial<ArtalkConfig>) {
35
38
  // 配置
36
39
  this.conf = Utils.mergeDeep(Artalk.defaults, customConf)
37
40
  this.conf.server = this.conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
@@ -53,12 +56,14 @@ export default class Artalk {
53
56
  }
54
57
 
55
58
  // 装载元素
56
- if (!!this.conf.el && this.conf.el instanceof HTMLElement) {
59
+ this.conf.el = this.conf.el || document.createElement('div') // 允许 this.conf.el 为空
60
+
61
+ if (this.conf.el instanceof HTMLElement) {
57
62
  this.$root = this.conf.el
58
63
  } else {
59
64
  try {
60
65
  const $root = document.querySelector<HTMLElement>(this.conf.el)
61
- if (!$root) throw Error(`Sorry, target element "${this.conf.el}" was not found.`)
66
+ if (!$root) throw Error(`Target element "${this.conf.el}" was not found.`)
62
67
  this.$root = $root
63
68
  } catch (e) {
64
69
  console.error(e)
@@ -67,7 +72,11 @@ export default class Artalk {
67
72
  }
68
73
 
69
74
  // Context 初始化
70
- this.ctx = new Context(this.$root, this.conf)
75
+ this.ctx = new ConcreteContext(this.$root, this.conf)
76
+
77
+ // API 初始化
78
+ this.api = new Api(this.ctx)
79
+ this.ctx.setApi(this.api)
71
80
 
72
81
  // 界面初始化
73
82
  this.$root.classList.add('artalk')
@@ -88,22 +97,30 @@ export default class Artalk {
88
97
  }
89
98
 
90
99
  private initCore() {
91
- // 组件初始化
100
+ /* 组件初始化 */
92
101
  this.initLayer()
93
102
  this.initDarkMode()
103
+
104
+ // CheckerLauncher
94
105
  this.checkerLauncher = new CheckerLauncher(this.ctx)
95
- Utils.initMarked(this.ctx) // 初始化 marked
106
+ this.ctx.setCheckerLauncher(this.checkerLauncher)
107
+
108
+ // 初始化 marked
109
+ Utils.initMarked(this.ctx)
96
110
 
97
111
  // 编辑器
98
112
  this.editor = new Editor(this.ctx)
113
+ this.ctx.setEditor(this.editor)
99
114
  this.$root.appendChild(this.editor.$el)
100
115
 
101
116
  // 评论列表
102
117
  this.list = new List(this.ctx)
118
+ this.ctx.setList(this.list)
103
119
  this.$root.appendChild(this.list.$el)
104
120
 
105
121
  // 侧边栏
106
122
  this.sidebarLayer = new SidebarLayer(this.ctx)
123
+ this.ctx.setSidebarLayer(this.sidebarLayer)
107
124
  this.$root.appendChild(this.sidebarLayer.$el)
108
125
 
109
126
  // 评论获取
@@ -113,7 +130,7 @@ export default class Artalk {
113
130
  this.initEventBind()
114
131
 
115
132
  // 其他
116
- this.initPV()
133
+ this.initCountAndPV()
117
134
 
118
135
  // 插件初始化
119
136
  Artalk.Plugins.forEach(plugin => {
@@ -128,8 +145,8 @@ export default class Artalk {
128
145
  Ui.showLoading(this.$root)
129
146
  let backendConf = {}
130
147
  try {
131
- backendConf = await (new Api(this.ctx)).conf()
132
- } catch (err) { console.error("配置远程获取失败", err) }
148
+ backendConf = await this.ctx.getApi().conf()
149
+ } catch (err) { console.error("Load config from remote err", err) }
133
150
  this.ctx.conf = Utils.mergeDeep(this.ctx.conf, backendConf)
134
151
  Ui.hideLoading(this.$root)
135
152
  }
@@ -142,33 +159,10 @@ export default class Artalk {
142
159
  this.list.checkGoToCommentByUrlHash()
143
160
  })
144
161
 
145
- // 仅管理员显示控制
146
- this.ctx.on('check-admin-show-el', () => {
147
- const items: HTMLElement[] = []
148
-
149
- this.$root.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
150
-
151
- // for layer
152
- const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
153
- if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
154
-
155
- // for sidebar
156
- // TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
157
- const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
158
- if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
159
-
160
- items.forEach(($item: HTMLElement) => {
161
- if (this.ctx.user.data.isAdmin)
162
- $item.classList.remove('atk-hide')
163
- else
164
- $item.classList.add('atk-hide')
165
- })
166
- })
167
-
168
162
  // 本地用户数据变更
169
163
  this.ctx.on('user-changed', () => {
170
- this.ctx.trigger('check-admin-show-el')
171
- this.ctx.trigger('list-refresh-ui')
164
+ this.ctx.checkAdminShowEl()
165
+ this.ctx.listRefreshUI()
172
166
  })
173
167
  }
174
168
 
@@ -220,19 +214,43 @@ export default class Artalk {
220
214
  }
221
215
  }
222
216
 
223
- /** PV */
224
- public async initPV() {
225
- const pvNum = await new Api(this.ctx).pv()
226
- if (Number.isNaN(Number(pvNum)))
227
- return
217
+ /** 初始化评论数和 PV 数量展示元素 */
218
+ public async initCountAndPV() {
219
+ // 评论数
220
+ const countEl = this.conf.countEl
221
+ if (countEl && document.querySelector(countEl)) {
222
+ this.handleStatCount({ api: 'page_comment', countEl })
223
+ }
228
224
 
229
- if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
230
- return
225
+ // PV
226
+ const curtPagePvNum = await this.ctx.getApi().pv()
227
+ const pvEl = this.conf.pvEl
228
+ if (pvEl && document.querySelector(pvEl)) {
229
+ this.handleStatCount({ api: 'page_pv', countEl: pvEl, curtPageCount: curtPagePvNum })
230
+ }
231
+ }
231
232
 
232
- const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
233
- // $pv.innerText = '-' // 默认占位符
233
+ private async handleStatCount(args: { api: 'page_pv'|'page_comment', countEl: string, curtPageCount?: number }) {
234
+ let pageCounts: {[key: string]: number} = {}
235
+
236
+ // 当前页面的统计数
237
+ const curtPageKey = this.ctx.conf.pageKey
238
+ if (args.curtPageCount) pageCounts[curtPageKey] = args.curtPageCount
239
+
240
+ // 查询其他页面的统计数
241
+ let queryPageKeys = Array.from(document.querySelectorAll(args.countEl))
242
+ .map(e => e.getAttribute('data-page-key') || curtPageKey)
243
+ .filter(pageKey => pageCounts[pageKey] === undefined)
244
+ queryPageKeys = [...new Set(queryPageKeys)] // 去重
245
+ if (queryPageKeys.length > 0) {
246
+ const counts: any = await this.ctx.getApi().stat(args.api, queryPageKeys)
247
+ pageCounts = { ...pageCounts, ...counts }
248
+ }
234
249
 
235
- $pv.innerText = String(pvNum)
250
+ document.querySelectorAll(args.countEl).forEach((el) => {
251
+ const pageKey = el.getAttribute('data-page-key') || curtPageKey
252
+ el.innerHTML = `${Number(pageCounts[pageKey] || 0)}`
253
+ })
236
254
  }
237
255
 
238
256
  /** 监听事件 */
@@ -0,0 +1,87 @@
1
+ import Api from '../api'
2
+ import Comment from './comment'
3
+ import ActionBtn from '../components/action-btn'
4
+
5
+ export default class CommentActions {
6
+ private comment: Comment
7
+
8
+ private get ctx() { return this.comment.ctx }
9
+ private get data() { return this.comment.getData() }
10
+ private get cConf() { return this.comment.getConf() }
11
+
12
+ public constructor(comment: Comment) {
13
+ this.comment = comment
14
+ }
15
+
16
+ /** 投票操作 */
17
+ public vote(type: 'up'|'down') {
18
+ const actionBtn = (type === 'up') ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown
19
+
20
+ this.ctx.getApi().vote(this.data.id, `comment_${type}`)
21
+ .then((v) => {
22
+ this.data.vote_up = v.up
23
+ this.data.vote_down = v.down
24
+ this.comment.getRender().voteBtnUp?.updateText()
25
+ this.comment.getRender().voteBtnDown?.updateText()
26
+ })
27
+ .catch((err) => {
28
+ actionBtn?.setError(this.ctx.$t('voteFail'))
29
+ console.log(err)
30
+ })
31
+ }
32
+
33
+ /** 管理员 - 评论状态修改 */
34
+ public adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
35
+ if (btnElem.isLoading) return // 若正在修改中
36
+
37
+ btnElem.setLoading(true, `${this.ctx.$t('editing')}...`)
38
+
39
+ // 克隆并修改当前数据
40
+ const modify = { ...this.data }
41
+ if (type === 'collapsed') {
42
+ modify.is_collapsed = !modify.is_collapsed
43
+ } else if (type === 'pending') {
44
+ modify.is_pending = !modify.is_pending
45
+ } else if (type === 'pinned') {
46
+ modify.is_pinned = !modify.is_pinned
47
+ }
48
+
49
+ this.ctx.getApi().commentEdit(modify).then((data) => {
50
+ btnElem.setLoading(false)
51
+
52
+ // 刷新当前 Comment UI
53
+ this.comment.setData(data)
54
+
55
+ // 刷新 List UI
56
+ this.ctx.listRefreshUI()
57
+ }).catch((err) => {
58
+ console.error(err)
59
+ btnElem.setError(this.ctx.$t('editFail'))
60
+ })
61
+ }
62
+
63
+ /** 管理员 - 评论删除 */
64
+ public adminDelete(btnElem: ActionBtn) {
65
+ if (btnElem.isLoading) return // 若正在删除中
66
+
67
+ btnElem.setLoading(true, `${this.ctx.$t('deleting')}...`)
68
+ this.ctx.getApi().commentDel(this.data.id, this.data.site_name)
69
+ .then(() => {
70
+ btnElem.setLoading(false)
71
+ if (this.cConf.onDelete) this.cConf.onDelete(this.comment)
72
+ })
73
+ .catch((e) => {
74
+ console.error(e)
75
+ btnElem.setError(this.ctx.$t('deleteFail'))
76
+ })
77
+ }
78
+
79
+ /** 快速跳转到该评论 */
80
+ public goToReplyComment() {
81
+ const origHash = window.location.hash
82
+ const modifyHash = `#atk-comment-${this.data.rid}`
83
+
84
+ window.location.hash = modifyHash
85
+ if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
86
+ }
87
+ }
@@ -4,7 +4,7 @@
4
4
  <div class="atk-main">
5
5
  <div class="atk-header">
6
6
  <span class="atk-item atk-nick"></span>
7
- <span class="atk-item atk-badge"></span>
7
+ <span class="atk-badge-wrap"></span>
8
8
  <span class="atk-item atk-date"></span>
9
9
  </div>
10
10
  <div class="atk-body">
@@ -0,0 +1,191 @@
1
+ import { CommentData } from '~/types/artalk-data'
2
+ import Context from '~/types/context'
3
+ import Component from '../lib/component'
4
+ import * as Utils from '../lib/utils'
5
+ import UADetect from '../lib/detect'
6
+ import CommentRender from './render'
7
+ import CommentActions from './actions'
8
+
9
+ export interface CommentConf {
10
+ isUnread?: boolean
11
+ openURL?: string
12
+ isFlatMode: boolean
13
+ replyTo?: CommentData
14
+ afterRender?: () => void
15
+ openEvt?: () => void
16
+ onReplyBtnClick?: Function
17
+ onDelete?: Function
18
+ }
19
+
20
+ export default class Comment extends Component {
21
+ private renderInstance: CommentRender
22
+ private actionInstance: CommentActions
23
+
24
+ private data: CommentData
25
+ private cConf: CommentConf
26
+
27
+ private parent: Comment|null
28
+ private children: Comment[] = []
29
+
30
+ private nestCurt: number // 当前嵌套层数
31
+ private nestMax: number // 最大嵌套层数
32
+
33
+ constructor(ctx: Context, data: CommentData, conf: CommentConf) {
34
+ super(ctx)
35
+
36
+ // 最大嵌套数
37
+ this.nestMax = ctx.conf.nestMax || 3
38
+
39
+ this.cConf = conf
40
+ this.data = { ...data }
41
+ this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
42
+
43
+ this.parent = null
44
+ this.nestCurt = 1 // 现在已嵌套 n 层
45
+
46
+ this.actionInstance = new CommentActions(this)
47
+ this.renderInstance = new CommentRender(this)
48
+ }
49
+
50
+ /** 渲染 UI */
51
+ public render() {
52
+ const newEl = this.renderInstance.render()
53
+
54
+ if (this.$el) this.$el.replaceWith(newEl)
55
+ this.$el = newEl
56
+
57
+ if (this.cConf.afterRender) this.cConf.afterRender()
58
+ }
59
+
60
+ /** 获取评论操作实例对象 */
61
+ public getActions() {
62
+ return this.actionInstance
63
+ }
64
+
65
+ /** 获取评论渲染器实例对象 */
66
+ public getRender() {
67
+ return this.renderInstance
68
+ }
69
+
70
+ /** 获取评论数据 */
71
+ public getData() {
72
+ return this.data
73
+ }
74
+
75
+ /** 设置数据 */
76
+ public setData(data: CommentData) {
77
+ this.data = data
78
+
79
+ this.render()
80
+ this.getRender().playFadeAnimForBody()
81
+ }
82
+
83
+ /** 获取父评论 */
84
+ public getParent() {
85
+ return this.parent
86
+ }
87
+
88
+ /** 获取所有子评论 */
89
+ public getChildren() {
90
+ return this.children
91
+ }
92
+
93
+ /** 获取当前嵌套层数 */
94
+ public getNestCurt() {
95
+ return this.nestCurt
96
+ }
97
+
98
+ /** 判断是否为根评论 */
99
+ public getIsRoot() {
100
+ return this.data.rid === 0
101
+ }
102
+
103
+ /** 获取评论 ID */
104
+ public getID() {
105
+ return this.data.id
106
+ }
107
+
108
+ /** 置入子评论 */
109
+ public putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
110
+ childC.parent = this
111
+ childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
112
+
113
+ this.children.push(childC)
114
+
115
+ const $children = this.getChildrenEl()
116
+ if (insertMode === 'append') $children.append(childC.getEl())
117
+ else if (insertMode === 'prepend') $children.prepend(childC.getEl())
118
+
119
+ childC.getRender().playFadeAnim()
120
+
121
+ // 内容限高
122
+ childC.getRender().checkHeightLimitArea('content')
123
+ }
124
+
125
+ /** 获取存放子评论的元素对象 */
126
+ public getChildrenEl(): HTMLElement {
127
+ let $children = this.getRender().getChildrenWrap()
128
+
129
+ if (!$children) {
130
+ // console.log(this.nestCurt)
131
+ if (this.nestCurt < this.nestMax) {
132
+ $children = this.getRender().renderChildrenWrap()
133
+ } else {
134
+ $children = this.parent!.getChildrenEl()
135
+ }
136
+ }
137
+
138
+ return $children
139
+ }
140
+
141
+ /** 获取所有父评论 */
142
+ public getParents() {
143
+ const parents: Comment[] = []
144
+ const once = (c: Comment) => {
145
+ if (c.parent) {
146
+ parents.push(c.parent)
147
+ once(c.parent)
148
+ }
149
+ }
150
+
151
+ once(this)
152
+ return parents
153
+ }
154
+
155
+ /** 获取评论元素对象 */
156
+ public getEl() {
157
+ return this.$el
158
+ }
159
+
160
+ /** 获取 Gravatar 头像 URL */
161
+ public getGravatarURL() {
162
+ return Utils.getGravatarURL(this.ctx, this.data.email_encrypted)
163
+ }
164
+
165
+ /** 获取评论 markdown 解析后的内容 */
166
+ public getContentMarked() {
167
+ return Utils.marked(this.ctx, this.data.content)
168
+ }
169
+
170
+ /** 获取格式化后的日期 */
171
+ public getDateFormatted() {
172
+ return Utils.timeAgo(new Date(this.data.date), this.ctx)
173
+ }
174
+
175
+ /** 获取用户 UserAgent 浏览器 */
176
+ public getUserUaBrowser() {
177
+ const info = UADetect(this.data.ua)
178
+ return `${info.browser} ${info.version}`
179
+ }
180
+
181
+ /** 获取用户 UserAgent 系统 */
182
+ public getUserUaOS() {
183
+ const info = UADetect(this.data.ua)
184
+ return `${info.os} ${info.osVersion}`
185
+ }
186
+
187
+ /** 获取配置 */
188
+ public getConf() {
189
+ return this.cConf
190
+ }
191
+ }
@@ -0,0 +1,3 @@
1
+ import Comment from './comment'
2
+
3
+ export default Comment