artalk 2.3.3 → 2.3.4
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/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +190 -57
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/dist/ArtalkLite.css +1 -1
- package/dist/ArtalkLite.es.js +183 -57
- package/dist/ArtalkLite.iife.js +1 -1
- package/dist/ArtalkLite.js +1 -1
- package/package.json +3 -3
- package/src/api/index.ts +7 -2
- package/src/api/request.ts +12 -8
- package/src/artalk.ts +26 -31
- package/src/comment/render.ts +24 -14
- package/src/context.ts +39 -0
- package/src/editor/editor.ts +110 -15
- package/src/i18n/en.ts +3 -0
- package/src/i18n/zh-CN.ts +4 -1
- package/src/list/list-lite.ts +8 -0
- package/src/style/comment.less +2 -3
- package/test/CSRF.html +26 -0
- package/types/artalk-data.d.ts +3 -0
- package/types/context.d.ts +15 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artalk",
|
|
3
|
-
"version": "2.3.
|
|
4
|
-
"backendMinVersion": "2.1.
|
|
3
|
+
"version": "2.3.4",
|
|
4
|
+
"backendMinVersion": "2.1.10",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"description": "A Selfhosted Comment System",
|
|
7
7
|
"keywords": [
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"hanabi": "^0.4.0",
|
|
31
31
|
"insane": "^2.6.2",
|
|
32
|
-
"marked": "^4.0.
|
|
32
|
+
"marked": "^4.0.17",
|
|
33
33
|
"abortcontroller-polyfill": "^1.7.3"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
package/src/api/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
|
|
2
2
|
import ArtalkConfig from '~/types/artalk-config'
|
|
3
3
|
import Context from '~/types/context'
|
|
4
|
-
import { Fetch, ToFormData, POST
|
|
4
|
+
import { Fetch, ToFormData, POST } from './request'
|
|
5
5
|
import * as Utils from '../lib/utils'
|
|
6
6
|
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
|
|
7
7
|
|
|
@@ -58,7 +58,7 @@ export default class Api {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** 评论 · 修改 */
|
|
61
|
-
public async commentEdit(data: CommentData) {
|
|
61
|
+
public async commentEdit(data: Partial<CommentData>) {
|
|
62
62
|
const params: any = {
|
|
63
63
|
...data,
|
|
64
64
|
}
|
|
@@ -126,6 +126,11 @@ export default class Api {
|
|
|
126
126
|
return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/** 用户 · 注销 */
|
|
130
|
+
public async logout() {
|
|
131
|
+
return POST(this.ctx, `${this.baseURL}/logout`)
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
// ============================
|
|
130
135
|
// 页面 Page
|
|
131
136
|
// ============================
|
package/src/api/request.ts
CHANGED
|
@@ -9,6 +9,9 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
|
|
|
9
9
|
init.headers = headers
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// 跨域请求携带 cookie (无需)
|
|
13
|
+
// init.credentials = 'include'
|
|
14
|
+
|
|
12
15
|
// 请求操作
|
|
13
16
|
const resp = await timeoutFetch(ctx, input, timeout || ctx.conf.reqTimeout || 15000, init)
|
|
14
17
|
|
|
@@ -60,19 +63,20 @@ export async function POST<T>(ctx: Context, url: string, data?: {[key: string]:
|
|
|
60
63
|
const init: RequestInit = {
|
|
61
64
|
method: 'POST',
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
data = { site_name: ctx.conf.site || '', ...(data || {}) } // 总是携带 site_name
|
|
67
|
+
init.body = ToFormData(data)
|
|
64
68
|
|
|
65
69
|
const json = await Fetch(ctx, url, init)
|
|
66
70
|
return ((json.data || {}) as T)
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
/** 公共 GET 请求 */
|
|
70
|
-
export async function GET<T>(ctx: Context, url: string, data?: {[key: string]: any}) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
73
|
+
/** 公共 GET 请求 (无 GET 方式 API) */
|
|
74
|
+
// export async function GET<T>(ctx: Context, url: string, data?: {[key: string]: any}) {
|
|
75
|
+
// const json = await Fetch(ctx, url + (data ? (`?${new URLSearchParams(data)}`) : ''), {
|
|
76
|
+
// method: 'GET',
|
|
77
|
+
// })
|
|
78
|
+
// return ((json.data || {}) as T)
|
|
79
|
+
// }
|
|
76
80
|
|
|
77
81
|
/** 对象转 FormData */
|
|
78
82
|
export function ToFormData(object: {[key: string]: any}): FormData {
|
package/src/artalk.ts
CHANGED
|
@@ -13,7 +13,7 @@ import Editor from './editor'
|
|
|
13
13
|
import List from './list'
|
|
14
14
|
import SidebarLayer from './layer/sidebar-layer'
|
|
15
15
|
|
|
16
|
-
import Layer
|
|
16
|
+
import Layer from './layer'
|
|
17
17
|
import * as Utils from './lib/utils'
|
|
18
18
|
import * as Ui from './lib/ui'
|
|
19
19
|
import * as Stat from './lib/stat'
|
|
@@ -35,9 +35,11 @@ export default class Artalk {
|
|
|
35
35
|
public list!: List
|
|
36
36
|
public sidebarLayer!: SidebarLayer
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** Plugins (in global scope) */
|
|
39
|
+
protected static Plugins: ArtalkPlug[] = [ Stat.PvCountWidget ]
|
|
40
|
+
|
|
41
|
+
/** Plugins (in a instance scope) */
|
|
42
|
+
protected instancePlugins: ArtalkPlug[] = []
|
|
41
43
|
|
|
42
44
|
constructor(customConf: Partial<ArtalkConfig>) {
|
|
43
45
|
/* 初始化基本配置 */
|
|
@@ -86,7 +88,7 @@ export default class Artalk {
|
|
|
86
88
|
// 事件绑定初始化
|
|
87
89
|
this.initEventBind()
|
|
88
90
|
|
|
89
|
-
// 插件初始化
|
|
91
|
+
// 插件初始化 (global scope)
|
|
90
92
|
Artalk.Plugins.forEach(plugin => {
|
|
91
93
|
if (typeof plugin === 'function')
|
|
92
94
|
plugin(this.ctx)
|
|
@@ -198,30 +200,6 @@ export default class Artalk {
|
|
|
198
200
|
}
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
/** 设置暗黑模式 */
|
|
202
|
-
public setDarkMode(darkMode: boolean) {
|
|
203
|
-
const darkModeClassName = 'atk-dark-mode'
|
|
204
|
-
|
|
205
|
-
this.ctx.conf.darkMode = darkMode
|
|
206
|
-
this.ctx.trigger('conf-updated')
|
|
207
|
-
|
|
208
|
-
if (this.conf.darkMode) {
|
|
209
|
-
this.$root.classList.add(darkModeClassName)
|
|
210
|
-
} else {
|
|
211
|
-
this.$root.classList.remove(darkModeClassName)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// for Layer
|
|
215
|
-
const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
|
|
216
|
-
if ($layerWrap) {
|
|
217
|
-
if (this.conf.darkMode) {
|
|
218
|
-
$layerWrap.classList.add(darkModeClassName)
|
|
219
|
-
} else {
|
|
220
|
-
$layerWrap.classList.remove(darkModeClassName)
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
203
|
/** 监听事件 */
|
|
226
204
|
public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
|
|
227
205
|
this.ctx.on(name, handler, 'external')
|
|
@@ -242,11 +220,28 @@ export default class Artalk {
|
|
|
242
220
|
this.ctx.listReload()
|
|
243
221
|
}
|
|
244
222
|
|
|
245
|
-
/**
|
|
246
|
-
public
|
|
223
|
+
/** 设置暗黑模式 */
|
|
224
|
+
public setDarkMode(darkMode: boolean) {
|
|
225
|
+
this.ctx.setDarkMode(darkMode)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Use Plugin (specific instance) */
|
|
229
|
+
public use(plugin: ArtalkPlug) {
|
|
230
|
+
this.instancePlugins.push(plugin)
|
|
231
|
+
if (typeof plugin === 'function') plugin(this.ctx)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Use Plugin (static method for global scope) */
|
|
235
|
+
public static use(plugin: ArtalkPlug) {
|
|
247
236
|
this.Plugins.push(plugin)
|
|
248
237
|
}
|
|
249
238
|
|
|
239
|
+
/** @deprecated Please replace it with lowercase function name `use(...)`. */
|
|
240
|
+
public static Use(plugin: ArtalkPlug) {
|
|
241
|
+
this.use(plugin)
|
|
242
|
+
console.warn('`Use(...)` is deprecated, replace it with lowercase `use(...)`.')
|
|
243
|
+
}
|
|
244
|
+
|
|
250
245
|
/** 装载数量统计元素 */
|
|
251
246
|
public static LoadCountWidget(customConf: Partial<ArtalkConfig>) {
|
|
252
247
|
const conf = this.HandelBaseConf(customConf)
|
package/src/comment/render.ts
CHANGED
|
@@ -214,7 +214,7 @@ export default class CommentRender {
|
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
//
|
|
217
|
+
// 回复按钮
|
|
218
218
|
if (this.data.is_allow_reply) {
|
|
219
219
|
const replyBtn = Utils.createElement(`<span>${this.ctx.$t('reply')}</span>`)
|
|
220
220
|
this.$actions.append(replyBtn)
|
|
@@ -228,7 +228,7 @@ export default class CommentRender {
|
|
|
228
228
|
})
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
//
|
|
231
|
+
// 折叠按钮
|
|
232
232
|
const collapseBtn = new ActionBtn(this.ctx, {
|
|
233
233
|
text: () => (this.data.is_collapsed ? this.ctx.$t('expand') : this.ctx.$t('collapse')),
|
|
234
234
|
adminOnly: true
|
|
@@ -238,7 +238,7 @@ export default class CommentRender {
|
|
|
238
238
|
this.comment.getActions().adminEdit('collapsed', collapseBtn)
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
-
//
|
|
241
|
+
// 审核按钮
|
|
242
242
|
const pendingBtn = new ActionBtn(this.ctx, {
|
|
243
243
|
text: () => (this.data.is_pending ? this.ctx.$t('pending') : this.ctx.$t('approved')),
|
|
244
244
|
adminOnly: true
|
|
@@ -248,7 +248,27 @@ export default class CommentRender {
|
|
|
248
248
|
this.comment.getActions().adminEdit('pending', pendingBtn)
|
|
249
249
|
})
|
|
250
250
|
|
|
251
|
-
//
|
|
251
|
+
// 置顶按钮
|
|
252
|
+
const pinnedBtn = new ActionBtn(this.ctx, {
|
|
253
|
+
text: () => (this.data.is_pinned ? this.ctx.$t('unpin') : this.ctx.$t('pin')),
|
|
254
|
+
adminOnly: true
|
|
255
|
+
})
|
|
256
|
+
pinnedBtn.appendTo(this.$actions)
|
|
257
|
+
pinnedBtn.setClick(() => {
|
|
258
|
+
this.comment.getActions().adminEdit('pinned', pendingBtn)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// 编辑按钮
|
|
262
|
+
const editBtn = new ActionBtn(this.ctx, {
|
|
263
|
+
text: this.ctx.$t('edit'),
|
|
264
|
+
adminOnly: true
|
|
265
|
+
})
|
|
266
|
+
editBtn.appendTo(this.$actions)
|
|
267
|
+
editBtn.setClick(() => {
|
|
268
|
+
this.ctx.editComment(this.data, this.$el)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// 删除按钮
|
|
252
272
|
const delBtn = new ActionBtn(this.ctx, {
|
|
253
273
|
text: this.ctx.$t('delete'),
|
|
254
274
|
confirm: true,
|
|
@@ -259,16 +279,6 @@ export default class CommentRender {
|
|
|
259
279
|
delBtn.setClick(() => {
|
|
260
280
|
this.comment.getActions().adminDelete(delBtn)
|
|
261
281
|
})
|
|
262
|
-
|
|
263
|
-
// 绑定置顶按钮事件
|
|
264
|
-
const pinnedBtn = new ActionBtn(this.ctx, {
|
|
265
|
-
text: () => (this.data.is_pinned ? this.ctx.$t('unpin') : this.ctx.$t('pin')),
|
|
266
|
-
adminOnly: true
|
|
267
|
-
})
|
|
268
|
-
pinnedBtn.appendTo(this.$actions)
|
|
269
|
-
pinnedBtn.setClick(() => {
|
|
270
|
-
this.comment.getActions().adminEdit('pinned', pendingBtn)
|
|
271
|
-
})
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
/** 内容限高检测 */
|
package/src/context.ts
CHANGED
|
@@ -115,6 +115,10 @@ export default class Context implements ContextApi {
|
|
|
115
115
|
this.list.insertComment(commentData)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
public updateComment(commentData: CommentData): void {
|
|
119
|
+
this.list.updateComment(commentData)
|
|
120
|
+
}
|
|
121
|
+
|
|
118
122
|
public replyComment(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void {
|
|
119
123
|
this.editor.setReply(commentData, $comment, scroll)
|
|
120
124
|
}
|
|
@@ -123,6 +127,14 @@ export default class Context implements ContextApi {
|
|
|
123
127
|
this.editor.cancelReply()
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
public editComment(commentData: CommentData, $comment: HTMLElement): void {
|
|
131
|
+
this.editor.setEditComment(commentData, $comment)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public cancelEditComment(): void {
|
|
135
|
+
this.editor.cancelEditComment()
|
|
136
|
+
}
|
|
137
|
+
|
|
126
138
|
public updateNotifies(notifies: NotifyData[]): void {
|
|
127
139
|
this.list.updateUnread(notifies)
|
|
128
140
|
}
|
|
@@ -132,6 +144,10 @@ export default class Context implements ContextApi {
|
|
|
132
144
|
this.list.reload()
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
public reload(): void {
|
|
148
|
+
this.listReload()
|
|
149
|
+
}
|
|
150
|
+
|
|
135
151
|
public listRefreshUI(): void {
|
|
136
152
|
this.list.refreshUI()
|
|
137
153
|
}
|
|
@@ -234,4 +250,27 @@ export default class Context implements ContextApi {
|
|
|
234
250
|
|
|
235
251
|
return str
|
|
236
252
|
}
|
|
253
|
+
|
|
254
|
+
public setDarkMode(darkMode: boolean): void {
|
|
255
|
+
const darkModeClassName = 'atk-dark-mode'
|
|
256
|
+
|
|
257
|
+
this.conf.darkMode = darkMode
|
|
258
|
+
this.trigger('conf-updated')
|
|
259
|
+
|
|
260
|
+
if (this.conf.darkMode) {
|
|
261
|
+
this.$root.classList.add(darkModeClassName)
|
|
262
|
+
} else {
|
|
263
|
+
this.$root.classList.remove(darkModeClassName)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// for Layer
|
|
267
|
+
const { $wrap: $layerWrap } = GetLayerWrap(this)
|
|
268
|
+
if ($layerWrap) {
|
|
269
|
+
if (this.conf.darkMode) {
|
|
270
|
+
$layerWrap.classList.add(darkModeClassName)
|
|
271
|
+
} else {
|
|
272
|
+
$layerWrap.classList.remove(darkModeClassName)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
237
276
|
}
|
package/src/editor/editor.ts
CHANGED
|
@@ -31,10 +31,18 @@ export default class Editor extends Component {
|
|
|
31
31
|
public $submitBtn: HTMLButtonElement
|
|
32
32
|
public $notifyWrap: HTMLElement
|
|
33
33
|
|
|
34
|
+
// Editor 的不同状态
|
|
35
|
+
private isTraveling = false
|
|
36
|
+
|
|
37
|
+
/** 回复评论 */
|
|
34
38
|
private replyComment: CommentData|null = null
|
|
35
39
|
private $sendReply: HTMLElement|null = null
|
|
40
|
+
public get isReplyMode() { return this.replyComment !== null }
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
/** 编辑评论 */
|
|
43
|
+
private editComment: CommentData|null = null
|
|
44
|
+
private $editCancelBtn: HTMLElement|null = null
|
|
45
|
+
public get isEditMode() { return this.editComment !== null }
|
|
38
46
|
|
|
39
47
|
/** 启用的插件 */
|
|
40
48
|
private readonly ENABLED_PLUGS = [ EmoticonsPlug, UploadPlug, PreviewPlug, HeaderInputPlug ]
|
|
@@ -98,6 +106,8 @@ export default class Editor extends Component {
|
|
|
98
106
|
}
|
|
99
107
|
|
|
100
108
|
private onHeaderInput(key: string, $input: HTMLInputElement) {
|
|
109
|
+
if (this.isEditMode) return // 评论编辑模式,不修改个人信息
|
|
110
|
+
|
|
101
111
|
this.user.update({
|
|
102
112
|
[key]: $input.value.trim()
|
|
103
113
|
})
|
|
@@ -128,8 +138,13 @@ export default class Editor extends Component {
|
|
|
128
138
|
})
|
|
129
139
|
}
|
|
130
140
|
|
|
141
|
+
private refreshSendBtnText() {
|
|
142
|
+
if (this.isEditMode) this.$submitBtn.innerText = this.$t('save')
|
|
143
|
+
else this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
|
|
144
|
+
}
|
|
145
|
+
|
|
131
146
|
private initSubmitBtn() {
|
|
132
|
-
this
|
|
147
|
+
this.refreshSendBtnText()
|
|
133
148
|
this.$submitBtn.addEventListener('click', () => (this.submit()))
|
|
134
149
|
}
|
|
135
150
|
|
|
@@ -186,21 +201,16 @@ export default class Editor extends Component {
|
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
203
|
|
|
189
|
-
public clearEditor() {
|
|
190
|
-
this.setContent('')
|
|
191
|
-
this.cancelReply()
|
|
192
|
-
}
|
|
193
|
-
|
|
194
204
|
private adjustTextareaHeight() {
|
|
195
205
|
const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
|
|
196
206
|
this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
|
|
197
207
|
this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
/** 设置回复评论 */
|
|
200
211
|
public setReply(commentData: CommentData, $comment: HTMLElement, scroll = true) {
|
|
201
|
-
if (this.
|
|
202
|
-
|
|
203
|
-
}
|
|
212
|
+
if (this.isEditMode) this.cancelEditComment()
|
|
213
|
+
if (this.isReplyMode) this.cancelReply()
|
|
204
214
|
|
|
205
215
|
if (this.$sendReply === null) {
|
|
206
216
|
this.$sendReply = Utils.createElement(`<div class="atk-send-reply">${this.$t('reply')} <span class="atk-text"></span><span class="atk-cancel">×</span></div>`);
|
|
@@ -221,6 +231,7 @@ export default class Editor extends Component {
|
|
|
221
231
|
this.$textarea.focus()
|
|
222
232
|
}
|
|
223
233
|
|
|
234
|
+
/** 取消回复评论 */
|
|
224
235
|
public cancelReply() {
|
|
225
236
|
if (this.$sendReply !== null) {
|
|
226
237
|
this.$sendReply.remove()
|
|
@@ -233,6 +244,53 @@ export default class Editor extends Component {
|
|
|
233
244
|
}
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
/** 设置编辑评论 */
|
|
248
|
+
public setEditComment(commentData: CommentData, $comment: HTMLElement) {
|
|
249
|
+
if (this.isEditMode) this.cancelEditComment()
|
|
250
|
+
if (this.isReplyMode) this.cancelReply()
|
|
251
|
+
|
|
252
|
+
if (this.$editCancelBtn === null) {
|
|
253
|
+
this.$editCancelBtn = Utils.createElement(`<div class="atk-send-reply">${this.$t('editCancel')}<span class="atk-cancel">×</span></div>`);
|
|
254
|
+
this.$editCancelBtn.onclick = () => {
|
|
255
|
+
this.cancelEditComment()
|
|
256
|
+
}
|
|
257
|
+
this.$textareaWrap.append(this.$editCancelBtn)
|
|
258
|
+
}
|
|
259
|
+
this.editComment = commentData
|
|
260
|
+
|
|
261
|
+
this.$header.style.display = 'none' // TODO 暂时隐藏
|
|
262
|
+
|
|
263
|
+
this.travel($comment)
|
|
264
|
+
|
|
265
|
+
this.$nick.value = commentData.nick
|
|
266
|
+
this.$email.value = commentData.email || ''
|
|
267
|
+
this.$link.value = commentData.link
|
|
268
|
+
|
|
269
|
+
this.setContent(commentData.content)
|
|
270
|
+
this.$textarea.focus()
|
|
271
|
+
|
|
272
|
+
this.refreshSendBtnText()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** 取消编辑评论 */
|
|
276
|
+
public cancelEditComment() {
|
|
277
|
+
if (this.$editCancelBtn !== null) {
|
|
278
|
+
this.$editCancelBtn.remove()
|
|
279
|
+
this.$editCancelBtn = null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.editComment = null
|
|
283
|
+
this.travelBack()
|
|
284
|
+
|
|
285
|
+
this.$nick.value = this.user.data.nick
|
|
286
|
+
this.$email.value = this.user.data.email
|
|
287
|
+
this.$link.value = this.user.data.link
|
|
288
|
+
|
|
289
|
+
this.setContent('')
|
|
290
|
+
this.refreshSendBtnText()
|
|
291
|
+
this.$header.style.display = '' // TODO
|
|
292
|
+
}
|
|
293
|
+
|
|
236
294
|
public showNotify(msg: string, type: "i"|"s"|"w"|"e") {
|
|
237
295
|
Ui.showNotify(this.$notifyWrap, msg, type)
|
|
238
296
|
}
|
|
@@ -245,7 +303,7 @@ export default class Editor extends Component {
|
|
|
245
303
|
Ui.hideLoading(this.$el)
|
|
246
304
|
}
|
|
247
305
|
|
|
248
|
-
/**
|
|
306
|
+
/** 点击评论提交按钮事件 */
|
|
249
307
|
public async submit() {
|
|
250
308
|
if (this.getFinalContent().trim() === '') {
|
|
251
309
|
this.$textarea.focus()
|
|
@@ -256,6 +314,17 @@ export default class Editor extends Component {
|
|
|
256
314
|
|
|
257
315
|
this.showLoading()
|
|
258
316
|
|
|
317
|
+
if (!this.isEditMode) {
|
|
318
|
+
await this.submitAdd()
|
|
319
|
+
} else {
|
|
320
|
+
await this.submitEdit()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.hideLoading()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 创建评论 */
|
|
327
|
+
public async submitAdd() {
|
|
259
328
|
try {
|
|
260
329
|
const nComment = await this.ctx.getApi().add({
|
|
261
330
|
content: this.getFinalContent(),
|
|
@@ -274,14 +343,40 @@ export default class Editor extends Component {
|
|
|
274
343
|
}
|
|
275
344
|
|
|
276
345
|
this.ctx.insertComment(nComment)
|
|
277
|
-
|
|
346
|
+
|
|
347
|
+
// 复原编辑器
|
|
348
|
+
this.setContent('')
|
|
349
|
+
this.cancelReply()
|
|
350
|
+
|
|
351
|
+
this.ctx.trigger('editor-submitted')
|
|
352
|
+
} catch (err: any) {
|
|
353
|
+
console.error(err)
|
|
354
|
+
this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** 修改评论 */
|
|
359
|
+
public async submitEdit() {
|
|
360
|
+
try {
|
|
361
|
+
const saveData = {
|
|
362
|
+
content: this.getFinalContent(),
|
|
363
|
+
nick: this.$nick.value,
|
|
364
|
+
email: this.$email.value,
|
|
365
|
+
link: this.$link.value,
|
|
366
|
+
}
|
|
367
|
+
const nComment = await this.ctx.getApi().commentEdit({
|
|
368
|
+
...this.editComment, ...saveData
|
|
369
|
+
})
|
|
370
|
+
this.ctx.updateComment(nComment)
|
|
371
|
+
|
|
372
|
+
// 复原编辑器
|
|
373
|
+
this.setContent('')
|
|
374
|
+
this.cancelEditComment()
|
|
375
|
+
|
|
278
376
|
this.ctx.trigger('editor-submitted')
|
|
279
377
|
} catch (err: any) {
|
|
280
378
|
console.error(err)
|
|
281
379
|
this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
|
|
282
|
-
return
|
|
283
|
-
} finally {
|
|
284
|
-
this.hideLoading()
|
|
285
380
|
}
|
|
286
381
|
}
|
|
287
382
|
|
package/src/i18n/en.ts
CHANGED
|
@@ -3,6 +3,7 @@ export default {
|
|
|
3
3
|
placeholder: 'Leave a comment',
|
|
4
4
|
noComment: 'No Comment',
|
|
5
5
|
send: 'Send',
|
|
6
|
+
save: 'Save',
|
|
6
7
|
nick: 'Nickname',
|
|
7
8
|
email: 'Email',
|
|
8
9
|
link: 'Website',
|
|
@@ -42,6 +43,8 @@ export default {
|
|
|
42
43
|
approved: 'Approved',
|
|
43
44
|
pending: 'Pending',
|
|
44
45
|
pendingMsg: 'Pending, visible only to commenter.',
|
|
46
|
+
edit: 'Edit',
|
|
47
|
+
editCancel: 'Cancel Edit',
|
|
45
48
|
delete: 'Delete',
|
|
46
49
|
deleteConfirm: 'Confirm',
|
|
47
50
|
pin: 'Pin',
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -5,6 +5,7 @@ const zhCN: I18n = {
|
|
|
5
5
|
placeholder: '键入内容...',
|
|
6
6
|
noComment: '「此时无声胜有声」',
|
|
7
7
|
send: '发送评论',
|
|
8
|
+
save: '保存评论',
|
|
8
9
|
nick: '昵称',
|
|
9
10
|
email: '邮箱',
|
|
10
11
|
link: '网址',
|
|
@@ -40,10 +41,12 @@ const zhCN: I18n = {
|
|
|
40
41
|
collapse: '折叠',
|
|
41
42
|
collapsed: '已折叠',
|
|
42
43
|
collapsedMsg: '该评论已被系统或管理员折叠',
|
|
43
|
-
expand: '
|
|
44
|
+
expand: '展开',
|
|
44
45
|
approved: '已审',
|
|
45
46
|
pending: '待审',
|
|
46
47
|
pendingMsg: '审核中,仅本人可见。',
|
|
48
|
+
edit: '编辑',
|
|
49
|
+
editCancel: '取消编辑',
|
|
47
50
|
delete: '删除',
|
|
48
51
|
deleteConfirm: '确认删除',
|
|
49
52
|
pin: '置顶',
|
package/src/list/list-lite.ts
CHANGED
|
@@ -429,6 +429,14 @@ export default class ListLite extends Component {
|
|
|
429
429
|
this.ctx.trigger('list-inserted', commentData)
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
/** 更新评论 */
|
|
433
|
+
public updateComment(commentData: CommentData) {
|
|
434
|
+
const comment = this.ctx.findComment(commentData.id)
|
|
435
|
+
if (comment) {
|
|
436
|
+
comment.setData(commentData)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
432
440
|
/** 更新未读数据 */
|
|
433
441
|
public updateUnread(notifies: NotifyData[]) {
|
|
434
442
|
this.unread = notifies
|
package/src/style/comment.less
CHANGED
package/test/CSRF.html
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>CSRF Test</title>
|
|
8
|
+
|
|
9
|
+
<!-- 解除注释来测试当 referer 为空时的行为 -->
|
|
10
|
+
<!-- <meta name="referrer" content="never"> -->
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<form action="http://localhost:23366/api/add" method="post">
|
|
15
|
+
<input type="text" name="name" value="admin">
|
|
16
|
+
<input type="text" name="email" value="test@example.com">
|
|
17
|
+
<input type="text" name="link" value="">
|
|
18
|
+
<input type="text" name="content" value="CSRF Success">
|
|
19
|
+
<input type="text" name="rid" value="0">
|
|
20
|
+
<input type="text" name="page_key" value="https://artalk.js.org/">
|
|
21
|
+
<input type="text" name="site_name" value="ArtalkDocs">
|
|
22
|
+
<button type="submit">Fire!</button>
|
|
23
|
+
</form>
|
|
24
|
+
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
package/types/artalk-data.d.ts
CHANGED
package/types/context.d.ts
CHANGED
|
@@ -63,18 +63,30 @@ export default interface ContextApi {
|
|
|
63
63
|
/** 插入评论 */
|
|
64
64
|
insertComment(commentData: CommentData): void
|
|
65
65
|
|
|
66
|
+
/** 更新评论 */
|
|
67
|
+
updateComment(commentData: CommentData): void
|
|
68
|
+
|
|
66
69
|
/** 评论回复 */
|
|
67
70
|
replyComment(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void
|
|
68
71
|
|
|
69
72
|
/** 取消回复评论 */
|
|
70
73
|
cancelReplyComment(): void
|
|
71
74
|
|
|
75
|
+
/** 编辑评论 */
|
|
76
|
+
editComment(commentData: CommentData, $comment: HTMLElement): void
|
|
77
|
+
|
|
78
|
+
/** 取消编辑评论 */
|
|
79
|
+
cancelEditComment(): void
|
|
80
|
+
|
|
72
81
|
/** 更新通知数据 */
|
|
73
82
|
updateNotifies(notifies: NotifyData[]): void
|
|
74
83
|
|
|
75
84
|
/** 列表 - 重新加载数据 */
|
|
76
85
|
listReload(): void
|
|
77
86
|
|
|
87
|
+
/** 列表 - 重新加载数据 (别名) */
|
|
88
|
+
reload(): void
|
|
89
|
+
|
|
78
90
|
/** 列表 - UI 更新 */
|
|
79
91
|
listRefreshUI(): void
|
|
80
92
|
|
|
@@ -125,4 +137,7 @@ export default interface ContextApi {
|
|
|
125
137
|
|
|
126
138
|
/** i18n 翻译 */
|
|
127
139
|
$t(key: keyof I18n, args?: {[key: string]: string}): string
|
|
140
|
+
|
|
141
|
+
/** 设置暗黑模式 */
|
|
142
|
+
setDarkMode(darkMode: boolean): void
|
|
128
143
|
}
|