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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.1.8",
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
  }
@@ -247,7 +247,7 @@ export default class Editor extends Component {
247
247
  })
248
248
  })
249
249
 
250
- this.initImgUploadBtn()
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
- initImgUploadBtn() {
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
- if (evt.dataTransfer?.files) {
303
+ const files = evt.dataTransfer?.files
304
+ if (files?.length) {
292
305
  evt.preventDefault()
293
- for (let i = 0; i < evt.dataTransfer.files.length; i++) {
294
- const file = evt.dataTransfer.files[i]
295
- this.uploadImg(file)
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 = `\n![](Uploading ${file.name}...)`
345
+ const uploadPlaceholderTxt = `${insertPrefix}![](Uploading ${file.name}...)`
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
- if (imgURL.startsWith('.') || (imgURL.startsWith('/') && !imgURL.startsWith('//'))) {
330
- // 若为相对路径,加上 artalk server
331
- imgURL = `${this.ctx.conf.server.replace(/\/api\/?$/, '')}/${imgURL.replace(/^\//, '')}`
332
- }
358
+
359
+ // 若为相对路径,加上 artalk server
360
+ if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
333
361
 
334
362
  // 上传成功插入图片
335
- this.$textarea.value = this.$textarea.value.replace(uploadPlaceholderTxt, `\n![](${imgURL})`)
363
+ this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}![](${imgURL})`))
336
364
  } else {
337
365
  // 上传失败删除加载文字
338
- this.$textarea.value = this.$textarea.value.replace(uploadPlaceholderTxt, '')
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
- getContent () {
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.getContent())
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.getContent().trim() === '') {
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.getContent(),
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,
@@ -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
- this.initDropdown()
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
- : `${this.conf.server.replace(/\/$/, '')}/../sidebar/`
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,
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
- let markedInstance: typeof libMarked
144
- export function marked(ctx: Context, src: string): string {
145
- if (!markedInstance) {
146
- const renderer = new libMarked.Renderer()
147
- const linkRenderer = renderer.link
148
- renderer.link = (href, title, text) => {
149
- const localLink = href?.startsWith(`${window.location.protocol}//${window.location.hostname}`);
150
- const html = linkRenderer.call(renderer as any, href, title, text);
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
- return markedInstance.parse(src)
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
+ }
@@ -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);
@@ -127,7 +127,10 @@
127
127
  & > .atk-list-body {
128
128
  min-height: 150px;
129
129
 
130
- & > .atk-list-comments-wrap {
130
+ .atk-list-comments-wrap {
131
+ .atk-main-editor {
132
+ margin-top: -1px;
133
+ }
131
134
  }
132
135
  }
133
136
 
package/tsconfig.json CHANGED
@@ -21,7 +21,8 @@
21
21
  "paths": {
22
22
  "~/*": ["./*"],
23
23
  "@/*": ["./src/*"]
24
- }
24
+ },
25
+ "types": ["vite/client"]
25
26
  },
26
27
  "include": ["./src", "./types"]
27
28
  }
@@ -80,6 +80,9 @@ export default interface ArtalkConfig {
80
80
  /** 显示 UA 徽标 */
81
81
  uaBadge?: boolean
82
82
 
83
+ /** 评论列表排序功能 (显示 Dropdown) */
84
+ listSort?: boolean
85
+
83
86
  /** 图片上传功能 */
84
87
  imgUpload?: boolean
85
88
 
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />