artalk 2.1.7 → 2.2.0

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.
@@ -14,8 +14,7 @@ export default class CheckerLauncher {
14
14
  public ctx: Context
15
15
 
16
16
  public launched: Checker[] = []
17
- public submitCaptchaVal?: string
18
- public submitCaptchaImgData?: string
17
+ public captchaConf: { val?: string, imgData?: string, iframe?: string } = {}
19
18
 
20
19
  constructor(ctx: Context) {
21
20
  this.ctx = ctx
@@ -26,9 +25,9 @@ export default class CheckerLauncher {
26
25
  /** 初始化事件绑定 */
27
26
  public initEventBind() {
28
27
  this.ctx.on('checker-captcha', (conf) => {
29
- if (conf.imgData) {
30
- this.submitCaptchaImgData = conf.imgData
31
- }
28
+ if (conf.imgData) this.captchaConf.imgData = conf.imgData
29
+ if (conf.iframe) this.captchaConf.iframe = conf.iframe
30
+
32
31
  this.fire(CaptchaChecker, conf)
33
32
  })
34
33
 
@@ -41,20 +40,42 @@ export default class CheckerLauncher {
41
40
  if (this.launched.includes(checker)) return // 阻止同时 fire 相同的 checker
42
41
  this.launched.push(checker)
43
42
 
43
+ // 创建层
44
44
  const layer = new Layer(this.ctx, `checker-${new Date().getTime()}`)
45
45
  layer.setMaskClickHide(false)
46
46
  layer.show()
47
47
 
48
+ // Checker 的上下文
49
+ let hideInteractInput = false
50
+ const checkerCtx: CheckerCtx = {
51
+ getLayer: () => layer,
52
+ hideInteractInput: () => {
53
+ hideInteractInput = true
54
+ },
55
+ triggerSuccess: () => {
56
+ this.close(checker, layer)
57
+ if (checker.onSuccess) checker.onSuccess(this, checkerCtx, "", "", formEl)
58
+ if (payload.onSuccess) payload.onSuccess("", dialog.$el)
59
+ },
60
+ cancel: () => {
61
+ this.close(checker, layer)
62
+ if (payload.onCancel) payload.onCancel()
63
+ }
64
+ }
65
+
66
+ // 创建表单
48
67
  const formEl = Utils.createElement()
49
- formEl.appendChild(checker.body(this))
68
+ formEl.appendChild(checker.body(this, checkerCtx))
50
69
 
51
- const input = Utils.createElement<HTMLInputElement>(
70
+ // 输入框
71
+ const $input = Utils.createElement<HTMLInputElement>(
52
72
  `<input id="check" type="${checker.inputType || 'text'}" autocomplete="off" required placeholder="">`
53
73
  )
54
- formEl.appendChild(input)
55
- setTimeout(() => input.focus(), 80) // 延迟 Focus
74
+ formEl.appendChild($input)
75
+ setTimeout(() => $input.focus(), 80) // 延迟 Focus
56
76
 
57
- input.onkeyup = (evt) => {
77
+ // 绑定键盘事件
78
+ $input.onkeyup = (evt) => {
58
79
  if (evt.key === 'Enter' || evt.keyCode === 13) {
59
80
  // 按下回车键
60
81
  evt.preventDefault()
@@ -70,7 +91,9 @@ export default class CheckerLauncher {
70
91
 
71
92
  // 确认按钮
72
93
  dialog.setYes((btnEl) => {
73
- const inputVal = input.value.trim()
94
+ const inputVal = $input.value.trim()
95
+
96
+ // 按钮操作
74
97
  if (!btnTextOrg) btnTextOrg = btnEl.innerText
75
98
  const btnTextSet = (btnText: string) => {
76
99
  btnEl.innerText = btnText
@@ -83,21 +106,25 @@ export default class CheckerLauncher {
83
106
 
84
107
  btnEl.innerText = '加载中...'
85
108
 
109
+ // 发送请求
86
110
  checker
87
- .request(this, inputVal)
111
+ .request(this, checkerCtx, inputVal)
88
112
  .then((data) => {
89
113
  // 请求成功
90
- this.done(checker, layer)
91
- if (checker.onSuccess) checker.onSuccess(this, data, inputVal, formEl)
114
+ this.close(checker, layer)
115
+
116
+ if (checker.onSuccess) checker.onSuccess(this, checkerCtx, data, inputVal, formEl)
92
117
  if (payload.onSuccess) payload.onSuccess(inputVal, dialog.$el)
93
118
  })
94
119
  .catch((err) => {
95
120
  // 请求失败
96
121
  btnTextSet(String(err.msg || String(err)))
97
- if (checker.onError) checker.onError(this, err, inputVal, formEl)
98
122
 
123
+ if (checker.onError) checker.onError(this, checkerCtx, err, inputVal, formEl)
124
+
125
+ // 错误显示 3s 后恢复按钮
99
126
  const tf = setTimeout(() => btnTextRestore(), 3000)
100
- input.onfocus = () => {
127
+ $input.onfocus = () => {
101
128
  btnTextRestore()
102
129
  clearTimeout(tf)
103
130
  }
@@ -108,17 +135,25 @@ export default class CheckerLauncher {
108
135
 
109
136
  // 取消按钮
110
137
  dialog.setNo(() => {
111
- this.done(checker, layer)
138
+ this.close(checker, layer)
112
139
  if (payload.onCancel) payload.onCancel()
113
140
  return false
114
141
  })
115
142
 
143
+ if (hideInteractInput) {
144
+ $input.style.display = 'none'
145
+ dialog.$el.querySelector<HTMLElement>('.atk-layer-dialog-actions')!.style.display = 'none'
146
+ }
147
+
148
+ // 层装载 dialog 元素
116
149
  layer.getEl().append(dialog.$el)
117
150
 
118
- if (payload.onMount) payload.onMount(dialog.$el) // onMount
151
+ // onMount 回调
152
+ if (payload.onMount) payload.onMount(dialog.$el)
119
153
  }
120
154
 
121
- private done(checker: Checker, layer: Layer) {
155
+ // 关闭 checker 对话框
156
+ private close(checker: Checker, layer: Layer) {
122
157
  layer.disposeNow()
123
158
  this.launched = this.launched.filter(c => c !== checker)
124
159
  }
@@ -127,8 +162,15 @@ export default class CheckerLauncher {
127
162
  export interface Checker {
128
163
  el?: HTMLElement
129
164
  inputType?: 'password' | 'text'
130
- body: (launcher: CheckerLauncher) => HTMLElement
131
- request: (launcher: CheckerLauncher, inputVal: string) => Promise<string>
132
- onSuccess?: (launcher: CheckerLauncher, data: string, inputVal: string, formEl: HTMLElement) => void
133
- onError?: (launcher: CheckerLauncher, err: any, inputVal: string, formEl: HTMLElement) => void
165
+ body: (launcher: CheckerLauncher, ctx: CheckerCtx) => HTMLElement
166
+ request: (launcher: CheckerLauncher, ctx: CheckerCtx, inputVal: string) => Promise<string>
167
+ onSuccess?: (launcher: CheckerLauncher, ctx: CheckerCtx, data: string, inputVal: string, formEl: HTMLElement) => void
168
+ onError?: (launcher: CheckerLauncher, ctx: CheckerCtx, err: any, inputVal: string, formEl: HTMLElement) => void
169
+ }
170
+
171
+ export interface CheckerCtx {
172
+ getLayer(): Layer
173
+ hideInteractInput(): void
174
+ triggerSuccess(): void
175
+ cancel(): void
134
176
  }
package/src/lib/ui.ts CHANGED
@@ -2,7 +2,7 @@ import * as Utils from './utils'
2
2
  import Context from '../context'
3
3
 
4
4
  /** 显示加载 */
5
- export function showLoading(parentElem: HTMLElement|Context) {
5
+ export function showLoading(parentElem: HTMLElement|Context, conf?: { transparentBg?: boolean }) {
6
6
  if (parentElem instanceof Context) parentElem = parentElem.$root
7
7
 
8
8
  let $loading = parentElem.querySelector<HTMLElement>('.atk-loading')
@@ -13,6 +13,7 @@ export function showLoading(parentElem: HTMLElement|Context) {
13
13
  <svg viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle></svg>
14
14
  </div>
15
15
  </div>`)
16
+ if (conf?.transparentBg) $loading.style.background = 'transparent'
16
17
  parentElem.appendChild($loading)
17
18
  }
18
19
  $loading.style.display = ''
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);
@@ -19,9 +19,9 @@
19
19
  .atk-arrow-down-icon {
20
20
  cursor: pointer;
21
21
  vertical-align: middle;
22
- border-top: 6px solid var(--at-color-grey);
23
- border-left: 4px solid transparent;
24
- border-right: 4px solid transparent;
22
+ border-top: 5px solid var(--at-color-grey);
23
+ border-left: 3px solid transparent;
24
+ border-right: 3px solid transparent;
25
25
  margin-top: -1px;
26
26
  margin-left: 0.4rem;
27
27
  display: inline-block;
@@ -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
 
@@ -303,6 +303,41 @@ i.atk-icon {
303
303
  }
304
304
  }
305
305
  }
306
+
307
+ .atk-checker-iframe-wrap {
308
+ position: fixed;
309
+ z-index: 999998;
310
+ left: 0;
311
+ top: 0;
312
+ height: 100vh;
313
+ width: 100vw;
314
+
315
+ & > iframe {
316
+ width: 100%;
317
+ height: 100%;
318
+ border: 0;
319
+ }
320
+
321
+ .atk-close-btn {
322
+ z-index: 999999;
323
+ position: fixed;
324
+ top: 20px;
325
+ right: 20px;
326
+ display: flex;
327
+ flex-direction: column;
328
+ width: 50px;
329
+ height: 50px;
330
+ align-items: center;
331
+ place-content: center;
332
+ cursor: pointer;
333
+ user-select: none;
334
+ margin-left: 10px;
335
+
336
+ &:hover .atk-icon-close {
337
+ background-color: rgba(232, 17, 35, 0.9);
338
+ }
339
+ }
340
+ }
306
341
  }
307
342
  }
308
343
 
package/tsconfig.json CHANGED
@@ -1,23 +1,8 @@
1
1
  {
2
+ "extends": "../../tsconfig.base.json",
2
3
  "compilerOptions": {
3
4
  "baseUrl": ".",
4
5
  "outDir": "dist",
5
- "target": "esnext",
6
- "useDefineForClassFields": true,
7
- "module": "esnext",
8
- "lib": ["esnext", "dom"],
9
- "moduleResolution": "node",
10
- "strict": true,
11
- "sourceMap": true,
12
- "experimentalDecorators": true,
13
- "resolveJsonModule": true,
14
- "esModuleInterop": true,
15
- "noEmit": true,
16
- "noImplicitAny": false,
17
- "allowSyntheticDefaultImports": true,
18
- "noUnusedLocals": true,
19
- "noUnusedParameters": true,
20
- "noImplicitReturns": true,
21
6
  "paths": {
22
7
  "~/*": ["./*"],
23
8
  "@/*": ["./src/*"]
@@ -80,6 +80,12 @@ export default interface ArtalkConfig {
80
80
  /** 显示 UA 徽标 */
81
81
  uaBadge?: boolean
82
82
 
83
+ /** 评论列表排序功能 (显示 Dropdown) */
84
+ listSort?: boolean
85
+
86
+ /** 图片上传功能 */
87
+ imgUpload?: boolean
88
+
83
89
  /** 版本检测 */
84
90
  versionCheck?: boolean
85
91
  }
@@ -87,6 +87,9 @@ export interface ListData {
87
87
 
88
88
  /** API 版本 */
89
89
  api_version: ApiVersionData
90
+
91
+ /** 后端配置 */
92
+ conf: any // TODO
90
93
  }
91
94
 
92
95
  export interface PageData {
package/types/event.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface EventPayloadMap {
8
8
  'editor-submit': undefined // 编辑器提交时
9
9
  'editor-submitted': undefined // 编辑器提交后
10
10
  'user-changed': LocalUser // 本地用户数据变更时
11
+ 'conf-updated': undefined // Artalk 配置变更时
11
12
 
12
13
  // List 操作(外部:不建议 listen,仅 trigger)
13
14
  'list-reload': undefined // 重新加载 List
@@ -54,6 +55,7 @@ export interface NotifyConf {
54
55
 
55
56
  export interface CheckerCaptchaPayload extends CheckerPayload {
56
57
  imgData?: string
58
+ iframe?: string
57
59
  }
58
60
 
59
61
  export interface UnreadUpdatePayload {
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />