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.
- package/LICENSE +165 -0
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +337 -83
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/package.json +14 -13
- package/src/api/index.ts +35 -0
- package/src/api/request.ts +1 -0
- package/src/artalk.ts +18 -0
- package/src/components/editor.ts +120 -5
- package/src/components/layer.ts +19 -8
- package/src/components/list-lite.ts +19 -10
- package/src/components/list.ts +6 -3
- package/src/components/sidebar-layer.ts +24 -2
- package/src/context.ts +3 -0
- package/src/defaults.ts +2 -0
- package/src/lib/checker/admin-checker.ts +4 -4
- package/src/lib/checker/captcha-checker.ts +52 -6
- package/src/lib/checker/index.ts +65 -23
- package/src/lib/ui.ts +2 -1
- package/src/lib/utils.ts +56 -35
- package/src/style/comment.less +3 -0
- package/src/style/list.less +7 -4
- package/src/style/main.less +35 -0
- package/tsconfig.json +1 -16
- package/types/artalk-config.d.ts +6 -0
- package/types/artalk-data.d.ts +3 -0
- package/types/event.d.ts +2 -0
- package/types/vite-env.d.ts +0 -1
package/src/lib/checker/index.ts
CHANGED
|
@@ -14,8 +14,7 @@ export default class CheckerLauncher {
|
|
|
14
14
|
public ctx: Context
|
|
15
15
|
|
|
16
16
|
public launched: Checker[] = []
|
|
17
|
-
public
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
91
|
-
|
|
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.
|
|
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
|
-
|
|
151
|
+
// onMount 回调
|
|
152
|
+
if (payload.onMount) payload.onMount(dialog.$el)
|
|
119
153
|
}
|
|
120
154
|
|
|
121
|
-
|
|
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
|
-
|
|
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
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
.atk-arrow-down-icon {
|
|
20
20
|
cursor: pointer;
|
|
21
21
|
vertical-align: middle;
|
|
22
|
-
border-top:
|
|
23
|
-
border-left:
|
|
24
|
-
border-right:
|
|
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
|
-
|
|
130
|
+
.atk-list-comments-wrap {
|
|
131
|
+
.atk-main-editor {
|
|
132
|
+
margin-top: -1px;
|
|
133
|
+
}
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
|
package/src/style/main.less
CHANGED
|
@@ -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/*"]
|
package/types/artalk-config.d.ts
CHANGED
package/types/artalk-data.d.ts
CHANGED
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 {
|
package/types/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|