artalk 2.2.9 → 2.2.12

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/index.html CHANGED
@@ -38,7 +38,9 @@
38
38
  <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
39
39
  <script>
40
40
  // VConsole will be exported to `window.VConsole` by default.
41
- var vConsole = new window.VConsole();
41
+ if (navigator.userAgentData.mobile) {
42
+ var vConsole = new window.VConsole();
43
+ }
42
44
  </script>
43
45
  </head>
44
46
  <body>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.9",
4
- "backendMinVersion": "2.1.4",
3
+ "version": "2.2.12",
4
+ "backendMinVersion": "2.1.6",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
@@ -29,7 +29,8 @@
29
29
  "dependencies": {
30
30
  "hanabi": "^0.4.0",
31
31
  "insane": "^2.6.2",
32
- "marked": "^4.0.14"
32
+ "marked": "^4.0.14",
33
+ "abortcontroller-polyfill": "^1.7.3"
33
34
  },
34
35
  "scripts": {
35
36
  "dev": "vite",
package/src/api/index.ts CHANGED
@@ -3,6 +3,7 @@ import ArtalkConfig from '~/types/artalk-config'
3
3
  import Context from '../context'
4
4
  import { Fetch, ToFormData, POST, GET } from './request'
5
5
  import * as Utils from '../lib/utils'
6
+ import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
6
7
 
7
8
  export default class Api {
8
9
  private ctx: Context
@@ -95,8 +96,6 @@ export default class Api {
95
96
  /** 用户 · 获取 */
96
97
  public userGet(name: string, email: string) {
97
98
  const ctrl = new AbortController()
98
- const { signal } = ctrl
99
-
100
99
  const params: any = {
101
100
  name, email, site_name: (this.ctx.conf.site || '')
102
101
  }
@@ -104,7 +103,7 @@ export default class Api {
104
103
  const req = Fetch(this.ctx, `${this.baseURL}/user-get`, {
105
104
  method: 'POST',
106
105
  body: ToFormData(params),
107
- signal,
106
+ signal: ctrl.signal,
108
107
  }).then((json) => ({
109
108
  user: json.data.user as UserData|null,
110
109
  is_login: json.data.is_login as boolean,
@@ -307,6 +306,8 @@ export default class Api {
307
306
  conf.emoticons = conf.emoticons.trim()
308
307
  if (conf.emoticons.startsWith("[") || conf.emoticons.startsWith("{")) {
309
308
  conf.emoticons = JSON.parse(conf.emoticons) // pase json
309
+ } else if (conf.emoticons === "false") {
310
+ conf.emoticons = false
310
311
  }
311
312
  }
312
313
 
@@ -4,81 +4,55 @@ import Context from '../context'
4
4
  export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit, timeout?: number): Promise<any> {
5
5
  // JWT
6
6
  if (ctx.user.data.token) {
7
- const requestHeaders: HeadersInit = new Headers()
8
- requestHeaders.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
-
10
- init.headers = requestHeaders
7
+ const headers = new Headers(init.headers) // 保留原有 headers
8
+ headers.set('Authorization', `Bearer ${ctx.user.data.token}`)
9
+ init.headers = headers
11
10
  }
12
11
 
13
12
  // 请求操作
14
- try {
13
+ const resp = await timeoutFetch(input, timeout || ctx.conf.reqTimeout || 15000, init)
15
14
 
16
- let resp: Response
17
- if ((typeof timeout !== 'number' && ctx.conf.reqTimeout === 0) || timeout === 0) {
18
- resp = await fetch(input, init)
19
- } else {
20
- // 请求超时检测
21
- resp = await timeoutPromise(timeout || ctx.conf.reqTimeout || 15000, fetch(input, init))
22
- }
15
+ const respHttpCode = resp.status
16
+ const noAccessCodes = [401, 400]
17
+ const isNoAccess = noAccessCodes.includes(respHttpCode)
23
18
 
24
- const noAccessCodes = [401, 400]
19
+ if (!resp.ok && !isNoAccess)
20
+ throw new Error(`请求响应 ${respHttpCode}`)
25
21
 
26
- if (!resp.ok && !noAccessCodes.includes(resp.status)) throw new Error(`请求响应 ${resp.status}`)
22
+ // 解析获取响应的 json
23
+ let json: any = await resp.json()
27
24
 
28
- // 解析获取响应的 json
29
- let json: any = await resp.json()
25
+ // 重新发起请求
26
+ const recall = (resolve, reject) => {
27
+ Fetch(ctx, input, init)
28
+ .then(d => { resolve(d) })
29
+ .catch(e => { reject(e) })
30
+ }
30
31
 
31
- // 重新发起请求
32
- const recall = (resolve, reject) => {
33
- Fetch(ctx, input, init).then(d => {
34
- resolve(d)
35
- }).catch(err => {
36
- reject(err)
32
+ // 请求弹出层验证
33
+ if (json.data?.need_captcha) {
34
+ // 请求需要验证码
35
+ json = await (new Promise<any>((resolve, reject) => {
36
+ ctx.trigger('checker-captcha', {
37
+ imgData: json.data.img_data,
38
+ iframe: json.data.iframe,
39
+ onSuccess: () => { recall(resolve, reject) },
40
+ onCancel: () => { reject(json) }
37
41
  })
38
- }
39
-
40
- // 请求弹出层验证
41
- if (json.data && json.data.need_captcha) {
42
- // 请求需要验证码
43
- json = await (new Promise<any>((resolve, reject) => {
44
- ctx.trigger('checker-captcha', {
45
- imgData: json.data.img_data,
46
- iframe: json.data.iframe,
47
- onSuccess: () => {
48
- recall(resolve, reject)
49
- },
50
- onCancel: () => {
51
- reject(json)
52
- }
53
- })
54
- }))
55
- } else if ((json.data && json.data.need_login) || noAccessCodes.includes(resp.status)) {
56
- // 请求需要管理员权限
57
- json = await (new Promise<any>((resolve, reject) => {
58
- ctx.trigger('checker-admin', {
59
- onSuccess: () => {
60
- recall(resolve, reject)
61
- },
62
- onCancel: () => {
63
- reject(json)
64
- }
65
- })
66
- }))
67
- }
68
-
69
- if (!json.success) throw json // throw 相当于 reject(json)
70
-
71
- return json
72
-
73
- } catch (err) {
74
- // 错误处理
75
- console.error(err)
42
+ }))
43
+ } else if (json.data?.need_login || isNoAccess) {
44
+ // 请求需要管理员权限
45
+ json = await (new Promise<any>((resolve, reject) => {
46
+ ctx.trigger('checker-admin', {
47
+ onSuccess: () => { recall(resolve, reject) },
48
+ onCancel: () => { reject(json) }
49
+ })
50
+ }))
51
+ }
76
52
 
77
- if (err instanceof TypeError)
78
- throw new Error(`网络错误`)
53
+ if (!json.success) throw json // throw 相当于 reject(json)
79
54
 
80
- throw err
81
- }
55
+ return json
82
56
  }
83
57
 
84
58
  /** 公共 POST 请求 */
@@ -108,21 +82,16 @@ export function ToFormData(object: {[key: string]: any}): FormData {
108
82
  }
109
83
 
110
84
  /** 我靠,fetch 一个 timeout,都要丑陋的实现 */
111
- function timeoutPromise<T>(ms: number, promise: Promise<T>): Promise<T> {
112
- return new Promise((resolve, reject) => {
113
- const timeoutId = setTimeout(() => {
114
- reject(new Error("请求超时"))
115
- }, ms)
116
-
117
- promise.then(
118
- (res) => {
119
- clearTimeout(timeoutId)
120
- resolve(res)
121
- },
122
- (err) => {
123
- clearTimeout(timeoutId)
124
- reject(err)
125
- }
126
- )
85
+ function timeoutFetch(url: RequestInfo, ms: number, opts: RequestInit) {
86
+ const controller = new AbortController()
87
+ opts.signal?.addEventListener('abort', () => controller.abort()) // 保留原有 signal 功能
88
+ let promise = fetch(url, { ...opts, signal: controller.signal })
89
+ if (ms > 0) {
90
+ const timer = setTimeout(() => controller.abort(), ms)
91
+ promise.finally(() => { clearTimeout(timer) })
92
+ }
93
+ promise = promise.catch((err) => {
94
+ throw ((err || {}).name === 'AbortError') ? new Error('请求超时或意外终止') : err
127
95
  })
96
+ return promise
128
97
  }
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
@@ -181,8 +186,23 @@ export default class Artalk {
181
186
 
182
187
  /** 暗黑模式 · 初始化 */
183
188
  public initDarkMode() {
189
+ if (this.conf.darkMode === 'auto') {
190
+ // 自动切换暗黑模式,事件监听
191
+ const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)')
192
+ darkModeMedia.addEventListener('change', (e) => { this.setDarkMode(e.matches) })
193
+ this.setDarkMode(darkModeMedia.matches)
194
+ } else {
195
+ this.setDarkMode(this.conf.darkMode || false)
196
+ }
197
+ }
198
+
199
+ /** 暗黑模式 · 设定 */
200
+ public setDarkMode(darkMode: boolean) {
184
201
  const darkModeClassName = 'atk-dark-mode'
185
202
 
203
+ this.ctx.conf.darkMode = darkMode
204
+ this.ctx.trigger('conf-updated')
205
+
186
206
  if (this.conf.darkMode) {
187
207
  this.$root.classList.add(darkModeClassName)
188
208
  } else {
@@ -200,13 +220,6 @@ export default class Artalk {
200
220
  }
201
221
  }
202
222
 
203
- /** 暗黑模式 · 设定 */
204
- public setDarkMode(darkMode: boolean) {
205
- this.ctx.conf.darkMode = darkMode
206
- this.ctx.trigger('conf-updated')
207
- this.initDarkMode()
208
- }
209
-
210
223
  /** PV */
211
224
  public async initPV() {
212
225
  const pvNum = await new Api(this.ctx).pv()
@@ -16,6 +16,7 @@ export default class Comment extends Component {
16
16
  public $main!: HTMLElement
17
17
  public $header!: HTMLElement
18
18
  public $headerNick!: HTMLElement
19
+ public $headerBadge?: HTMLElement
19
20
  public $body!: HTMLElement
20
21
  public $content!: HTMLElement
21
22
  public $children!: HTMLElement|null
@@ -132,13 +133,14 @@ export default class Comment extends Component {
132
133
  this.$headerNick.innerText = this.data.nick
133
134
  }
134
135
 
135
- const $badge = this.$el.querySelector<HTMLElement>('.atk-badge')!
136
+ this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
136
137
  if (this.data.badge_name) {
137
- $badge.innerText = this.data.badge_name
138
+ this.$headerBadge.innerText = this.data.badge_name
138
139
  if (this.data.badge_color)
139
- $badge.style.backgroundColor = this.data.badge_color
140
+ this.$headerBadge.style.backgroundColor = this.data.badge_color
140
141
  } else {
141
- $badge.remove()
142
+ this.$headerBadge.remove()
143
+ this.$headerBadge = undefined
142
144
  }
143
145
 
144
146
  if (this.data.is_pinned) {
@@ -204,7 +206,8 @@ export default class Comment extends Component {
204
206
  this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.replyTo.nick}`
205
207
  this.$replyAt.onclick = () => { this.goToReplyComment() }
206
208
 
207
- this.$headerNick.insertAdjacentElement('afterend', this.$replyAt)
209
+ const $after = this.$headerBadge || this.$headerNick
210
+ $after.insertAdjacentElement('afterend', this.$replyAt)
208
211
  }
209
212
 
210
213
  // 回复的对象
@@ -153,6 +153,7 @@ export default class Editor extends Component {
153
153
  this.getInputEl('link')!.value = data.user.link
154
154
  }
155
155
  })
156
+ .catch(() => {})
156
157
  .finally(() => {
157
158
  this.queryUserInfo.abortFunc = null // 清理
158
159
  })
@@ -213,22 +214,14 @@ export default class Editor extends Component {
213
214
  this.openedPlugName = null
214
215
  this.$plugBtnWrap.innerHTML = ''
215
216
 
216
- // 依次实例化 plug
217
+ // 初始化 plug 按钮
217
218
  this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
219
+ if (PlugObj.Name === 'emoticons' && !this.conf.emoticons) return
220
+
218
221
  // 切换按钮
219
222
  const btnElem = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${PlugObj.Name}">${PlugObj.BtnHTML}</span>`)
220
223
  this.$plugBtnWrap.appendChild(btnElem)
221
224
 
222
- // 表情包插件预加载
223
- if (PlugObj.Name === 'emoticons') {
224
- const emoPlug = new PlugObj(this) as EmoticonsPlug
225
- this.plugList[PlugObj.Name] = emoPlug
226
-
227
- window.setTimeout(() => {
228
- emoPlug.loadEmoticonsData()
229
- }, 1000) // 延迟 1s 加载
230
- }
231
-
232
225
  btnElem.addEventListener('click', () => {
233
226
  let plug = this.plugList[PlugObj.Name]
234
227
  if (!plug) {
@@ -272,6 +265,16 @@ export default class Editor extends Component {
272
265
  })
273
266
  })
274
267
 
268
+ // 表情包插件预加载
269
+ if (this.conf.emoticons) {
270
+ const emoPlug = new EmoticonsPlug(this)
271
+ this.plugList[EmoticonsPlug.Name] = emoPlug
272
+
273
+ window.setTimeout(() => {
274
+ emoPlug.loadEmoticonsData()
275
+ }, 1000) // 延迟 1s 加载
276
+ }
277
+
275
278
  this.initImgUpload()
276
279
  }
277
280
 
@@ -25,7 +25,7 @@ export default class ListLite extends Component {
25
25
  public flatMode = false
26
26
 
27
27
  /** 嵌套模式 */
28
- public nestSortBy: ListNest.SortByType = 'DATE_DESC'
28
+ public nestSortBy!: ListNest.SortByType
29
29
 
30
30
  /** 分页 */
31
31
  public pageMode: 'pagination'|'read-more' = 'pagination'
@@ -58,6 +58,9 @@ export default class ListLite extends Component {
58
58
  // 评论为空时显示字符
59
59
  if (ctx.conf.noComment) this.noCommentText = ctx.conf.noComment
60
60
 
61
+ // 嵌套排序方式
62
+ this.nestSortBy = this.ctx.conf.nestSort || 'DATE_ASC'
63
+
61
64
  // 评论时间自动更新
62
65
  window.setInterval(() => {
63
66
  this.$el.querySelectorAll<HTMLElement>('[data-atk-comment-date]').forEach(el => {
@@ -305,6 +308,12 @@ export default class ListLite extends Component {
305
308
  // 子评论查找回复对象
306
309
  comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
307
310
 
311
+ // 渲染元素
312
+ comment.render()
313
+
314
+ // 放入 comment 总表中
315
+ this.commentList.push(comment)
316
+
308
317
  return comment
309
318
  }
310
319
 
@@ -325,28 +334,21 @@ export default class ListLite extends Component {
325
334
  const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.nestSortBy, this.conf.nestMax)
326
335
  rootNodes.forEach((rootNode: ListNest.CommentNode) => {
327
336
  const rootC = this.createComment(rootNode.comment, srcData)
328
- rootC.render()
329
337
 
330
338
  // 显示并播放渐入动画
331
339
  this.$commentsWrap.appendChild(rootC.getEl())
332
340
  rootC.playFadeInAnim()
333
341
 
334
- // 评论放入 comments 总表中
335
- this.commentList.push(rootC)
336
-
337
342
  // 加载子评论
338
343
  const that = this
339
344
  ;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
340
345
  parentNode.children.forEach((node: ListNest.CommentNode) => {
341
346
  const childD = node.comment
342
347
  const childC = that.createComment(childD, srcData)
343
- childC.render()
344
348
 
345
349
  // 插入到父评论中
346
350
  parentC.putChild(childC)
347
351
 
348
- that.commentList.push(childC)
349
-
350
352
  // 递归加载子评论
351
353
  loadChildren(childC, node)
352
354
  })
@@ -361,10 +363,6 @@ export default class ListLite extends Component {
361
363
  private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
362
364
  if (cData.is_collapsed) cData.is_allow_reply = false
363
365
  const comment = this.createComment(cData, ctxData)
364
- comment.render()
365
-
366
- // 将评论导入 comments 总表中
367
- this.commentList.push(comment)
368
366
 
369
367
  // 可见评论添加到界面
370
368
  // 注:不可见评论用于显示 “引用内容”
@@ -388,14 +386,11 @@ export default class ListLite extends Component {
388
386
 
389
387
  if (commentData.rid === 0) {
390
388
  // root评论 新增
391
- comment.render()
392
389
  this.$commentsWrap.prepend(comment.getEl())
393
- this.commentList.push(comment)
394
390
  } else {
395
391
  // 子评论 新增
396
392
  const parent = this.findComment(commentData.rid)
397
393
  if (parent) {
398
- comment.render()
399
394
  parent.putChild(comment, (this.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
400
395
 
401
396
  // 若父评论存在 “子评论部分” 限高,取消限高
package/src/defaults.ts CHANGED
@@ -9,9 +9,13 @@ const defaults: ArtalkConfig = {
9
9
  placeholder: '键入内容...',
10
10
  noComment: '「此时无声胜有声」',
11
11
  sendBtn: '发送评论',
12
- darkMode: false,
12
+ darkMode: 'auto',
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,6 @@ const defaults: ArtalkConfig = {
19
23
  uaBadge: true,
20
24
  listSort: true,
21
25
  pvEl: '#ArtalkPV',
22
- flatMode: 'auto',
23
- nestMax: 3,
24
26
 
25
27
  gravatar: {
26
28
  default: 'mp',
@@ -27,7 +27,7 @@ export default interface ArtalkConfig {
27
27
  editorTravel?: boolean
28
28
 
29
29
  /** 表情包 */
30
- emoticons?: any
30
+ emoticons?: object|any[]|string|false
31
31
 
32
32
  /** Gravatar 头像 */
33
33
  gravatar?: {
@@ -66,7 +66,7 @@ export default interface ArtalkConfig {
66
66
  pvEl?: string
67
67
 
68
68
  /** 暗黑模式 */
69
- darkMode?: boolean
69
+ darkMode?: boolean|'auto'
70
70
 
71
71
  /** 请求超时(单位:秒) */
72
72
  reqTimeout?: number
@@ -77,6 +77,9 @@ export default interface ArtalkConfig {
77
77
  /** 嵌套模式 · 最大层数 */
78
78
  nestMax?: number
79
79
 
80
+ /** 嵌套模式 · 排序方式 */
81
+ nestSort?: 'DATE_ASC'|'DATE_DESC'
82
+
80
83
  /** 显示 UA 徽标 */
81
84
  uaBadge?: boolean
82
85
 
package/vite.config.ts CHANGED
@@ -15,18 +15,12 @@ export default defineConfig({
15
15
  formats: ["es", "umd", "iife"]
16
16
  },
17
17
  rollupOptions: {
18
- // 确保外部化处理那些你不想打包进库的依赖
19
- external: ['vue'],
20
18
  output: {
21
19
  assetFileNames: assetInfo => {
22
20
  if (/\.css$/.test(assetInfo.name)) {
23
21
  return 'Artalk.css'
24
22
  }
25
23
  return "[name].[ext]"
26
- },
27
- // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
28
- globals: {
29
- vue: 'Vue'
30
24
  }
31
25
  }
32
26
  }