artalk 2.2.12 → 2.3.2
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 +1535 -1175
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/dist/ArtalkLite.css +1 -0
- package/dist/ArtalkLite.es.js +5123 -0
- package/dist/ArtalkLite.iife.js +1 -0
- package/dist/ArtalkLite.js +1 -0
- package/index.html +2 -2
- package/package.json +6 -5
- package/src/api/index.ts +20 -1
- package/src/api/request.ts +7 -7
- package/src/artalk.ts +117 -121
- package/src/comment/actions.ts +86 -0
- package/src/{components/html → comment}/comment.html +1 -1
- package/src/comment/comment.ts +191 -0
- package/src/comment/index.ts +3 -0
- package/src/comment/render.ts +425 -0
- package/src/components/action-btn.ts +6 -3
- package/src/components/dialog.ts +7 -3
- package/src/components/read-more-btn.ts +6 -2
- package/src/context.ts +204 -13
- package/src/defaults.ts +16 -5
- package/src/editor/editor.html +18 -0
- package/src/editor/editor.ts +419 -0
- package/src/editor/index.ts +3 -0
- package/src/editor/plugs/editor-plug.ts +51 -0
- package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.less +2 -1
- package/src/{components/editor-plugs → editor/plugs}/emoticons-plug.ts +65 -63
- package/src/editor/plugs/header-input-plug.ts +78 -0
- package/src/{components/editor-plugs → editor/plugs}/preview-plug.less +0 -0
- package/src/editor/plugs/preview-plug.ts +40 -0
- package/src/editor/plugs/upload-plug.ts +124 -0
- package/src/i18n/en.ts +78 -0
- package/src/i18n/index.ts +21 -0
- package/src/i18n/zh-CN.ts +82 -0
- package/src/layer/index.ts +4 -0
- package/src/{components → layer}/layer.ts +9 -9
- package/src/{components/html → layer}/sidebar-layer.html +0 -0
- package/src/{components → layer}/sidebar-layer.ts +17 -47
- package/src/lib/checker/admin-checker.ts +7 -7
- package/src/lib/checker/captcha-checker.ts +5 -6
- package/src/lib/checker/index.ts +22 -17
- package/src/lib/component.ts +7 -2
- package/src/lib/stat.ts +63 -0
- package/src/lib/ui.ts +6 -12
- package/src/lib/user.ts +20 -3
- package/src/lib/utils.ts +23 -9
- package/src/list/index.ts +3 -0
- package/src/{components → list}/list-lite.ts +80 -100
- package/src/{components → list}/list-nest.ts +0 -0
- package/src/{components/html → list}/list.html +3 -6
- package/src/{components → list}/list.ts +44 -42
- package/src/style/_variables.less +1 -2
- package/src/style/comment.less +26 -13
- package/src/style/editor.less +22 -14
- package/src/style/list.less +6 -9
- package/src/style/main.less +9 -8
- package/src/style/pagination.less +1 -1
- package/test/ArtalkCount.html +16 -0
- package/test/ArtalkI18n.html +15 -0
- package/test/ArtalkLite.html +15 -0
- package/types/artalk-config.d.ts +50 -32
- package/types/context.d.ts +128 -0
- package/types/event.d.ts +6 -66
- package/types/global.d.ts +1 -0
- package/types/plug.d.ts +4 -0
- package/{vite-common.config.ts → vite-base.config.ts} +6 -4
- package/vite-demo.config.ts +2 -4
- package/vite-lite.config.ts +22 -0
- package/vite.config.ts +9 -12
- package/src/components/_component.ts.txt +0 -10
- package/src/components/comment.ts +0 -605
- package/src/components/editor-plugs/_example-plug.ts.txt +0 -29
- package/src/components/editor-plugs/editor-plug.ts +0 -20
- package/src/components/editor-plugs/preview-plug.ts +0 -48
- package/src/components/editor.ts +0 -607
- package/src/components/html/editor.html +0 -18
|
@@ -14,24 +14,53 @@ type OwOFormatType = {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export default class EmoticonsPlug extends EditorPlug {
|
|
17
|
-
public
|
|
17
|
+
public static Name = 'emoticons'
|
|
18
|
+
|
|
19
|
+
declare protected $panel: HTMLElement
|
|
20
|
+
|
|
18
21
|
public emoticons: EmoticonListData = []
|
|
22
|
+
public loadingTask: Promise<void>|null = null
|
|
19
23
|
|
|
20
24
|
public $grpWrap!: HTMLElement
|
|
21
25
|
public $grpSwitcher!: HTMLElement
|
|
22
26
|
|
|
23
|
-
public
|
|
24
|
-
|
|
25
|
-
constructor (public editor: Editor) {
|
|
27
|
+
public constructor(public editor: Editor) {
|
|
26
28
|
super(editor)
|
|
27
29
|
|
|
28
|
-
this
|
|
30
|
+
this.registerPanel(`<div class="atk-editor-plug-emoticons"></div>`)
|
|
31
|
+
this.registerBtn(this.ctx.$t('emoticon'))
|
|
32
|
+
|
|
33
|
+
// 表情包预加载
|
|
34
|
+
window.setTimeout(() => {
|
|
35
|
+
this.loadEmoticonsData()
|
|
36
|
+
}, 1000) // 延迟 1s 加载
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public onPanelShow() {
|
|
40
|
+
;(async () => {
|
|
41
|
+
await this.loadEmoticonsData()
|
|
42
|
+
|
|
43
|
+
// 初始化元素
|
|
44
|
+
if (!this.isImgLoaded) {
|
|
45
|
+
this.initEmoticonsList()
|
|
46
|
+
this.isImgLoaded = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 延迟执行,防止无法读取高度
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
this.changeListHeight()
|
|
52
|
+
}, 30)
|
|
53
|
+
})()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public onPanelHide() {
|
|
57
|
+
this.$panel.parentElement!.style.height = ''
|
|
29
58
|
}
|
|
30
59
|
|
|
31
60
|
isListLoaded = false
|
|
32
61
|
isImgLoaded = false
|
|
33
62
|
|
|
34
|
-
async loadEmoticonsData() {
|
|
63
|
+
public async loadEmoticonsData() {
|
|
35
64
|
if (this.isListLoaded) return
|
|
36
65
|
if (this.loadingTask !== null) {
|
|
37
66
|
await this.loadingTask
|
|
@@ -40,23 +69,23 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
40
69
|
|
|
41
70
|
// 数据处理
|
|
42
71
|
this.loadingTask = (async () => {
|
|
43
|
-
Ui.showLoading(this.$
|
|
72
|
+
Ui.showLoading(this.$panel)
|
|
44
73
|
this.emoticons = await this.handleData(this.ctx.conf.emoticons)
|
|
45
|
-
Ui.hideLoading(this.$
|
|
74
|
+
Ui.hideLoading(this.$panel)
|
|
46
75
|
this.loadingTask = null
|
|
47
76
|
this.isListLoaded = true
|
|
48
77
|
})()
|
|
49
78
|
await this.loadingTask
|
|
50
79
|
}
|
|
51
80
|
|
|
52
|
-
async handleData(data: any): Promise<any[]> {
|
|
81
|
+
private async handleData(data: any): Promise<any[]> {
|
|
53
82
|
if (!Array.isArray(data) && ['object', 'string'].includes(typeof data)) {
|
|
54
83
|
data = [data]
|
|
55
84
|
}
|
|
56
85
|
|
|
57
86
|
if (!Array.isArray(data)) {
|
|
58
|
-
Ui.setError(this.$
|
|
59
|
-
Ui.hideLoading(this.$
|
|
87
|
+
Ui.setError(this.$panel, "表情包数据必须为 Array/Object/String 类型")
|
|
88
|
+
Ui.hideLoading(this.$panel)
|
|
60
89
|
return []
|
|
61
90
|
}
|
|
62
91
|
|
|
@@ -105,7 +134,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
105
134
|
}
|
|
106
135
|
|
|
107
136
|
/** 远程加载 */
|
|
108
|
-
async remoteLoad(url: string): Promise<any> {
|
|
137
|
+
private async remoteLoad(url: string): Promise<any> {
|
|
109
138
|
if (!url) return []
|
|
110
139
|
|
|
111
140
|
try {
|
|
@@ -113,14 +142,14 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
113
142
|
const json = await resp.json()
|
|
114
143
|
return json
|
|
115
144
|
} catch (err) {
|
|
116
|
-
Ui.hideLoading(this.$
|
|
117
|
-
Ui.setError(this.$
|
|
145
|
+
Ui.hideLoading(this.$panel)
|
|
146
|
+
Ui.setError(this.$panel, `表情加载失败 ${String(err)}`)
|
|
118
147
|
return []
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
/** 避免表情 item.key 为 null 的情况 */
|
|
123
|
-
solveNullKey(data: EmoticonGrpData[]) {
|
|
152
|
+
private solveNullKey(data: EmoticonGrpData[]) {
|
|
124
153
|
data.forEach((grp) => {
|
|
125
154
|
grp.items.forEach((item, index) => {
|
|
126
155
|
if (!item.key) item.key = `${grp.name} ${index+1}`
|
|
@@ -129,7 +158,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
/** 避免相同 item.key */
|
|
132
|
-
solveSameKey(data: EmoticonGrpData[]) {
|
|
161
|
+
private solveSameKey(data: EmoticonGrpData[]) {
|
|
133
162
|
const tmp: {[key: string]: number} = {}
|
|
134
163
|
data.forEach((grp) => {
|
|
135
164
|
grp.items.forEach(item => {
|
|
@@ -143,7 +172,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
/** 判断是否为 OwO 格式 */
|
|
146
|
-
isOwOFormat(data: any) {
|
|
175
|
+
private isOwOFormat(data: any) {
|
|
147
176
|
try {
|
|
148
177
|
return (typeof data === 'object') && !!Object.values(data).length
|
|
149
178
|
&& Array.isArray(Object.keys(Object.values<any>(data)[0].container))
|
|
@@ -152,7 +181,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
152
181
|
}
|
|
153
182
|
|
|
154
183
|
/** 转换 OwO 格式数据 */
|
|
155
|
-
convertOwO(owoData: OwOFormatType): EmoticonListData {
|
|
184
|
+
private convertOwO(owoData: OwOFormatType): EmoticonListData {
|
|
156
185
|
const dest: EmoticonListData = []
|
|
157
186
|
Object.entries(owoData).forEach(([grpName, grp]) => {
|
|
158
187
|
const nGrp: EmoticonGrpData = { name: grpName, type: grp.type, items: [] }
|
|
@@ -171,10 +200,10 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
/** 初始化表情列表界面 */
|
|
174
|
-
initEmoticonsList
|
|
203
|
+
private initEmoticonsList() {
|
|
175
204
|
// 表情列表
|
|
176
205
|
this.$grpWrap = Utils.createElement(`<div class="atk-grp-wrap"></div>`)
|
|
177
|
-
this.$
|
|
206
|
+
this.$panel.append(this.$grpWrap)
|
|
178
207
|
|
|
179
208
|
this.emoticons.forEach((grp, index) => {
|
|
180
209
|
const $grp = Utils.createElement(`<div class="atk-grp" style="display: none;"></div>`)
|
|
@@ -208,24 +237,25 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
208
237
|
})
|
|
209
238
|
})
|
|
210
239
|
|
|
211
|
-
//
|
|
212
|
-
this
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
240
|
+
// 表情分类切换 bar
|
|
241
|
+
if (this.emoticons.length > 1) {
|
|
242
|
+
this.$grpSwitcher = Utils.createElement(`<div class="atk-grp-switcher"></div>`)
|
|
243
|
+
this.$panel.append(this.$grpSwitcher)
|
|
244
|
+
this.emoticons.forEach((grp, index) => {
|
|
245
|
+
const $item = Utils.createElement('<span />')
|
|
246
|
+
$item.innerText = grp.name
|
|
247
|
+
$item.setAttribute('data-index', String(index))
|
|
248
|
+
$item.onclick = () => (this.openGrp(index))
|
|
249
|
+
this.$grpSwitcher.append($item)
|
|
250
|
+
})
|
|
251
|
+
}
|
|
221
252
|
|
|
222
253
|
// 默认打开第一个分类
|
|
223
|
-
if (this.emoticons.length > 0)
|
|
224
|
-
this.openGrp(0)
|
|
254
|
+
if (this.emoticons.length > 0) this.openGrp(0)
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
/** 打开一个表情组 */
|
|
228
|
-
openGrp
|
|
258
|
+
public openGrp(index: number) {
|
|
229
259
|
Array.from(this.$grpWrap.children).forEach((item) => {
|
|
230
260
|
const el = item as HTMLElement
|
|
231
261
|
if (el.getAttribute('data-index') !== String(index)) {
|
|
@@ -241,41 +271,13 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
241
271
|
this.changeListHeight()
|
|
242
272
|
}
|
|
243
273
|
|
|
244
|
-
|
|
245
|
-
static BtnHTML = '表情'
|
|
246
|
-
|
|
247
|
-
getEl () {
|
|
248
|
-
return this.$el
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
changeListHeight () {
|
|
274
|
+
private changeListHeight() {
|
|
252
275
|
/* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
|
|
253
276
|
this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
|
|
254
277
|
}
|
|
255
278
|
|
|
256
|
-
onShow () {
|
|
257
|
-
;(async () => {
|
|
258
|
-
await this.loadEmoticonsData()
|
|
259
|
-
|
|
260
|
-
// 初始化元素
|
|
261
|
-
if (!this.isImgLoaded) {
|
|
262
|
-
this.initEmoticonsList()
|
|
263
|
-
this.isImgLoaded = true
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 延迟执行,防止无法读取高度
|
|
267
|
-
setTimeout(() => {
|
|
268
|
-
this.changeListHeight()
|
|
269
|
-
}, 30)
|
|
270
|
-
})()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
onHide () {
|
|
274
|
-
this.$el.parentElement!.style.height = ''
|
|
275
|
-
}
|
|
276
|
-
|
|
277
279
|
/** 处理评论 content 中的表情内容 */
|
|
278
|
-
public transEmoticonImageText
|
|
280
|
+
public transEmoticonImageText(text: string) {
|
|
279
281
|
if (!this.emoticons || !Array.isArray(this.emoticons))
|
|
280
282
|
return text
|
|
281
283
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Editor from '../editor'
|
|
2
|
+
import EditorPlug from './editor-plug'
|
|
3
|
+
|
|
4
|
+
export default class HeaderInputPlug extends EditorPlug {
|
|
5
|
+
public static Name = 'headerInput'
|
|
6
|
+
|
|
7
|
+
public constructor(editor: Editor) {
|
|
8
|
+
super(editor)
|
|
9
|
+
|
|
10
|
+
this.registerHeaderInputEvt((key, $input) => {
|
|
11
|
+
if (key === 'nick' || key === 'email') {
|
|
12
|
+
this.fetchUserInfo()
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Link URL 自动补全协议
|
|
17
|
+
this.editor.$link.addEventListener('change', () => {
|
|
18
|
+
const link = this.editor.$link.value.trim()
|
|
19
|
+
if (!!link && !/^(http|https):\/\//.test(link)) {
|
|
20
|
+
this.editor.$link.value = `https://${link}`
|
|
21
|
+
this.ctx.user.update({ link: this.editor.$link.value })
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
queryUserInfo = {
|
|
27
|
+
timeout: <number|null>null,
|
|
28
|
+
abortFunc: <(() => void)|null>null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 远程获取用户数据 */
|
|
32
|
+
fetchUserInfo() {
|
|
33
|
+
this.ctx.user.logout()
|
|
34
|
+
|
|
35
|
+
// 获取用户信息
|
|
36
|
+
if (this.queryUserInfo.timeout) window.clearTimeout(this.queryUserInfo.timeout) // 清除待发出的请求
|
|
37
|
+
if (this.queryUserInfo.abortFunc) this.queryUserInfo.abortFunc() // 之前发出未完成的请求立刻中止
|
|
38
|
+
|
|
39
|
+
this.queryUserInfo.timeout = window.setTimeout(() => {
|
|
40
|
+
this.queryUserInfo.timeout = null // 清理
|
|
41
|
+
|
|
42
|
+
const {req, abort} = this.ctx.getApi().userGet(
|
|
43
|
+
this.ctx.user.data.nick, this.ctx.user.data.email
|
|
44
|
+
)
|
|
45
|
+
this.queryUserInfo.abortFunc = abort
|
|
46
|
+
req.then(data => {
|
|
47
|
+
if (!data.is_login) {
|
|
48
|
+
this.ctx.user.logout()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 未读消息更新
|
|
52
|
+
this.ctx.updateNotifies(data.unread)
|
|
53
|
+
|
|
54
|
+
// 若用户为管理员,执行登陆操作
|
|
55
|
+
if (this.ctx.user.checkHasBasicUserInfo() && !data.is_login && data.user?.is_admin) {
|
|
56
|
+
this.showLoginDialog()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 自动填入 link
|
|
60
|
+
if (data.user && data.user.link) {
|
|
61
|
+
this.editor.$link.value = data.user.link
|
|
62
|
+
this.ctx.user.update({ link: data.user.link })
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch(() => {})
|
|
66
|
+
.finally(() => {
|
|
67
|
+
this.queryUserInfo.abortFunc = null // 清理
|
|
68
|
+
})
|
|
69
|
+
}, 400) // 延迟执行,减少请求次数
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
showLoginDialog() {
|
|
73
|
+
this.ctx.checkAdmin({
|
|
74
|
+
onSuccess: () => {
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import './preview-plug.less'
|
|
2
|
+
|
|
3
|
+
import Editor from '../editor'
|
|
4
|
+
import EditorPlug from './editor-plug'
|
|
5
|
+
|
|
6
|
+
export default class PreviewPlug extends EditorPlug {
|
|
7
|
+
public static Name = 'preview'
|
|
8
|
+
declare protected $panel: HTMLElement
|
|
9
|
+
|
|
10
|
+
private isBind = false
|
|
11
|
+
|
|
12
|
+
public constructor(editor: Editor) {
|
|
13
|
+
super(editor)
|
|
14
|
+
|
|
15
|
+
this.registerPanel(`<div class="atk-editor-plug-preview"></div>`)
|
|
16
|
+
|
|
17
|
+
let btnText = this.editor.$t('preview')
|
|
18
|
+
if (this.ctx.markedInstance) btnText += ` <i title="Markdown is supported"><svg class="markdown" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></i>`
|
|
19
|
+
this.registerBtn(btnText)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public onPanelShow() {
|
|
23
|
+
this.updateContent()
|
|
24
|
+
|
|
25
|
+
if (!this.isBind) {
|
|
26
|
+
const event = () => {
|
|
27
|
+
this.updateContent()
|
|
28
|
+
}
|
|
29
|
+
this.editor.$textarea.addEventListener('input', event)
|
|
30
|
+
this.editor.$textarea.addEventListener('change', event)
|
|
31
|
+
this.isBind = true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public updateContent() {
|
|
36
|
+
if (this.$panel.style.display !== 'none') {
|
|
37
|
+
this.$panel.innerHTML = this.editor.getContentMarked()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as Utils from '@/lib/utils'
|
|
2
|
+
import Editor from '../editor'
|
|
3
|
+
import EditorPlug from './editor-plug'
|
|
4
|
+
|
|
5
|
+
export default class UploadPlug extends EditorPlug {
|
|
6
|
+
public static Name = 'upload'
|
|
7
|
+
|
|
8
|
+
public $imgUploadInput?: HTMLInputElement
|
|
9
|
+
|
|
10
|
+
/** 允许的图片格式 */
|
|
11
|
+
allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
|
|
12
|
+
|
|
13
|
+
public constructor(editor: Editor) {
|
|
14
|
+
super(editor)
|
|
15
|
+
|
|
16
|
+
this.$imgUploadInput = document.createElement('input')
|
|
17
|
+
this.$imgUploadInput.type = 'file'
|
|
18
|
+
this.$imgUploadInput.style.display = 'none'
|
|
19
|
+
this.$imgUploadInput.accept = this.allowImgExts.map(o => `.${o}`).join(',')
|
|
20
|
+
|
|
21
|
+
const $btn = this.registerBtn(`${this.ctx.$t('image')}`)
|
|
22
|
+
$btn.after(this.$imgUploadInput)
|
|
23
|
+
$btn.onclick = () => {
|
|
24
|
+
// 选择图片
|
|
25
|
+
const $input = this.$imgUploadInput!
|
|
26
|
+
$input.onchange = () => {
|
|
27
|
+
(async () => { // 解决阻塞 UI 问题
|
|
28
|
+
if (!$input.files || $input.files.length === 0) return
|
|
29
|
+
const file = $input.files[0]
|
|
30
|
+
this.uploadImg(file)
|
|
31
|
+
})()
|
|
32
|
+
}
|
|
33
|
+
$input.click() // 显示选择图片对话框
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.ctx.on('conf-updated', () => {
|
|
37
|
+
if (!this.ctx.conf.imgUpload) {
|
|
38
|
+
this.getBtn()!.setAttribute('atk-only-admin-show', '')
|
|
39
|
+
this.ctx.checkAdminShowEl()
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 统一从 FileList 获取文件并上传图片方法
|
|
44
|
+
const uploadFromFileList = (files?: FileList) => {
|
|
45
|
+
if (!files) return
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < files.length; i++) {
|
|
48
|
+
const file = files[i]
|
|
49
|
+
this.uploadImg(file)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 拖拽图片
|
|
54
|
+
// @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
55
|
+
// 阻止浏览器的默认释放行为
|
|
56
|
+
this.editor.$textarea.addEventListener('dragover', (evt) => {
|
|
57
|
+
evt.stopPropagation()
|
|
58
|
+
evt.preventDefault()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
this.editor.$textarea.addEventListener('drop', (evt) => {
|
|
62
|
+
const files = evt.dataTransfer?.files
|
|
63
|
+
if (files?.length) {
|
|
64
|
+
evt.preventDefault()
|
|
65
|
+
uploadFromFileList(files)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// 粘贴图片
|
|
70
|
+
this.editor.$textarea.addEventListener('paste', (evt) => {
|
|
71
|
+
const files = evt.clipboardData?.files
|
|
72
|
+
if (files?.length) {
|
|
73
|
+
evt.preventDefault()
|
|
74
|
+
uploadFromFileList(files)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async uploadImg(file: File) {
|
|
80
|
+
const fileExt = /[^.]+$/.exec(file.name)
|
|
81
|
+
if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
|
|
82
|
+
|
|
83
|
+
// 未登录提示
|
|
84
|
+
if (!this.ctx.user.checkHasBasicUserInfo()) {
|
|
85
|
+
this.editor.showNotify(this.ctx.$t('uploadLoginMsg'), 'w')
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 插入图片前换一行
|
|
90
|
+
let insertPrefix = '\n'
|
|
91
|
+
if (this.editor.$textarea.value.trim() === '') insertPrefix = ''
|
|
92
|
+
|
|
93
|
+
// 插入占位加载文字
|
|
94
|
+
const uploadPlaceholderTxt = `${insertPrefix}`
|
|
95
|
+
this.editor.insertContent(uploadPlaceholderTxt)
|
|
96
|
+
|
|
97
|
+
// 上传图片
|
|
98
|
+
let resp: any
|
|
99
|
+
try {
|
|
100
|
+
if (!this.ctx.conf.imgUploader) {
|
|
101
|
+
// 使用 ArtalkGo 进行图片上传
|
|
102
|
+
resp = await this.ctx.getApi().imgUpload(file)
|
|
103
|
+
} else {
|
|
104
|
+
// 使用自定义的图片上传器
|
|
105
|
+
resp = {img_url: await this.ctx.conf.imgUploader(file)}
|
|
106
|
+
}
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
console.error(err)
|
|
109
|
+
this.editor.showNotify(`${this.ctx.$t('uploadFail')},${err.msg}`, 'e')
|
|
110
|
+
}
|
|
111
|
+
if (!!resp && resp.img_url) {
|
|
112
|
+
let imgURL = resp.img_url as string
|
|
113
|
+
|
|
114
|
+
// 若为相对路径,加上 artalk server
|
|
115
|
+
if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
|
|
116
|
+
|
|
117
|
+
// 上传成功插入图片
|
|
118
|
+
this.editor.setContent(this.editor.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}`))
|
|
119
|
+
} else {
|
|
120
|
+
// 上传失败删除加载文字
|
|
121
|
+
this.editor.setContent(this.editor.$textarea.value.replace(uploadPlaceholderTxt, ''))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/i18n/en.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
/* Editor */
|
|
3
|
+
placeholder: 'Leave a comment',
|
|
4
|
+
noComment: 'No Comment',
|
|
5
|
+
send: 'Send',
|
|
6
|
+
nick: 'Nickname',
|
|
7
|
+
email: 'Email',
|
|
8
|
+
link: 'Website',
|
|
9
|
+
emoticon: 'Emoji',
|
|
10
|
+
preview: 'Preview',
|
|
11
|
+
image: 'Image',
|
|
12
|
+
uploadFail: 'Upload Failed',
|
|
13
|
+
commentFail: 'Failed to comment',
|
|
14
|
+
restoredMsg: 'Content has been restored',
|
|
15
|
+
onlyAdminCanReply: 'Only admin can reply',
|
|
16
|
+
uploadLoginMsg: 'Please fill in your name and email to upload',
|
|
17
|
+
|
|
18
|
+
/* List */
|
|
19
|
+
counter: '{count} Comments',
|
|
20
|
+
sortLatest: 'Latest',
|
|
21
|
+
sortOldest: 'Oldest',
|
|
22
|
+
sortBest: 'Best',
|
|
23
|
+
sortAuthor: 'Author',
|
|
24
|
+
openComment: 'Open Comment',
|
|
25
|
+
closeComment: 'Close Comment',
|
|
26
|
+
listLoadFailMsg: 'Failed to load comments',
|
|
27
|
+
listRetry: 'Click to retry',
|
|
28
|
+
loadMore: 'Load More',
|
|
29
|
+
|
|
30
|
+
/* Comment */
|
|
31
|
+
admin: 'Admin',
|
|
32
|
+
reply: 'Reply',
|
|
33
|
+
voteUp: 'Up',
|
|
34
|
+
voteDown: 'Down',
|
|
35
|
+
voteFail: 'Vote Failed',
|
|
36
|
+
readMore: 'Read More',
|
|
37
|
+
actionConfirm: 'Confirm',
|
|
38
|
+
collapse: 'Collapse',
|
|
39
|
+
collapsed: 'Collapsed',
|
|
40
|
+
collapsedMsg: 'This comment has been collapsed',
|
|
41
|
+
expand: 'Expand',
|
|
42
|
+
approved: 'Approved',
|
|
43
|
+
pending: 'Pending',
|
|
44
|
+
pendingMsg: 'Pending, visible only to commenter.',
|
|
45
|
+
delete: 'Delete',
|
|
46
|
+
deleteConfirm: 'Confirm',
|
|
47
|
+
pin: 'Pin',
|
|
48
|
+
unpin: 'Unpin',
|
|
49
|
+
|
|
50
|
+
/* Time */
|
|
51
|
+
seconds: 'seconds ago',
|
|
52
|
+
minutes: 'minutes ago',
|
|
53
|
+
hours: 'hours ago',
|
|
54
|
+
days: 'days ago',
|
|
55
|
+
now: 'just now',
|
|
56
|
+
|
|
57
|
+
/* Checker */
|
|
58
|
+
adminCheck: 'Enter admin password:',
|
|
59
|
+
captchaCheck: 'Enter the CAPTCHA to continue:',
|
|
60
|
+
confirm: 'Confirm',
|
|
61
|
+
cancel: 'Cancel',
|
|
62
|
+
|
|
63
|
+
/* Sidebar */
|
|
64
|
+
msgCenter: 'Messages',
|
|
65
|
+
ctrlCenter: 'Admin',
|
|
66
|
+
|
|
67
|
+
/* General */
|
|
68
|
+
frontend: 'Frontend',
|
|
69
|
+
backend: 'Backend',
|
|
70
|
+
loading: 'Loading',
|
|
71
|
+
loadFail: 'Load Failed',
|
|
72
|
+
editing: 'Editing',
|
|
73
|
+
editFail: 'Edit Failed',
|
|
74
|
+
deleting: 'Deleting',
|
|
75
|
+
deleteFail: 'Delete Failed',
|
|
76
|
+
reqGot: 'Request got',
|
|
77
|
+
reqAborted: 'Request timed out or terminated unexpectedly',
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import en from './en'
|
|
2
|
+
import zhCN from './zh-CN'
|
|
3
|
+
|
|
4
|
+
export type I18n = typeof en
|
|
5
|
+
|
|
6
|
+
// @note the Key name is followed by `ISO 639`
|
|
7
|
+
// https://en.wikipedia.org/wiki/ISO_639
|
|
8
|
+
// https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
|
|
9
|
+
export const internal = {
|
|
10
|
+
'en': en,
|
|
11
|
+
'en-US': en,
|
|
12
|
+
'zh-CN': zhCN,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const external = {
|
|
16
|
+
'jp-JP': {},
|
|
17
|
+
'zh-TW': {},
|
|
18
|
+
'fr-FR': {},
|
|
19
|
+
'de-DE': {},
|
|
20
|
+
'bn-IN': {},
|
|
21
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { I18n } from '.'
|
|
2
|
+
|
|
3
|
+
const zhCN: I18n = {
|
|
4
|
+
/* Editor */
|
|
5
|
+
placeholder: '键入内容...',
|
|
6
|
+
noComment: '「此时无声胜有声」',
|
|
7
|
+
send: '发送评论',
|
|
8
|
+
nick: '昵称',
|
|
9
|
+
email: '邮件',
|
|
10
|
+
link: '网址',
|
|
11
|
+
emoticon: '表情',
|
|
12
|
+
preview: '预览',
|
|
13
|
+
image: '图片',
|
|
14
|
+
uploadFail: '上传失败',
|
|
15
|
+
commentFail: '评论失败',
|
|
16
|
+
restoredMsg: '内容已自动恢复',
|
|
17
|
+
onlyAdminCanReply: '仅管理员可评论',
|
|
18
|
+
uploadLoginMsg: '填入你的名字邮箱才能上传哦',
|
|
19
|
+
|
|
20
|
+
/* List */
|
|
21
|
+
counter: '{count} 条评论',
|
|
22
|
+
sortLatest: '最新',
|
|
23
|
+
sortOldest: '最早',
|
|
24
|
+
sortBest: '最热',
|
|
25
|
+
sortAuthor: '作者',
|
|
26
|
+
openComment: '打开评论',
|
|
27
|
+
closeComment: '关闭评论',
|
|
28
|
+
listLoadFailMsg: '无法获取评论列表数据',
|
|
29
|
+
listRetry: '点击重新获取',
|
|
30
|
+
loadMore: '加载更多',
|
|
31
|
+
|
|
32
|
+
/* Comment */
|
|
33
|
+
admin: '管理员',
|
|
34
|
+
reply: '回复',
|
|
35
|
+
voteUp: '赞同',
|
|
36
|
+
voteDown: '反对',
|
|
37
|
+
voteFail: '投票失败',
|
|
38
|
+
readMore: '阅读更多',
|
|
39
|
+
actionConfirm: '确认操作',
|
|
40
|
+
collapse: '折叠',
|
|
41
|
+
collapsed: '已折叠',
|
|
42
|
+
collapsedMsg: '该评论已被系统或管理员折叠',
|
|
43
|
+
expand: '取消折叠',
|
|
44
|
+
approved: '已审',
|
|
45
|
+
pending: '待审',
|
|
46
|
+
pendingMsg: '审核中,仅本人可见。',
|
|
47
|
+
delete: '删除',
|
|
48
|
+
deleteConfirm: '确认删除',
|
|
49
|
+
pin: '置顶',
|
|
50
|
+
unpin: '取消置顶',
|
|
51
|
+
|
|
52
|
+
/* Time */
|
|
53
|
+
seconds: '秒前',
|
|
54
|
+
minutes: '分钟前',
|
|
55
|
+
hours: '小时前',
|
|
56
|
+
days: '天前',
|
|
57
|
+
now: '刚刚',
|
|
58
|
+
|
|
59
|
+
/* Checker */
|
|
60
|
+
adminCheck: '键入密码来验证管理员身份:',
|
|
61
|
+
captchaCheck: '键入验证码继续:',
|
|
62
|
+
confirm: '确认',
|
|
63
|
+
cancel: '取消',
|
|
64
|
+
|
|
65
|
+
/* Sidebar */
|
|
66
|
+
msgCenter: '通知中心',
|
|
67
|
+
ctrlCenter: '控制中心',
|
|
68
|
+
|
|
69
|
+
/* General */
|
|
70
|
+
frontend: '前端',
|
|
71
|
+
backend: '后端',
|
|
72
|
+
loading: '加载中',
|
|
73
|
+
loadFail: '加载失败',
|
|
74
|
+
editing: '修改中',
|
|
75
|
+
editFail: '修改失败',
|
|
76
|
+
deleting: '删除中',
|
|
77
|
+
deleteFail: '删除失败',
|
|
78
|
+
reqGot: '请求响应',
|
|
79
|
+
reqAborted: '请求超时或意外终止',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default zhCN
|