artalk 2.4.3 → 2.5.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 (145) hide show
  1. package/README.md +27 -0
  2. package/dist/Artalk.css +1 -1
  3. package/dist/Artalk.es.js +4895 -4067
  4. package/dist/Artalk.iife.js +1 -1
  5. package/dist/Artalk.js +1 -1
  6. package/dist/ArtalkLite.css +1 -1
  7. package/dist/ArtalkLite.es.js +2496 -1779
  8. package/dist/ArtalkLite.iife.js +1 -1
  9. package/dist/ArtalkLite.js +1 -1
  10. package/dist/i18n/jp.js +1 -1
  11. package/dist/i18n/zh-TW.js +1 -1
  12. package/dist/src/api/index.d.ts +2 -2
  13. package/dist/src/api/system.d.ts +5 -2
  14. package/dist/src/artalk.d.ts +42 -34
  15. package/dist/src/comment/comment.d.ts +2 -2
  16. package/dist/src/comment/height-limit.d.ts +30 -0
  17. package/dist/src/comment/render-ctx.d.ts +23 -0
  18. package/dist/src/comment/render.d.ts +2 -50
  19. package/dist/src/comment/renders/actions.d.ts +5 -0
  20. package/dist/src/comment/renders/avatar.d.ts +5 -0
  21. package/dist/src/comment/renders/content.d.ts +5 -0
  22. package/dist/src/comment/renders/header.d.ts +5 -0
  23. package/dist/src/comment/renders/index.d.ts +2 -0
  24. package/dist/src/comment/renders/pending.d.ts +5 -0
  25. package/dist/src/comment/renders/reply-at.d.ts +5 -0
  26. package/dist/src/comment/renders/reply-to.d.ts +5 -0
  27. package/dist/src/components/dialog.d.ts +1 -1
  28. package/dist/src/config.d.ts +8 -0
  29. package/dist/src/context.d.ts +15 -29
  30. package/dist/src/editor/edit-mode.d.ts +9 -0
  31. package/dist/src/editor/editor-ui.d.ts +25 -0
  32. package/dist/src/editor/editor.d.ts +37 -45
  33. package/dist/src/editor/funcs.d.ts +2 -0
  34. package/dist/src/editor/mover.d.ts +8 -0
  35. package/dist/src/editor/plug-manager.d.ts +17 -0
  36. package/dist/src/editor/plugs/editor-plug.d.ts +13 -1
  37. package/dist/src/editor/reply.d.ts +9 -0
  38. package/dist/src/editor/submit.d.ts +16 -0
  39. package/dist/src/editor/ui.d.ts +25 -0
  40. package/dist/src/i18n/index.d.ts +11 -7
  41. package/dist/src/layer/index.d.ts +2 -2
  42. package/dist/src/layer/layer.d.ts +1 -1
  43. package/dist/src/lib/checker/captcha-renders.d.ts +5 -0
  44. package/dist/src/lib/checker/index.d.ts +18 -10
  45. package/dist/src/lib/dark-mode.d.ts +4 -0
  46. package/dist/src/lib/marked.d.ts +9 -0
  47. package/dist/src/lib/utils.d.ts +1 -5
  48. package/dist/src/list/list-lite.d.ts +15 -11
  49. package/dist/src/list/list-nest.d.ts +2 -2
  50. package/dist/src/list/list-ui.d.ts +7 -0
  51. package/dist/src/list/paginator/adaptors/index.d.ts +12 -0
  52. package/dist/src/list/paginator/adaptors/pagination.d.ts +9 -0
  53. package/dist/src/list/paginator/adaptors/read-more.d.ts +10 -0
  54. package/dist/src/list/paginator/holder.d.ts +16 -0
  55. package/dist/src/list/paginator/index.d.ts +10 -0
  56. package/dist/src/service.d.ts +40 -0
  57. package/dist/types/artalk-config.d.ts +5 -2
  58. package/dist/types/artalk-data.d.ts +3 -3
  59. package/dist/types/context.d.ts +11 -17
  60. package/dist/types/global.d.ts +1 -0
  61. package/package.json +6 -7
  62. package/src/api/system.ts +3 -12
  63. package/src/artalk.ts +101 -158
  64. package/src/comment/comment.ts +7 -6
  65. package/src/comment/height-limit.ts +100 -0
  66. package/src/comment/render-ctx.ts +30 -0
  67. package/src/comment/render.ts +31 -375
  68. package/src/comment/renders/actions.ts +114 -0
  69. package/src/comment/renders/avatar.ts +22 -0
  70. package/src/comment/renders/content.ts +39 -0
  71. package/src/comment/renders/header.ts +84 -0
  72. package/src/comment/renders/index.ts +19 -0
  73. package/src/comment/renders/pending.ts +12 -0
  74. package/src/comment/renders/reply-at.ts +16 -0
  75. package/src/comment/renders/reply-to.ts +24 -0
  76. package/src/config.ts +65 -0
  77. package/src/context.ts +31 -88
  78. package/src/defaults.ts +1 -0
  79. package/src/editor/edit-mode.ts +102 -0
  80. package/src/editor/editor.ts +100 -394
  81. package/src/editor/funcs.ts +96 -0
  82. package/src/editor/mover.ts +43 -0
  83. package/src/editor/plug-manager.ts +172 -0
  84. package/src/editor/plugs/editor-plug.ts +20 -2
  85. package/src/editor/plugs/{emoticons-plug.less → emoticons-plug.scss} +5 -4
  86. package/src/editor/plugs/emoticons-plug.ts +2 -1
  87. package/src/editor/plugs/header-input-plug.ts +5 -5
  88. package/src/editor/plugs/{preview-plug.less → preview-plug.scss} +1 -1
  89. package/src/editor/plugs/preview-plug.ts +8 -4
  90. package/src/editor/plugs/upload-plug.ts +10 -13
  91. package/src/editor/reply.ts +62 -0
  92. package/src/editor/submit.ts +98 -0
  93. package/src/editor/ui.ts +36 -0
  94. package/src/i18n/index.ts +21 -10
  95. package/src/layer/index.ts +2 -2
  96. package/src/layer/layer.ts +4 -4
  97. package/src/layer/sidebar-layer.ts +2 -1
  98. package/src/lib/checker/admin.ts +36 -0
  99. package/src/lib/checker/captcha-renders.ts +68 -0
  100. package/src/lib/checker/captcha.ts +26 -0
  101. package/src/lib/checker/index.ts +38 -20
  102. package/src/lib/dark-mode.ts +48 -0
  103. package/src/lib/marked.ts +133 -0
  104. package/src/lib/utils.ts +1 -123
  105. package/src/list/list-lite.ts +59 -162
  106. package/src/list/list-ui.ts +80 -0
  107. package/src/list/list.ts +16 -8
  108. package/src/list/paginator/adaptors/index.ts +21 -0
  109. package/src/list/paginator/adaptors/pagination.ts +42 -0
  110. package/src/list/paginator/adaptors/read-more.ts +56 -0
  111. package/src/list/paginator/holder.ts +55 -0
  112. package/src/list/paginator/index.ts +13 -0
  113. package/src/service.ts +118 -0
  114. package/src/style/{_color.less → _color.scss} +3 -3
  115. package/src/style/{_extend.less → _extend.scss} +1 -1
  116. package/src/style/_variables.scss +3 -0
  117. package/src/style/{comment.less → comment.scss} +36 -12
  118. package/src/style/{editor.less → editor.scss} +11 -11
  119. package/src/style/{list.less → list.scss} +8 -6
  120. package/src/style/{main.less → main.scss} +24 -23
  121. package/src/style/{sidebar-layer.less → sidebar-layer.scss} +8 -8
  122. package/types/artalk-config.d.ts +5 -2
  123. package/types/artalk-data.d.ts +3 -3
  124. package/types/context.d.ts +11 -17
  125. package/types/global.d.ts +1 -0
  126. package/CNAME +0 -1
  127. package/LICENSE +0 -165
  128. package/index.html +0 -157
  129. package/postcss.config.js +0 -5
  130. package/scripts/build-i18n.mjs +0 -46
  131. package/src/lib/checker/admin-checker.ts +0 -36
  132. package/src/lib/checker/captcha-checker.ts +0 -84
  133. package/src/style/_variables.less +0 -3
  134. package/test/ArtalkCount.html +0 -16
  135. package/test/ArtalkI18n.html +0 -16
  136. package/test/ArtalkLite.html +0 -15
  137. package/test/CSRF.html +0 -26
  138. package/tsconfig.json +0 -12
  139. package/vite-base.config.ts +0 -21
  140. package/vite-demo.config.ts +0 -12
  141. package/vite-lite.config.ts +0 -22
  142. package/vite.config.ts +0 -27
  143. /package/dist/src/lib/checker/{admin-checker.d.ts → admin.d.ts} +0 -0
  144. /package/dist/src/lib/checker/{captcha-checker.d.ts → captcha.d.ts} +0 -0
  145. /package/src/style/{pagination.less → pagination.scss} +0 -0
@@ -0,0 +1,133 @@
1
+ import { marked as libMarked } from 'marked'
2
+ import insane from 'insane'
3
+ import hanabi from 'hanabi'
4
+ import Context from '~/types/context'
5
+
6
+ let instance: (typeof libMarked)|undefined
7
+
8
+ export type TMarked = typeof libMarked
9
+
10
+ /** Get Marked instance */
11
+ export function getInstance() {
12
+ return instance
13
+ }
14
+
15
+ /** 初始化 marked */
16
+ export function initMarked() {
17
+ try { if (!libMarked.name) return } catch { return }
18
+
19
+ const renderer = new libMarked.Renderer()
20
+ const orgLinkRenderer = renderer.link
21
+ renderer.link = (href, title, text) => {
22
+ const localLink = href?.startsWith(`${window.location.protocol}//${window.location.hostname}`);
23
+ const html = orgLinkRenderer.call(renderer as any, href, title, text);
24
+ return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
25
+ }
26
+
27
+ renderer.code = (block, lang) => {
28
+ // Colorize the block only if the language is known to highlight.js
29
+ const realLang = (!lang ? 'plaintext' : lang)
30
+ let colorized = block
31
+ if ((window as any).hljs) {
32
+ if (realLang && (window as any).hljs.getLanguage(realLang)) {
33
+ colorized = (window as any).hljs.highlight(realLang, block).value
34
+ }
35
+ } else {
36
+ colorized = hanabi(block)
37
+ }
38
+
39
+ return `<pre rel="${realLang}">\n`
40
+ + `<code class="hljs language-${realLang}">${colorized.replace(/&amp;/g, '&')}</code>\n`
41
+ + `</pre>`
42
+ }
43
+
44
+ // @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
45
+ const nMarked = libMarked
46
+ libMarked.setOptions({
47
+ renderer,
48
+ pedantic: false,
49
+ gfm: true,
50
+ breaks: true,
51
+ smartLists: true,
52
+ smartypants: true,
53
+ xhtml: false,
54
+ sanitize: false,
55
+ silent: true,
56
+ })
57
+
58
+ instance = nMarked
59
+ }
60
+
61
+ /** 解析 markdown */
62
+ export default function marked(ctx: Context, src: string): string {
63
+ let markedContent = getInstance()?.parse(src)
64
+ if (!markedContent) {
65
+ // 无 Markdown 模式简单处理
66
+ markedContent = src
67
+ // .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
68
+ .replace(/```\s*([^]+?.*?[^]+?[^]+?)```/g, (_, code) => `<pre><code>${hanabi(code)}</code></pre>`)
69
+ // .replace(/`([^`]+?)`/g, '<code>$1</code>')
70
+ .replace(/!\[(.*?)\]\((.*?)\)/g, (_, alt, imgSrc) => `<img src="${imgSrc}" alt="${alt}" />`)
71
+ .replace(/\[(.*?)\]\((.*?)\)/g, (_, text, link) => `<a href="${link}" target="_blank">${text}</a>`)
72
+ .replace(/\n/g, '<br>')
73
+ }
74
+
75
+ // @link https://github.com/markedjs/marked/discussions/1232
76
+ // @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225
77
+ let dest = insane(markedContent, {
78
+ allowedClasses: {},
79
+ // @refer CVE-2018-8495
80
+ // @link https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-8495
81
+ // @link https://leucosite.com/Microsoft-Edge-RCE/
82
+ // @link https://medium.com/@knownsec404team/analysis-of-the-security-issues-of-url-scheme-in-pc-from-cve-2018-8495-934478a36756
83
+ allowedSchemes: [
84
+ 'http', 'https', 'mailto',
85
+ 'data' // for support base64 encoded image (安全性有待考虑)
86
+ ],
87
+ allowedTags: [
88
+ 'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
89
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
90
+ 'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
91
+ 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'
92
+ ],
93
+ allowedAttributes: {
94
+ '*': ['title', 'accesskey'],
95
+ a: ['href', 'name', 'target', 'aria-label', 'rel'],
96
+ img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label'],
97
+ // for code highlight
98
+ code: ['class'],
99
+ span: ['class', 'style'],
100
+ },
101
+ filter: node => {
102
+ // allow hljs style
103
+ const allowed = [
104
+ [ 'code', /^hljs\W+language-(.*)$/ ],
105
+ [ 'span', /^(hljs-.*)$/ ]
106
+ ]
107
+ allowed.forEach(([ tag, reg ]) => {
108
+ if (
109
+ node.tag === tag
110
+ && !!node.attrs.class
111
+ && !(reg as RegExp).test(node.attrs.class)
112
+ ) {
113
+ delete node.attrs.class
114
+ }
115
+ })
116
+
117
+ // allow <span> set color sty
118
+ if (node.tag === 'span' && !!node.attrs.style
119
+ && !/^color:(\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)) {
120
+ delete node.attrs.style
121
+ }
122
+
123
+ return true
124
+ }
125
+ })
126
+
127
+ // 内容替换器
128
+ ctx.markedReplacers.forEach((replacer) => {
129
+ if (typeof replacer === 'function') dest = replacer(dest)
130
+ })
131
+
132
+ return dest
133
+ }
package/src/lib/utils.ts CHANGED
@@ -1,6 +1,3 @@
1
- import { marked as libMarked } from 'marked'
2
- import insane from 'insane'
3
- import hanabi from 'hanabi'
4
1
  import Context from '~/types/context'
5
2
 
6
3
  export function createElement<E extends HTMLElement = HTMLElement>(htmlStr: string = ''): E {
@@ -77,6 +74,7 @@ export function timeAgo(date: Date, ctx: Context) {
77
74
  // 计算相差秒数
78
75
  const leave3 = leave2 % (60 * 1000) // 计算分钟数后剩余的毫秒数
79
76
  const seconds = Math.round(leave3 / 1000)
77
+ if (seconds < 10) return ctx.$t('now')
80
78
  return `${seconds} ${ctx.$t('seconds')}`
81
79
  }
82
80
  return `${minutes} ${ctx.$t('minutes')}`
@@ -140,126 +138,6 @@ export function versionCompare(a: string, b: string) {
140
138
  return 0
141
139
  }
142
140
 
143
- /** 初始化 marked */
144
- export function initMarked(ctx: Context) {
145
- try { if (!libMarked.name) return } catch { return }
146
-
147
- const renderer = new libMarked.Renderer()
148
- const orgLinkRenderer = renderer.link
149
- renderer.link = (href, title, text) => {
150
- const localLink = href?.startsWith(`${window.location.protocol}//${window.location.hostname}`);
151
- const html = orgLinkRenderer.call(renderer as any, href, title, text);
152
- return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
153
- }
154
-
155
- renderer.code = (block, lang) => {
156
- // Colorize the block only if the language is known to highlight.js
157
- const realLang = (!lang ? 'plaintext' : lang)
158
- let colorized = block
159
- if ((window as any).hljs) {
160
- if (realLang && (window as any).hljs.getLanguage(realLang)) {
161
- colorized = (window as any).hljs.highlight(realLang, block).value
162
- }
163
- } else {
164
- colorized = hanabi(block)
165
- }
166
-
167
- return `<pre rel="${realLang}">\n`
168
- + `<code class="hljs language-${realLang}">${colorized.replace(/&amp;/g, '&')}</code>\n`
169
- + `</pre>`
170
- }
171
-
172
- // @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
173
- const nMarked = libMarked
174
- libMarked.setOptions({
175
- renderer,
176
- pedantic: false,
177
- gfm: true,
178
- breaks: true,
179
- smartLists: true,
180
- smartypants: true,
181
- xhtml: false,
182
- sanitize: false,
183
- silent: true,
184
- })
185
-
186
- ctx.markedInstance = nMarked
187
- }
188
-
189
- /** 解析 markdown */
190
- export function marked(ctx: Context, src: string): string {
191
- let markedContent = ctx.markedInstance?.parse(src)
192
- if (!markedContent) {
193
- // 无 Markdown 模式简单处理
194
- markedContent = src
195
- // .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
196
- .replace(/```\s*([^]+?.*?[^]+?[^]+?)```/g, (_, code) => `<pre><code>${hanabi(code)}</code></pre>`)
197
- // .replace(/`([^`]+?)`/g, '<code>$1</code>')
198
- .replace(/!\[(.*?)\]\((.*?)\)/g, (_, alt, imgSrc) => `<img src="${imgSrc}" alt="${alt}" />`)
199
- .replace(/\[(.*?)\]\((.*?)\)/g, (_, text, link) => `<a href="${link}" target="_blank">${text}</a>`)
200
- .replace(/\n/g, '<br>')
201
- }
202
-
203
- // @link https://github.com/markedjs/marked/discussions/1232
204
- // @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225
205
- let dest = insane(markedContent, {
206
- allowedClasses: {},
207
- // @refer CVE-2018-8495
208
- // @link https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-8495
209
- // @link https://leucosite.com/Microsoft-Edge-RCE/
210
- // @link https://medium.com/@knownsec404team/analysis-of-the-security-issues-of-url-scheme-in-pc-from-cve-2018-8495-934478a36756
211
- allowedSchemes: [
212
- 'http', 'https', 'mailto',
213
- 'data' // for support base64 encoded image (安全性有待考虑)
214
- ],
215
- allowedTags: [
216
- 'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
217
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
218
- 'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
219
- 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'
220
- ],
221
- allowedAttributes: {
222
- '*': ['title', 'accesskey'],
223
- a: ['href', 'name', 'target', 'aria-label', 'rel'],
224
- img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label'],
225
- // for code highlight
226
- code: ['class'],
227
- span: ['class', 'style'],
228
- },
229
- filter: node => {
230
- // allow hljs style
231
- const allowed = [
232
- [ 'code', /^hljs\W+language-(.*)$/ ],
233
- [ 'span', /^(hljs-.*)$/ ]
234
- ]
235
- allowed.forEach(([ tag, reg ]) => {
236
- if (
237
- node.tag === tag
238
- && !!node.attrs.class
239
- && !(reg as RegExp).test(node.attrs.class)
240
- ) {
241
- delete node.attrs.class
242
- }
243
- })
244
-
245
- // allow <span> set color sty
246
- if (node.tag === 'span' && !!node.attrs.style
247
- && !/^color:(\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)) {
248
- delete node.attrs.style
249
- }
250
-
251
- return true
252
- }
253
- })
254
-
255
- // 内容替换器
256
- ctx.markedReplacers.forEach((replacer) => {
257
- if (typeof replacer === 'function') dest = replacer(dest)
258
- })
259
-
260
- return dest
261
- }
262
-
263
141
  /** 获取修正后的 UserAgent */
264
142
  export async function getCorrectUserAgent() {
265
143
  const uaRaw = navigator.userAgent
@@ -4,24 +4,32 @@ import Component from '../lib/component'
4
4
  import * as Utils from '../lib/utils'
5
5
  import * as Ui from '../lib/ui'
6
6
  import Comment from '../comment'
7
- import Pagination from '../components/pagination'
8
- import ReadMoreBtn from '../components/read-more-btn'
7
+ import PgHolder, { TPgMode } from './paginator'
9
8
  import * as ListNest from './list-nest'
10
- import { backendMinVersion, version as ARTALK_VERSION } from '../../package.json'
9
+ import * as ListUi from './list-ui'
10
+ import { version as ARTALK_VERSION } from '../../package.json'
11
+ import { handleBackendRefConf } from '../config'
11
12
 
12
13
  export default class ListLite extends Component {
14
+ /** 列表评论集区域元素 */
13
15
  protected $commentsWrap: HTMLElement
14
16
 
17
+ /** 最后一次请求得到的列表数据 */
15
18
  protected data?: ListData
19
+
20
+ /** 是否处于加载中状态 */
16
21
  protected isLoading: boolean = false
17
22
 
18
- public noCommentText?: string // 无评论时显示
23
+ /** 配置是否已加载 */
24
+ private confLoaded = false
25
+
26
+ /** 无评论显示文字 */
27
+ public noCommentText?: string
19
28
 
20
29
  /** 嵌套模式下的排序方式 */
21
30
  private _nestSortBy?: ListNest.SortByType
22
31
  public get nestSortBy() {
23
- if (this._nestSortBy !== undefined) return this._nestSortBy
24
- return this.ctx.conf.nestSort || 'DATE_ASC'
32
+ return this._nestSortBy || this.ctx.conf.nestSort || 'DATE_ASC'
25
33
  }
26
34
  public set nestSortBy(val: ListNest.SortByType) {
27
35
  this._nestSortBy = val
@@ -48,14 +56,18 @@ export default class ListLite extends Component {
48
56
  }
49
57
 
50
58
  /** 分页方式 */
51
- public _pageMode?: 'pagination'|'read-more'
59
+ public _pageMode?: TPgMode
52
60
  public get pageMode() {
53
61
  return this._pageMode || (this.conf.pagination.readMore ? 'read-more' : 'pagination')
54
62
  }
55
63
  public set pageMode(val: 'pagination'|'read-more') {
56
64
  this._pageMode = val
65
+ this.pgHolder?.setMode(this._pageMode)
57
66
  }
58
67
 
68
+ /** 分页方式持有者 */
69
+ public pgHolder?: PgHolder
70
+
59
71
  /** 每页数量 (每次请求获取量) */
60
72
  private _pageSize?: number
61
73
  public get pageSize() {
@@ -65,18 +77,17 @@ export default class ListLite extends Component {
65
77
  this._pageSize = val
66
78
  }
67
79
 
68
- public scrollListenerAt?: HTMLElement // 监听指定元素上的滚动
69
- public repositionAt?: HTMLElement // 翻页归位到指定元素
70
-
71
- protected pagination?: Pagination // 分页条
72
- protected readMoreBtn?: ReadMoreBtn // 阅读更多
73
- protected autoLoadScrollEvent?: any // 自动加载滚动事件
80
+ /** 监听指定元素上的滚动 */
81
+ public scrollListenerAt?: HTMLElement
82
+ /** 翻页归位到指定元素 */
83
+ public repositionAt?: HTMLElement
74
84
 
85
+ // 一些 Hook 函数
75
86
  public renderComment?: (comment: Comment) => void
76
87
  public paramsEditor?: (params: any) => void
77
88
  public onAfterLoad?: (data: ListData) => void
78
89
 
79
- // 未读标记
90
+ /** 未读标记 */
80
91
  protected unread: NotifyData[] = []
81
92
  public unreadHighlight?: boolean // 高亮未读
82
93
 
@@ -111,16 +122,23 @@ export default class ListLite extends Component {
111
122
  this.data = undefined
112
123
  }
113
124
 
125
+ public getLoading() {
126
+ return this.isLoading
127
+ }
128
+
114
129
  public getCommentsWrapEl() {
115
130
  return this.$commentsWrap
116
131
  }
117
132
 
118
- /** 加载动画 */
133
+ /** 设置加载状态 */
119
134
  public setLoading(val: boolean, isFirstLoad: boolean = false) {
120
135
  this.isLoading = val
121
- if (isFirstLoad) Ui.setLoading(val, this.$el)
122
- else if (this.pageMode === 'read-more') this.readMoreBtn!.setLoading(val)
123
- else if (this.pageMode === 'pagination') this.pagination!.setLoading(val)
136
+ if (isFirstLoad) {
137
+ Ui.setLoading(val, this.$el)
138
+ return
139
+ }
140
+
141
+ this.pgHolder?.setLoading(val)
124
142
  }
125
143
 
126
144
  /** 评论获取 */
@@ -144,6 +162,7 @@ export default class ListLite extends Component {
144
162
  // 请求评论数据
145
163
  let listData: ListData
146
164
  try {
165
+ // 执行请求
147
166
  listData = await this.ctx.getApi().comment
148
167
  .get(offset, this.pageSize, this.flatMode, this.paramsEditor)
149
168
  } catch (e: any) {
@@ -167,112 +186,54 @@ export default class ListLite extends Component {
167
186
  }
168
187
  }
169
188
 
170
- private confLoaded = false
171
-
172
189
  protected onLoad(data: ListData, offset: number) {
173
190
  this.data = data
174
191
 
175
192
  // 装载后端提供的配置
176
193
  if (!this.confLoaded) {
177
- if (this.conf.useBackendConf)
178
- this.ctx.updateConf(data.conf.frontend_conf)
179
- else
180
- this.ctx.updateConf({})
194
+ // 仅应用一次配置
195
+ const backendRefConf = handleBackendRefConf(data.conf.frontend_conf)
196
+ if (this.conf.useBackendConf) this.ctx.updateConf(backendRefConf)
197
+ else this.ctx.updateConf({}) // 让事件监听 `on('conf-loaded')` 有效,与前者保持相同生命周期环节
181
198
  this.confLoaded = true
182
199
  }
183
200
 
184
- // 版本检测
185
- const feMinVersion = data.api_version?.fe_min_version || '0.0.0'
186
- if (this.ctx.conf.versionCheck && this.versionCheck('frontend', feMinVersion, ARTALK_VERSION)) return
187
- if (this.ctx.conf.versionCheck && this.versionCheck('backend', backendMinVersion, data.api_version?.version)) return
201
+ // 前后端版本一致性检测
202
+ if (this.ctx.conf.versionCheck && ListUi.versionCheckDialog(this, ARTALK_VERSION, data.api_version.version)) return
188
203
 
189
204
  // 导入数据
190
205
  this.importComments(data.comments)
191
206
 
192
- // 分页方式
193
- this.refreshPagination(offset, (this.flatMode ? data.total : data.total_roots)) // 初始化
207
+ // 分页功能
208
+ this.refreshPagination(offset, (this.flatMode ? data.total : data.total_roots))
194
209
 
195
210
  // 加载后事件
196
211
  this.refreshUI()
197
212
 
213
+ // 未读消息提示功能
198
214
  this.ctx.updateNotifies(data.unread || [])
215
+
216
+ // 事件触发,列表已加载
199
217
  this.ctx.trigger('list-loaded')
200
218
 
219
+ // Hook 函数调用
201
220
  if (this.onAfterLoad) this.onAfterLoad(data)
202
221
  }
203
222
 
204
223
  /** 分页模式 */
205
224
  private refreshPagination(offset: number, total: number) {
206
- const modePagination = (this.pageMode === 'pagination')
207
- const modeReadMoreBtn = (this.pageMode === 'read-more')
208
- const initialized = (modePagination) ? !!this.pagination : !!this.readMoreBtn
209
-
210
225
  // 初始化
211
- if (!initialized) {
212
- this.initPagination()
213
- }
214
-
215
- // 更新
216
- if (modePagination) this.pagination!.update(offset, total)
217
- if (modeReadMoreBtn) this.readMoreBtn!.update(offset, total)
218
- }
219
-
220
- private initPagination() {
221
- // 加载更多按钮
222
- if (this.pageMode === 'read-more') {
223
- this.readMoreBtn = new ReadMoreBtn({
226
+ if (!this.pgHolder) {
227
+ this.pgHolder = new PgHolder({
228
+ list: this,
229
+ mode: this.pageMode,
224
230
  pageSize: this.pageSize,
225
- onClick: async (o) => {
226
- await this.fetchComments(o)
227
- },
228
- text: this.ctx.$t('loadMore'),
231
+ total
229
232
  })
230
- this.$el.append(this.readMoreBtn.$el)
231
-
232
- // 滚动到底部自动加载
233
- if (this.conf.pagination.autoLoad) {
234
- // 添加滚动事件监听
235
- const at = this.scrollListenerAt || document
236
- if (this.autoLoadScrollEvent) at.removeEventListener('scroll', this.autoLoadScrollEvent) // 解除原有
237
- this.autoLoadScrollEvent = () => {
238
- if (this.pageMode !== 'read-more'
239
- || !this.readMoreBtn?.hasMore
240
- || this.isLoading
241
- ) return
242
-
243
- const $target = this.$el.querySelector<HTMLElement>('.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)') // 获取倒数第3个评论元素
244
- if ($target && Ui.isVisible($target, this.scrollListenerAt)) {
245
- this.readMoreBtn.click() // 自动点击加载更多按钮
246
- }
247
- }
248
- at.addEventListener('scroll', this.autoLoadScrollEvent)
249
- }
250
233
  }
251
234
 
252
- // 分页条
253
- if (this.pageMode === 'pagination') {
254
- this.pagination = new Pagination((!this.flatMode ? this.data!.total_roots : this.data!.total), {
255
- pageSize: this.pageSize,
256
- onChange: async (o) => {
257
- if (this.ctx.conf.editorTravel === true) {
258
- this.ctx.editorTravelBack() // 防止评论框被吞
259
- }
260
-
261
- await this.fetchComments(o)
262
-
263
- // 滚动到第一个评论的位置
264
- if (this.repositionAt) {
265
- const at = this.scrollListenerAt || window
266
- at.scroll({
267
- top: this.repositionAt ? Utils.getOffset(this.repositionAt).top : 0,
268
- left: 0,
269
- })
270
- }
271
- }
272
- })
273
-
274
- this.$el.append(this.pagination.$el)
275
- }
235
+ // 更新
236
+ this.pgHolder?.update(offset, total)
276
237
  }
277
238
 
278
239
  /** 错误处理 */
@@ -286,58 +247,17 @@ export default class ListLite extends Component {
286
247
 
287
248
  // 加载更多按钮显示错误
288
249
  if (offset !== 0 && this.pageMode === 'read-more') {
289
- this.readMoreBtn?.showErr(this.$t('loadFail'))
250
+ this.pgHolder?.showErr(this.$t('loadFail'))
290
251
  return
291
252
  }
292
253
 
293
254
  // 显示错误对话框
294
- const $err = Utils.createElement(`<span>${msg},${this.$t('listLoadFailMsg')}<br/></span>`)
295
-
296
- const $retryBtn = Utils.createElement(`<span style="cursor:pointer;">${this.$t('listRetry')}</span>`)
297
- $retryBtn.onclick = () => (this.fetchComments(0))
298
- $err.appendChild($retryBtn)
299
-
300
- const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
301
- $err.appendChild(adminBtn)
302
- if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
303
-
304
- let sidebarView = ''
305
-
306
- // 找不到站点错误,打开侧边栏并填入创建站点表单
307
- if (errData?.err_no_site) {
308
- const viewLoadParam = {
309
- create_name: this.ctx.conf.site,
310
- create_urls: `${window.location.protocol}//${window.location.host}`
311
- }
312
- // TODO 真的是飞鸽传书啊
313
- sidebarView = `sites|${JSON.stringify(viewLoadParam)}`
314
- }
315
-
316
- adminBtn.onclick = () => this.ctx.showSidebar({
317
- view: sidebarView as any
318
- })
319
-
320
- Ui.setError(this.$el, $err)
255
+ Ui.setError(this.$el, ListUi.renderErrorDialog(this, msg, errData))
321
256
  }
322
257
 
323
258
  /** 刷新界面 */
324
259
  public refreshUI() {
325
- // 无评论
326
- const isNoComment = this.ctx.getCommentList().length <= 0
327
- let $noComment = this.$commentsWrap.querySelector<HTMLElement>('.atk-list-no-comment')
328
-
329
- if (isNoComment) {
330
- if (!$noComment) {
331
- $noComment = Utils.createElement('<div class="atk-list-no-comment"></div>')
332
- $noComment.innerHTML = this.noCommentText || this.ctx.conf.noComment || this.ctx.$t('noComment')
333
- this.$commentsWrap.appendChild($noComment)
334
- }
335
- } else {
336
- $noComment?.remove()
337
- }
338
-
339
- // 仅管理员显示控制
340
- this.ctx.checkAdminShowEl()
260
+ ListUi.refreshUI(this)
341
261
  }
342
262
 
343
263
  /** 重新加载列表 */
@@ -501,27 +421,4 @@ export default class ListLite extends Component {
501
421
  })
502
422
  }
503
423
  }
504
-
505
- /** 版本检测 */
506
- public versionCheck(name: 'frontend'|'backend', needVersion: string, curtVersion: string): boolean {
507
- const needUpdate = Utils.versionCompare(needVersion, curtVersion) === 1
508
- if (needUpdate) {
509
- // 需要更新
510
- const errEl = Utils.createElement(`<div>Artalk ${this.$t(name)}版本已过时,请更新以获得完整体验<br/>`
511
- + `如果你是管理员,请前往 “<a href="https://artalk.js.org/" target="_blank">官方文档</a>” 获得帮助`
512
- + `<br/><br/>`
513
- + `<span style="color: var(--at-color-meta);">当前${this.$t(name)}版本 ${curtVersion},需求版本 >= ${needVersion}</span><br/><br/>`
514
- + `</div>`)
515
- const ignoreBtn = Utils.createElement('<span style="cursor:pointer;">忽略</span>')
516
- ignoreBtn.onclick = () => {
517
- Ui.setError(this.$el.parentElement!, null)
518
- this.ctx.conf.versionCheck = false
519
- this.fetchComments(0)
520
- }
521
- errEl.append(ignoreBtn)
522
- Ui.setError(this.$el.parentElement!, errEl, '<span class="atk-warn-title">Artalk Warn</span>')
523
- }
524
-
525
- return needUpdate
526
- }
527
424
  }