artalk 2.1.6 → 2.1.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.
- package/LICENSE +674 -0
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +261 -41
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/index.html +4 -4
- package/package.json +12 -12
- package/src/api/index.ts +23 -0
- package/src/artalk.ts +18 -0
- package/src/components/comment.ts +20 -3
- package/src/components/editor.ts +120 -5
- package/src/components/html/list.html +4 -2
- package/src/components/list-lite.ts +7 -0
- package/src/components/list.ts +63 -0
- package/src/components/sidebar-layer.ts +3 -2
- package/src/context.ts +3 -0
- package/src/defaults.ts +2 -0
- package/src/lib/utils.ts +56 -35
- package/src/style/comment.less +9 -0
- package/src/style/list.less +70 -1
- package/tsconfig.json +2 -1
- package/types/artalk-config.d.ts +6 -0
- package/types/artalk-data.d.ts +6 -0
- package/types/event.d.ts +1 -0
- package/types/vite-env.d.ts +0 -1
package/index.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
</style>
|
|
32
32
|
</head>
|
|
33
33
|
<body>
|
|
34
|
-
<div class="card billboard
|
|
34
|
+
<div class="card billboard atk-fade-in">
|
|
35
35
|
<div class="title"><a href="https://github.com/ArtalkJS/Artalk" target="_blank"><span>『 </span>Artalk<span> 』</span></a></div>
|
|
36
36
|
<div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span></div>
|
|
37
37
|
<div class="social">
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
<a href="https://github.com/ArtalkJS/Artalk" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
|
-
<div class="card
|
|
45
|
+
<div class="card atk-fade-in">
|
|
46
46
|
<div id="comments"></div>
|
|
47
47
|
</div>
|
|
48
|
-
<div class="float-btn to-night
|
|
49
|
-
<div class="float-btn to-top
|
|
48
|
+
<div class="float-btn to-night atk-fade-in">Blink</div>
|
|
49
|
+
<div class="float-btn to-top atk-fade-in" style="display: none;">TOP</div>
|
|
50
50
|
|
|
51
51
|
<script type="module">
|
|
52
52
|
import Artalk from './src/main'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artalk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.9",
|
|
4
4
|
"license": "GPL-3.0",
|
|
5
5
|
"description": "A Selfhosted Comment System",
|
|
6
6
|
"keywords": [
|
|
@@ -22,16 +22,6 @@
|
|
|
22
22
|
"jsdelivr": "./dist/Artalk.iife.js",
|
|
23
23
|
"module": "./dist/Artalk.es.js",
|
|
24
24
|
"types": "./types/index.d.ts",
|
|
25
|
-
"scripts": {
|
|
26
|
-
"dev": "vite",
|
|
27
|
-
"build": "yarn lint && vite build",
|
|
28
|
-
"build:demo": "vite build --config vite-demo.config.ts",
|
|
29
|
-
"serve": "vite preview",
|
|
30
|
-
"deploy": "gh-pages -d deploy",
|
|
31
|
-
"predeploy": "yarn run build && yarn run build:demo && yarn run copy:deploy",
|
|
32
|
-
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
|
|
33
|
-
"lint": "eslint --ext .js,.ts src"
|
|
34
|
-
},
|
|
35
25
|
"devDependencies": {
|
|
36
26
|
"@types/marked": "^4.0.3"
|
|
37
27
|
},
|
|
@@ -39,5 +29,15 @@
|
|
|
39
29
|
"hanabi": "^0.4.0",
|
|
40
30
|
"insane": "^2.6.2",
|
|
41
31
|
"marked": "^4.0.14"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"dev": "vite",
|
|
35
|
+
"build": "pnpm lint && vite build",
|
|
36
|
+
"build:demo": "vite build --config vite-demo.config.ts",
|
|
37
|
+
"serve": "vite preview",
|
|
38
|
+
"deploy": "gh-pages -d deploy",
|
|
39
|
+
"predeploy": "pnpm build && pnpm build:demo && pnpm copy:deploy",
|
|
40
|
+
"copy:deploy": "cp -r dist deploy && cp CNAME deploy",
|
|
41
|
+
"lint": "eslint --ext .js,.ts src"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|
package/src/api/index.ts
CHANGED
|
@@ -264,6 +264,29 @@ export default class Api {
|
|
|
264
264
|
return p.pv as number
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/** 图片上传 */
|
|
268
|
+
public async imgUpload(file: File) {
|
|
269
|
+
const params: any = {
|
|
270
|
+
name: this.ctx.user.data.nick,
|
|
271
|
+
email: this.ctx.user.data.email,
|
|
272
|
+
page_key: this.ctx.conf.pageKey,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.ctx.conf.site) params.site_name = this.ctx.conf.site
|
|
276
|
+
|
|
277
|
+
const form = ToFormData(params)
|
|
278
|
+
form.set('file', file)
|
|
279
|
+
|
|
280
|
+
const init: RequestInit = {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: form
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const json = await Fetch(this.ctx, `${this.baseURL}/img-upload`, init)
|
|
286
|
+
return ((json.data || {}) as any) as { img_file: string, img_url: string }
|
|
287
|
+
|
|
288
|
+
}
|
|
289
|
+
|
|
267
290
|
// ============================
|
|
268
291
|
// 验证码
|
|
269
292
|
// ============================
|
package/src/artalk.ts
CHANGED
|
@@ -12,6 +12,7 @@ import SidebarLayer from './components/sidebar-layer'
|
|
|
12
12
|
import { GetLayerWrap } from './components/layer'
|
|
13
13
|
import { EventPayloadMap, Handler } from '~/types/event'
|
|
14
14
|
import Api from './api'
|
|
15
|
+
import * as Utils from './lib/utils'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Artalk
|
|
@@ -59,6 +60,7 @@ export default class Artalk {
|
|
|
59
60
|
|
|
60
61
|
// 组件初始化
|
|
61
62
|
this.checkerLauncher = new CheckerLauncher(this.ctx)
|
|
63
|
+
Utils.initMarked(this.ctx) // 初始化 marked
|
|
62
64
|
|
|
63
65
|
// 编辑器
|
|
64
66
|
this.editor = new Editor(this.ctx)
|
|
@@ -80,6 +82,13 @@ export default class Artalk {
|
|
|
80
82
|
|
|
81
83
|
// 其他
|
|
82
84
|
this.initPV()
|
|
85
|
+
|
|
86
|
+
// 插件初始化
|
|
87
|
+
Artalk.Plugins.forEach(plugin => {
|
|
88
|
+
if (typeof plugin === 'function') {
|
|
89
|
+
plugin(this.ctx)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
/** 事件绑定 · 初始化 */
|
|
@@ -148,6 +157,7 @@ export default class Artalk {
|
|
|
148
157
|
/** 暗黑模式 · 设定 */
|
|
149
158
|
public setDarkMode(darkMode: boolean) {
|
|
150
159
|
this.ctx.conf.darkMode = darkMode
|
|
160
|
+
this.ctx.trigger('conf-updated')
|
|
151
161
|
this.initDarkMode()
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -179,4 +189,12 @@ export default class Artalk {
|
|
|
179
189
|
public trigger<K extends keyof EventPayloadMap>(name: K, payload?: EventPayloadMap[K]) {
|
|
180
190
|
this.ctx.trigger(name, payload, 'external')
|
|
181
191
|
}
|
|
192
|
+
|
|
193
|
+
/** Plugins */
|
|
194
|
+
protected static Plugins: ((ctx: Context) => void)[] = []
|
|
195
|
+
|
|
196
|
+
/** Enable Plugin */
|
|
197
|
+
public static Use(plugin: ((ctx: Context) => void)) {
|
|
198
|
+
this.Plugins.push(plugin)
|
|
199
|
+
}
|
|
182
200
|
}
|
|
@@ -136,6 +136,11 @@ export default class Comment extends Component {
|
|
|
136
136
|
$badge.remove()
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
if (this.data.is_pinned) {
|
|
140
|
+
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
|
|
141
|
+
$nick.insertAdjacentElement('afterend', $pinnedBadge)
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
140
145
|
$date.innerText = this.getDateFormatted()
|
|
141
146
|
$date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
|
|
@@ -272,6 +277,16 @@ export default class Comment extends Component {
|
|
|
272
277
|
delBtn.setClick(() => {
|
|
273
278
|
this.adminDelete(delBtn)
|
|
274
279
|
})
|
|
280
|
+
|
|
281
|
+
// 绑定置顶按钮事件
|
|
282
|
+
const pinnedBtn = new ActionBtn({
|
|
283
|
+
text: () => (this.data.is_pinned ? '取消置顶' : '置顶'),
|
|
284
|
+
adminOnly: true
|
|
285
|
+
})
|
|
286
|
+
pinnedBtn.appendTo(this.$actions)
|
|
287
|
+
pinnedBtn.setClick(() => {
|
|
288
|
+
this.adminEdit('pinned', pendingBtn)
|
|
289
|
+
})
|
|
275
290
|
}
|
|
276
291
|
//#endregion
|
|
277
292
|
|
|
@@ -404,9 +419,9 @@ export default class Comment extends Component {
|
|
|
404
419
|
})
|
|
405
420
|
}
|
|
406
421
|
|
|
407
|
-
/** 管理员 -
|
|
408
|
-
adminEdit(type: 'collapsed'|'pending', btnElem: ActionBtn) {
|
|
409
|
-
if (btnElem.isLoading) return //
|
|
422
|
+
/** 管理员 - 评论状态修改 */
|
|
423
|
+
adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
|
|
424
|
+
if (btnElem.isLoading) return // 若正在修改中
|
|
410
425
|
|
|
411
426
|
btnElem.setLoading(true, '修改中...')
|
|
412
427
|
|
|
@@ -416,6 +431,8 @@ export default class Comment extends Component {
|
|
|
416
431
|
modify.is_collapsed = !modify.is_collapsed
|
|
417
432
|
} else if (type === 'pending') {
|
|
418
433
|
modify.is_pending = !modify.is_pending
|
|
434
|
+
} else if (type === 'pinned') {
|
|
435
|
+
modify.is_pinned = !modify.is_pinned
|
|
419
436
|
}
|
|
420
437
|
|
|
421
438
|
new Api(this.ctx).commentEdit(modify).then((comment) => {
|
package/src/components/editor.ts
CHANGED
|
@@ -21,6 +21,7 @@ export default class Editor extends Component {
|
|
|
21
21
|
public $plugWrap: HTMLElement
|
|
22
22
|
public $bottom: HTMLElement
|
|
23
23
|
public $plugBtnWrap: HTMLElement
|
|
24
|
+
public $imgUploadBtn?: HTMLElement
|
|
24
25
|
public $submitBtn: HTMLButtonElement
|
|
25
26
|
public $notifyWrap: HTMLElement
|
|
26
27
|
|
|
@@ -63,6 +64,7 @@ export default class Editor extends Component {
|
|
|
63
64
|
this.ctx.on('editor-notify', (f) => (this.showNotify(f.msg, f.type)))
|
|
64
65
|
this.ctx.on('editor-travel', ($el) => (this.travel($el)))
|
|
65
66
|
this.ctx.on('editor-travel-back', () => (this.travelBack()))
|
|
67
|
+
this.ctx.on('conf-updated', () => (this.refreshUploadBtn()))
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
initLocalStorage () {
|
|
@@ -200,7 +202,7 @@ export default class Editor extends Component {
|
|
|
200
202
|
// 依次实例化 plug
|
|
201
203
|
this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
|
|
202
204
|
// 切换按钮
|
|
203
|
-
const btnElem = Utils.createElement(`<span class="atk-plug-btn">${PlugObj.BtnHTML}</span>`)
|
|
205
|
+
const btnElem = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${PlugObj.Name}">${PlugObj.BtnHTML}</span>`)
|
|
204
206
|
this.$plugBtnWrap.appendChild(btnElem)
|
|
205
207
|
btnElem.addEventListener('click', () => {
|
|
206
208
|
let plug = this.plugList[PlugObj.Name]
|
|
@@ -244,6 +246,8 @@ export default class Editor extends Component {
|
|
|
244
246
|
btnElem.classList.add('active')
|
|
245
247
|
})
|
|
246
248
|
})
|
|
249
|
+
|
|
250
|
+
this.initImgUpload()
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
/** 关闭编辑器插件 */
|
|
@@ -253,6 +257,116 @@ export default class Editor extends Component {
|
|
|
253
257
|
this.openedPlugName = null
|
|
254
258
|
}
|
|
255
259
|
|
|
260
|
+
/** 允许的图片格式 */
|
|
261
|
+
allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
|
|
262
|
+
|
|
263
|
+
/** 初始化图片上传功能 */
|
|
264
|
+
initImgUpload() {
|
|
265
|
+
this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
|
|
266
|
+
this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
|
|
267
|
+
|
|
268
|
+
// 按钮点击
|
|
269
|
+
this.$imgUploadBtn.onclick = () => {
|
|
270
|
+
// 选择图片
|
|
271
|
+
const $input = document.createElement('input')
|
|
272
|
+
$input.type = 'file'
|
|
273
|
+
$input.accept = this.allowImgExts.map(o => `.${o}`).join(',')
|
|
274
|
+
$input.onchange = () => {
|
|
275
|
+
(async () => { // 解决阻塞 UI 问题
|
|
276
|
+
if (!$input.files || $input.files.length === 0) return
|
|
277
|
+
const file = $input.files[0]
|
|
278
|
+
this.uploadImg(file)
|
|
279
|
+
})()
|
|
280
|
+
}
|
|
281
|
+
$input.click() // 显示选择图片对话框
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 统一从 FileList 获取文件并上传图片方法
|
|
285
|
+
const uploadFromFileList = (files?: FileList) => {
|
|
286
|
+
if (!files) return
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < files.length; i++) {
|
|
289
|
+
const file = files[i]
|
|
290
|
+
this.uploadImg(file)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 拖拽图片
|
|
295
|
+
// @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
296
|
+
// 阻止浏览器的默认释放行为
|
|
297
|
+
this.$textarea.addEventListener('dragover', (evt) => {
|
|
298
|
+
evt.stopPropagation()
|
|
299
|
+
evt.preventDefault()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
this.$textarea.addEventListener('drop', (evt) => {
|
|
303
|
+
const files = evt.dataTransfer?.files
|
|
304
|
+
if (files?.length) {
|
|
305
|
+
evt.preventDefault()
|
|
306
|
+
uploadFromFileList(files)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// 粘贴图片
|
|
311
|
+
this.$textarea.addEventListener('paste', (evt) => {
|
|
312
|
+
const files = evt.clipboardData?.files
|
|
313
|
+
if (files?.length) {
|
|
314
|
+
evt.preventDefault()
|
|
315
|
+
uploadFromFileList(files)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 刷新图片上传按钮 */
|
|
321
|
+
refreshUploadBtn() {
|
|
322
|
+
if (!this.$imgUploadBtn) return
|
|
323
|
+
|
|
324
|
+
if (!this.ctx.conf.imgUpload) {
|
|
325
|
+
this.$imgUploadBtn.setAttribute('atk-only-admin-show', '')
|
|
326
|
+
this.ctx.trigger('check-admin-show-el')
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async uploadImg(file: File) {
|
|
331
|
+
const fileExt = /[^.]+$/.exec(file.name)
|
|
332
|
+
if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
|
|
333
|
+
|
|
334
|
+
// 未登录提示
|
|
335
|
+
if (!this.ctx.user.checkHasBasicUserInfo()) {
|
|
336
|
+
this.showNotify('填入你的名字邮箱才能上传哦', 'w')
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 插入图片前换一行
|
|
341
|
+
let insertPrefix = '\n'
|
|
342
|
+
if (this.$textarea.value.trim() === '') insertPrefix = ''
|
|
343
|
+
|
|
344
|
+
// 插入占位加载文字
|
|
345
|
+
const uploadPlaceholderTxt = `${insertPrefix}`
|
|
346
|
+
this.insertContent(uploadPlaceholderTxt)
|
|
347
|
+
|
|
348
|
+
// 上传图片
|
|
349
|
+
let resp: any
|
|
350
|
+
try {
|
|
351
|
+
resp = await new Api(this.ctx).imgUpload(file)
|
|
352
|
+
} catch (err: any) {
|
|
353
|
+
console.error(err)
|
|
354
|
+
this.showNotify(`图片上传失败,${err.msg}`, 'e')
|
|
355
|
+
}
|
|
356
|
+
if (!!resp && resp.img_url) {
|
|
357
|
+
let imgURL = resp.img_url as string
|
|
358
|
+
|
|
359
|
+
// 若为相对路径,加上 artalk server
|
|
360
|
+
if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
|
|
361
|
+
|
|
362
|
+
// 上传成功插入图片
|
|
363
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}`))
|
|
364
|
+
} else {
|
|
365
|
+
// 上传失败删除加载文字
|
|
366
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, ''))
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
256
370
|
insertContent (val: string) {
|
|
257
371
|
if ((document as any).selection) {
|
|
258
372
|
this.$textarea.focus();
|
|
@@ -287,7 +401,8 @@ export default class Editor extends Component {
|
|
|
287
401
|
this.cancelReply()
|
|
288
402
|
}
|
|
289
403
|
|
|
290
|
-
|
|
404
|
+
/** 获取最终用于 submit 的数据 */
|
|
405
|
+
getFinalContent () {
|
|
291
406
|
let content = this.getContentOriginal()
|
|
292
407
|
|
|
293
408
|
// 表情包处理
|
|
@@ -304,7 +419,7 @@ export default class Editor extends Component {
|
|
|
304
419
|
}
|
|
305
420
|
|
|
306
421
|
getContentMarked () {
|
|
307
|
-
return Utils.marked(this.ctx, this.
|
|
422
|
+
return Utils.marked(this.ctx, this.getFinalContent())
|
|
308
423
|
}
|
|
309
424
|
|
|
310
425
|
initBottomPart () {
|
|
@@ -363,7 +478,7 @@ export default class Editor extends Component {
|
|
|
363
478
|
}
|
|
364
479
|
|
|
365
480
|
async submit () {
|
|
366
|
-
if (this.
|
|
481
|
+
if (this.getFinalContent().trim() === '') {
|
|
367
482
|
this.$textarea.focus()
|
|
368
483
|
return
|
|
369
484
|
}
|
|
@@ -374,7 +489,7 @@ export default class Editor extends Component {
|
|
|
374
489
|
|
|
375
490
|
try {
|
|
376
491
|
const nComment = await new Api(this.ctx).add({
|
|
377
|
-
content: this.
|
|
492
|
+
content: this.getFinalContent(),
|
|
378
493
|
nick: this.user.data.nick,
|
|
379
494
|
email: this.user.data.email,
|
|
380
495
|
link: this.user.data.link,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<div class="atk-list">
|
|
2
2
|
<div class="atk-list-header">
|
|
3
3
|
<div class="atk-comment-count">
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<div class="atk-text">
|
|
5
|
+
<span class="atk-comment-count-num">0</span>
|
|
6
|
+
条评论
|
|
7
|
+
</div>
|
|
6
8
|
</div>
|
|
7
9
|
<div class="atk-right-action">
|
|
8
10
|
<span data-action="admin-close-comment" class="atk-hide" atk-only-admin-show>关闭评论</span>
|
|
@@ -119,6 +119,11 @@ export default class ListLite extends Component {
|
|
|
119
119
|
// 版本检测
|
|
120
120
|
if (this.ctx.conf.versionCheck && this.versionCheck(data.api_version)) return
|
|
121
121
|
|
|
122
|
+
// 图片上传功能
|
|
123
|
+
if (data.conf && typeof data.conf.img_upload === "boolean") {
|
|
124
|
+
this.ctx.conf.imgUpload = data.conf.img_upload
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
// 导入数据
|
|
123
128
|
this.importComments(data.comments)
|
|
124
129
|
|
|
@@ -130,6 +135,7 @@ export default class ListLite extends Component {
|
|
|
130
135
|
|
|
131
136
|
this.ctx.trigger('unread-update', { notifies: data.unread || [] })
|
|
132
137
|
this.ctx.trigger('comments-loaded')
|
|
138
|
+
this.ctx.trigger('conf-updated')
|
|
133
139
|
|
|
134
140
|
if (this.onAfterLoad) this.onAfterLoad(data)
|
|
135
141
|
}
|
|
@@ -481,6 +487,7 @@ export default class ListLite extends Component {
|
|
|
481
487
|
ignoreBtn.onclick = () => {
|
|
482
488
|
Ui.setError(this.ctx, null)
|
|
483
489
|
this.ctx.conf.versionCheck = false
|
|
490
|
+
this.ctx.trigger('conf-updated')
|
|
484
491
|
this.fetchComments(0)
|
|
485
492
|
}
|
|
486
493
|
errEl.append(ignoreBtn)
|
package/src/components/list.ts
CHANGED
|
@@ -12,6 +12,8 @@ export default class List extends ListLite {
|
|
|
12
12
|
private $closeCommentBtn!: HTMLElement
|
|
13
13
|
private $openSidebarBtn!: HTMLElement
|
|
14
14
|
private $unreadBadge!: HTMLElement
|
|
15
|
+
private $commentCount!: HTMLElement
|
|
16
|
+
private $dropdownWrap?: HTMLElement
|
|
15
17
|
|
|
16
18
|
constructor (ctx: Context) {
|
|
17
19
|
const el = Utils.createElement(ListHTML)
|
|
@@ -40,6 +42,13 @@ export default class List extends ListLite {
|
|
|
40
42
|
// 操作按钮
|
|
41
43
|
this.initListActionBtn()
|
|
42
44
|
|
|
45
|
+
this.$commentCount = this.$el.querySelector('.atk-comment-count')!
|
|
46
|
+
|
|
47
|
+
// 评论列表排序 Dropdown 下拉选择层
|
|
48
|
+
if (this.ctx.conf.listSort) {
|
|
49
|
+
this.initDropdown()
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
// copyright
|
|
44
53
|
this.$el.querySelector<HTMLElement>('.atk-copyright')!.innerHTML = `Powered By <a href="https://artalk.js.org" target="_blank" title="Artalk v${ARTALK_VERSION}">Artalk</a>`
|
|
45
54
|
|
|
@@ -171,4 +180,58 @@ export default class List extends ListLite {
|
|
|
171
180
|
this.$unreadBadge.style.display = 'none'
|
|
172
181
|
}
|
|
173
182
|
}
|
|
183
|
+
|
|
184
|
+
/** 初始化选择下拉层 */
|
|
185
|
+
protected initDropdown() {
|
|
186
|
+
this.$dropdownWrap = this.$commentCount
|
|
187
|
+
this.$commentCount.classList.add('atk-dropdown-wrap')
|
|
188
|
+
|
|
189
|
+
// 插入图标
|
|
190
|
+
this.$dropdownWrap.append(Utils.createElement(`<span class="atk-arrow-down-icon"></span>`))
|
|
191
|
+
|
|
192
|
+
const reloadUseParamsEditor = (func: (p: any) => void) => {
|
|
193
|
+
this.paramsEditor = (p) => { func(p) }
|
|
194
|
+
this.fetchComments(0)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 下拉列表
|
|
198
|
+
const dropdownList = [
|
|
199
|
+
['最新', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_desc' }) }],
|
|
200
|
+
['最热', () => { reloadUseParamsEditor(p => { p.sort_by = 'vote' }) }],
|
|
201
|
+
['最早', () => { reloadUseParamsEditor(p => { p.sort_by = 'date_asc' }) }],
|
|
202
|
+
['作者', () => { reloadUseParamsEditor(p => { p.view_only_admin = true }) }],
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
// 列表项点击事件
|
|
206
|
+
let curtActive = 0 // 当前选中
|
|
207
|
+
const onItemClick = (i: number, $item: HTMLElement, name: string, action: Function) => {
|
|
208
|
+
action()
|
|
209
|
+
|
|
210
|
+
// set active
|
|
211
|
+
curtActive = i
|
|
212
|
+
$dropdown.querySelectorAll('.active').forEach((e) => { e.classList.remove('active') })
|
|
213
|
+
$item.classList.add('active')
|
|
214
|
+
|
|
215
|
+
// 关闭层 (临时消失,取消 :hover)
|
|
216
|
+
$dropdown.style.display = 'none'
|
|
217
|
+
setTimeout(() => { $dropdown.style.display = '' }, 80)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 生成列表元素
|
|
221
|
+
const $dropdown = Utils.createElement(`<ul class="atk-dropdown atk-fade-in"></ul>`)
|
|
222
|
+
dropdownList.forEach((item, i) => {
|
|
223
|
+
const name = item[0] as string
|
|
224
|
+
const action = item[1] as Function
|
|
225
|
+
|
|
226
|
+
const $item = Utils.createElement(`<li class="atk-dropdown-item"><span></span></li>`)
|
|
227
|
+
const $link = $item.querySelector<HTMLElement>('span')!
|
|
228
|
+
$link.innerText = name
|
|
229
|
+
$link.onclick = () => { onItemClick(i, $item, name, action) }
|
|
230
|
+
$dropdown.append($item)
|
|
231
|
+
|
|
232
|
+
if (i === curtActive) $item.classList.add('active') // 默认选中项
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
this.$dropdownWrap.append($dropdown)
|
|
236
|
+
}
|
|
174
237
|
}
|
|
@@ -64,14 +64,15 @@ export default class SidebarLayer extends Component {
|
|
|
64
64
|
this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
|
|
65
65
|
|
|
66
66
|
const baseURL = (import.meta.env.MODE === 'development') ? 'http://localhost:23367/'
|
|
67
|
-
:
|
|
67
|
+
: Utils.getURLBasedOnApi(this.ctx, 'sidebar/')
|
|
68
68
|
const userData = encodeURIComponent(JSON.stringify(this.ctx.user.data))
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
const location = window.location
|
|
71
71
|
this.iframeLoad(`${baseURL}`
|
|
72
72
|
+ `?pageKey=${encodeURIComponent(this.conf.pageKey)}`
|
|
73
73
|
+ `&site=${encodeURIComponent(this.conf.site || '')}`
|
|
74
74
|
+ `&user=${userData}`
|
|
75
|
+
+ `&referer=${encodeURIComponent(`${location.protocol}//${location.host}${location.pathname}`)}`
|
|
75
76
|
+ `${((this.conf.darkMode) ? `&darkMode=1` : ``)}`)
|
|
76
77
|
|
|
77
78
|
this.$iframeWrap.append(this.$iframe)
|
package/src/context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { marked as libMarked } from 'marked'
|
|
1
2
|
import User from './lib/user'
|
|
2
3
|
import ArtalkConfig from '~/types/artalk-config'
|
|
3
4
|
import { EventPayloadMap, Event, EventScopeType, Handler } from '~/types/event'
|
|
@@ -39,4 +40,6 @@ export default class Context {
|
|
|
39
40
|
.map((evt) => evt.handler)
|
|
40
41
|
.forEach((handler) => handler(payload as any))
|
|
41
42
|
}
|
|
43
|
+
|
|
44
|
+
public markedInstance!: typeof libMarked
|
|
42
45
|
}
|
package/src/defaults.ts
CHANGED
|
@@ -17,6 +17,7 @@ const defaults: ArtalkConfig = {
|
|
|
17
17
|
vote: true,
|
|
18
18
|
voteDown: false,
|
|
19
19
|
uaBadge: true,
|
|
20
|
+
listSort: true,
|
|
20
21
|
pvEl: '#ArtalkPV',
|
|
21
22
|
flatMode: 'auto',
|
|
22
23
|
maxNesting: 3,
|
|
@@ -36,6 +37,7 @@ const defaults: ArtalkConfig = {
|
|
|
36
37
|
children: 400,
|
|
37
38
|
},
|
|
38
39
|
|
|
40
|
+
imgUpload: true,
|
|
39
41
|
reqTimeout: 15000,
|
|
40
42
|
versionCheck: true,
|
|
41
43
|
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -140,43 +140,44 @@ export function versionCompare(a: string, b: string) {
|
|
|
140
140
|
return 0
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
|
|
155
|
-
const nMarked = libMarked
|
|
156
|
-
libMarked.setOptions({
|
|
157
|
-
renderer,
|
|
158
|
-
highlight: (code) => hanabi(code),
|
|
159
|
-
pedantic: false,
|
|
160
|
-
gfm: true,
|
|
161
|
-
breaks: true,
|
|
162
|
-
smartLists: true,
|
|
163
|
-
smartypants: true,
|
|
164
|
-
xhtml: false,
|
|
165
|
-
sanitize: true,
|
|
166
|
-
sanitizer: (html) => insane(html, {
|
|
167
|
-
...insane.defaults,
|
|
168
|
-
allowedAttributes: {
|
|
169
|
-
...insane.defaults.allowedAttributes,
|
|
170
|
-
img: ['src', 'atk-emoticon']
|
|
171
|
-
},
|
|
172
|
-
}),
|
|
173
|
-
silent: true,
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
markedInstance = nMarked
|
|
143
|
+
/** 初始化 marked */
|
|
144
|
+
export function initMarked(ctx: Context) {
|
|
145
|
+
const renderer = new libMarked.Renderer()
|
|
146
|
+
const linkRenderer = renderer.link
|
|
147
|
+
renderer.link = (href, title, text) => {
|
|
148
|
+
const localLink = href?.startsWith(`${window.location.protocol}//${window.location.hostname}`);
|
|
149
|
+
const html = linkRenderer.call(renderer as any, href, title, text);
|
|
150
|
+
return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
|
|
177
151
|
}
|
|
178
152
|
|
|
179
|
-
|
|
153
|
+
// @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
|
|
154
|
+
const nMarked = libMarked
|
|
155
|
+
libMarked.setOptions({
|
|
156
|
+
renderer,
|
|
157
|
+
highlight: (code) => hanabi(code),
|
|
158
|
+
pedantic: false,
|
|
159
|
+
gfm: true,
|
|
160
|
+
breaks: true,
|
|
161
|
+
smartLists: true,
|
|
162
|
+
smartypants: true,
|
|
163
|
+
xhtml: false,
|
|
164
|
+
sanitize: true,
|
|
165
|
+
sanitizer: (html) => insane(html, {
|
|
166
|
+
...insane.defaults,
|
|
167
|
+
allowedAttributes: {
|
|
168
|
+
...insane.defaults.allowedAttributes,
|
|
169
|
+
img: ['src', 'atk-emoticon']
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
silent: true,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
ctx.markedInstance = nMarked
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 解析 markdown */
|
|
179
|
+
export function marked(ctx: Context, src: string): string {
|
|
180
|
+
return ctx.markedInstance.parse(src)
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
/** 获取修正后的 UserAgent */
|
|
@@ -203,3 +204,23 @@ export async function getCorrectUserAgent() {
|
|
|
203
204
|
|
|
204
205
|
return uaRaw
|
|
205
206
|
}
|
|
207
|
+
|
|
208
|
+
/** 是否为完整的 URL */
|
|
209
|
+
export function isValidURL(urlRaw: string) {
|
|
210
|
+
// @link https://www.rfc-editor.org/rfc/rfc3986
|
|
211
|
+
let url: URL
|
|
212
|
+
try {
|
|
213
|
+
url = new URL(urlRaw)
|
|
214
|
+
} catch (_) { return false }
|
|
215
|
+
return url.protocol === "http:" || url.protocol === "https:"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 获取基于 conf.server 的 URL */
|
|
219
|
+
export function getURLBasedOnApi(ctx: Context, path: string) {
|
|
220
|
+
return `${ctx.conf.server.replace(/\/api\/?$/, '')}/${path.replace(/^\//, '')}`
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** 获取基于某个 baseURL 的 URL */
|
|
224
|
+
export function getURLBasedOn(baseURL: string, path: string) {
|
|
225
|
+
return `${baseURL.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
226
|
+
}
|