artalk 2.1.8 → 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 +99 -56
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/package.json +12 -12
- package/src/artalk.ts +17 -0
- package/src/components/editor.ts +48 -22
- package/src/components/list.ts +5 -2
- package/src/components/sidebar-layer.ts +3 -2
- package/src/context.ts +3 -0
- package/src/defaults.ts +1 -0
- package/src/lib/utils.ts +56 -35
- package/src/style/comment.less +3 -0
- package/src/style/list.less +4 -1
- package/tsconfig.json +2 -1
- package/types/artalk-config.d.ts +3 -0
- package/types/vite-env.d.ts +0 -1
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/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
|
/** 事件绑定 · 初始化 */
|
|
@@ -180,4 +189,12 @@ export default class Artalk {
|
|
|
180
189
|
public trigger<K extends keyof EventPayloadMap>(name: K, payload?: EventPayloadMap[K]) {
|
|
181
190
|
this.ctx.trigger(name, payload, 'external')
|
|
182
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
|
+
}
|
|
183
200
|
}
|
package/src/components/editor.ts
CHANGED
|
@@ -247,7 +247,7 @@ export default class Editor extends Component {
|
|
|
247
247
|
})
|
|
248
248
|
})
|
|
249
249
|
|
|
250
|
-
this.
|
|
250
|
+
this.initImgUpload()
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
/** 关闭编辑器插件 */
|
|
@@ -260,11 +260,12 @@ export default class Editor extends Component {
|
|
|
260
260
|
/** 允许的图片格式 */
|
|
261
261
|
allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
|
|
262
262
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
263
|
+
/** 初始化图片上传功能 */
|
|
264
|
+
initImgUpload() {
|
|
265
265
|
this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
|
|
266
266
|
this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
|
|
267
267
|
|
|
268
|
+
// 按钮点击
|
|
268
269
|
this.$imgUploadBtn.onclick = () => {
|
|
269
270
|
// 选择图片
|
|
270
271
|
const $input = document.createElement('input')
|
|
@@ -280,6 +281,17 @@ export default class Editor extends Component {
|
|
|
280
281
|
$input.click() // 显示选择图片对话框
|
|
281
282
|
}
|
|
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
|
+
// 拖拽图片
|
|
283
295
|
// @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
|
|
284
296
|
// 阻止浏览器的默认释放行为
|
|
285
297
|
this.$textarea.addEventListener('dragover', (evt) => {
|
|
@@ -288,12 +300,19 @@ export default class Editor extends Component {
|
|
|
288
300
|
})
|
|
289
301
|
|
|
290
302
|
this.$textarea.addEventListener('drop', (evt) => {
|
|
291
|
-
|
|
303
|
+
const files = evt.dataTransfer?.files
|
|
304
|
+
if (files?.length) {
|
|
292
305
|
evt.preventDefault()
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)
|
|
297
316
|
}
|
|
298
317
|
})
|
|
299
318
|
}
|
|
@@ -312,8 +331,18 @@ export default class Editor extends Component {
|
|
|
312
331
|
const fileExt = /[^.]+$/.exec(file.name)
|
|
313
332
|
if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
|
|
314
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
|
+
|
|
315
344
|
// 插入占位加载文字
|
|
316
|
-
const uploadPlaceholderTxt =
|
|
345
|
+
const uploadPlaceholderTxt = `${insertPrefix}`
|
|
317
346
|
this.insertContent(uploadPlaceholderTxt)
|
|
318
347
|
|
|
319
348
|
// 上传图片
|
|
@@ -326,20 +355,16 @@ export default class Editor extends Component {
|
|
|
326
355
|
}
|
|
327
356
|
if (!!resp && resp.img_url) {
|
|
328
357
|
let imgURL = resp.img_url as string
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
358
|
+
|
|
359
|
+
// 若为相对路径,加上 artalk server
|
|
360
|
+
if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
|
|
333
361
|
|
|
334
362
|
// 上传成功插入图片
|
|
335
|
-
this
|
|
363
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}`))
|
|
336
364
|
} else {
|
|
337
365
|
// 上传失败删除加载文字
|
|
338
|
-
this
|
|
366
|
+
this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, ''))
|
|
339
367
|
}
|
|
340
|
-
|
|
341
|
-
// 更新内容保存到 localStorage
|
|
342
|
-
this.saveContent()
|
|
343
368
|
}
|
|
344
369
|
|
|
345
370
|
insertContent (val: string) {
|
|
@@ -376,7 +401,8 @@ export default class Editor extends Component {
|
|
|
376
401
|
this.cancelReply()
|
|
377
402
|
}
|
|
378
403
|
|
|
379
|
-
|
|
404
|
+
/** 获取最终用于 submit 的数据 */
|
|
405
|
+
getFinalContent () {
|
|
380
406
|
let content = this.getContentOriginal()
|
|
381
407
|
|
|
382
408
|
// 表情包处理
|
|
@@ -393,7 +419,7 @@ export default class Editor extends Component {
|
|
|
393
419
|
}
|
|
394
420
|
|
|
395
421
|
getContentMarked () {
|
|
396
|
-
return Utils.marked(this.ctx, this.
|
|
422
|
+
return Utils.marked(this.ctx, this.getFinalContent())
|
|
397
423
|
}
|
|
398
424
|
|
|
399
425
|
initBottomPart () {
|
|
@@ -452,7 +478,7 @@ export default class Editor extends Component {
|
|
|
452
478
|
}
|
|
453
479
|
|
|
454
480
|
async submit () {
|
|
455
|
-
if (this.
|
|
481
|
+
if (this.getFinalContent().trim() === '') {
|
|
456
482
|
this.$textarea.focus()
|
|
457
483
|
return
|
|
458
484
|
}
|
|
@@ -463,7 +489,7 @@ export default class Editor extends Component {
|
|
|
463
489
|
|
|
464
490
|
try {
|
|
465
491
|
const nComment = await new Api(this.ctx).add({
|
|
466
|
-
content: this.
|
|
492
|
+
content: this.getFinalContent(),
|
|
467
493
|
nick: this.user.data.nick,
|
|
468
494
|
email: this.user.data.email,
|
|
469
495
|
link: this.user.data.link,
|
package/src/components/list.ts
CHANGED
|
@@ -42,9 +42,12 @@ export default class List extends ListLite {
|
|
|
42
42
|
// 操作按钮
|
|
43
43
|
this.initListActionBtn()
|
|
44
44
|
|
|
45
|
-
// 下拉选择层
|
|
46
45
|
this.$commentCount = this.$el.querySelector('.atk-comment-count')!
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
// 评论列表排序 Dropdown 下拉选择层
|
|
48
|
+
if (this.ctx.conf.listSort) {
|
|
49
|
+
this.initDropdown()
|
|
50
|
+
}
|
|
48
51
|
|
|
49
52
|
// copyright
|
|
50
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>`
|
|
@@ -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
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
|
+
}
|
package/src/style/comment.less
CHANGED
|
@@ -212,6 +212,8 @@
|
|
|
212
212
|
display: flex;
|
|
213
213
|
flex-direction: row;
|
|
214
214
|
align-items: center;
|
|
215
|
+
flex-wrap: wrap;
|
|
216
|
+
margin-bottom: -15px;
|
|
215
217
|
|
|
216
218
|
& > span {
|
|
217
219
|
color: var(--at-color-meta);
|
|
@@ -220,6 +222,7 @@
|
|
|
220
222
|
display: inline-flex;
|
|
221
223
|
cursor: pointer;
|
|
222
224
|
user-select: none;
|
|
225
|
+
margin-bottom: 15px;
|
|
223
226
|
|
|
224
227
|
&.atk-error, &.atk-error:hover {
|
|
225
228
|
color: var(--at-color-red);
|
package/src/style/list.less
CHANGED
package/tsconfig.json
CHANGED
package/types/artalk-config.d.ts
CHANGED
package/types/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|