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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import '@/style/editor.less'
|
|
2
|
+
|
|
3
|
+
import { CommentData } from '~/types/artalk-data'
|
|
4
|
+
import Context from '~/types/context'
|
|
5
|
+
import Component from '../lib/component'
|
|
6
|
+
import * as Utils from '../lib/utils'
|
|
7
|
+
import * as Ui from '../lib/ui'
|
|
8
|
+
|
|
9
|
+
import EditorHTML from './editor.html?raw'
|
|
10
|
+
|
|
11
|
+
import EmoticonsPlug from './plugs/emoticons-plug'
|
|
12
|
+
import UploadPlug from './plugs/upload-plug'
|
|
13
|
+
import PreviewPlug from './plugs/preview-plug'
|
|
14
|
+
import EditorPlug from './plugs/editor-plug'
|
|
15
|
+
import HeaderInputPlug from './plugs/header-input-plug'
|
|
16
|
+
|
|
17
|
+
export default class Editor extends Component {
|
|
18
|
+
private get user() { return this.ctx.user }
|
|
19
|
+
|
|
20
|
+
public $header: HTMLElement
|
|
21
|
+
public $nick: HTMLInputElement
|
|
22
|
+
public $email: HTMLInputElement
|
|
23
|
+
public $link: HTMLInputElement
|
|
24
|
+
public get $inputs() {
|
|
25
|
+
return { nick: this.$nick, email: this.$email, link: this.$link }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public $textareaWrap: HTMLElement
|
|
29
|
+
public $textarea: HTMLTextAreaElement
|
|
30
|
+
public $bottom: HTMLElement
|
|
31
|
+
public $submitBtn: HTMLButtonElement
|
|
32
|
+
public $notifyWrap: HTMLElement
|
|
33
|
+
|
|
34
|
+
private replyComment: CommentData|null = null
|
|
35
|
+
private $sendReply: HTMLElement|null = null
|
|
36
|
+
|
|
37
|
+
private isTraveling = false
|
|
38
|
+
|
|
39
|
+
/** 启用的插件 */
|
|
40
|
+
private readonly ENABLED_PLUGS = [ EmoticonsPlug, UploadPlug, PreviewPlug, HeaderInputPlug ]
|
|
41
|
+
public plugList: { [name: string]: EditorPlug } = {}
|
|
42
|
+
private openedPlugName: string|null = null
|
|
43
|
+
public $plugPanelWrap: HTMLElement
|
|
44
|
+
public $plugBtnWrap: HTMLElement
|
|
45
|
+
|
|
46
|
+
public constructor(ctx: Context) {
|
|
47
|
+
super(ctx)
|
|
48
|
+
|
|
49
|
+
this.$el = Utils.createElement(EditorHTML)
|
|
50
|
+
|
|
51
|
+
this.$header = this.$el.querySelector('.atk-header')!
|
|
52
|
+
this.$nick = this.$header.querySelector('[name="nick"]')!
|
|
53
|
+
this.$email = this.$header.querySelector('[name="email"]')!
|
|
54
|
+
this.$link = this.$header.querySelector('[name="link"]')!
|
|
55
|
+
|
|
56
|
+
this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
|
|
57
|
+
this.$textarea = this.$el.querySelector('.atk-textarea')!
|
|
58
|
+
this.$bottom = this.$el.querySelector('.atk-bottom')!
|
|
59
|
+
this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
|
|
60
|
+
this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
|
|
61
|
+
|
|
62
|
+
this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
|
|
63
|
+
this.$plugPanelWrap = this.$el.querySelector('.atk-plug-panel-wrap')!
|
|
64
|
+
|
|
65
|
+
// 执行初始化
|
|
66
|
+
this.initLocalStorage()
|
|
67
|
+
this.initHeader()
|
|
68
|
+
this.initTextarea()
|
|
69
|
+
this.initPlugs()
|
|
70
|
+
this.initSubmitBtn()
|
|
71
|
+
|
|
72
|
+
// 监听事件
|
|
73
|
+
this.ctx.on('conf-updated', () => {})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private initLocalStorage() {
|
|
77
|
+
const localContent = window.localStorage.getItem('ArtalkContent') || ''
|
|
78
|
+
if (localContent.trim() !== '') {
|
|
79
|
+
this.showNotify(this.$t('restoredMsg'), 'i')
|
|
80
|
+
this.setContent(localContent)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.$textarea.addEventListener('input', () => (this.saveToLocalStorage()))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private saveToLocalStorage() {
|
|
87
|
+
window.localStorage.setItem('ArtalkContent', this.getContentOriginal().trim())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private initHeader() {
|
|
91
|
+
Object.entries(this.$inputs).forEach(([key, $input]) => {
|
|
92
|
+
$input.value = this.user.data[key] || ''
|
|
93
|
+
$input.addEventListener('input', () => this.onHeaderInput(key, $input))
|
|
94
|
+
|
|
95
|
+
// 设置 Placeholder
|
|
96
|
+
$input.placeholder = `${this.$t(key as any)}`
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private onHeaderInput(key: string, $input: HTMLInputElement) {
|
|
101
|
+
this.user.update({
|
|
102
|
+
[key]: $input.value.trim()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// 插件监听事件响应
|
|
106
|
+
Object.entries(this.plugList).forEach(([plugName, plug]) => {
|
|
107
|
+
if (plug.onHeaderInput) plug.onHeaderInput(key, $input)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private initTextarea() {
|
|
112
|
+
// 占位符
|
|
113
|
+
this.$textarea.placeholder = this.ctx.conf.placeholder || this.$t('placeholder')
|
|
114
|
+
|
|
115
|
+
// 修复按下 Tab 输入的内容
|
|
116
|
+
this.$textarea.addEventListener('keydown', (e) => {
|
|
117
|
+
const keyCode = e.keyCode || e.which
|
|
118
|
+
|
|
119
|
+
if (keyCode === 9) {
|
|
120
|
+
e.preventDefault()
|
|
121
|
+
this.insertContent('\t')
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 输入框高度随内容而变化
|
|
126
|
+
this.$textarea.addEventListener('input', () => {
|
|
127
|
+
this.adjustTextareaHeight()
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private initSubmitBtn() {
|
|
132
|
+
this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
|
|
133
|
+
this.$submitBtn.addEventListener('click', () => (this.submit()))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 最终用于 submit 的数据 */
|
|
137
|
+
public getFinalContent() {
|
|
138
|
+
let content = this.getContentOriginal()
|
|
139
|
+
|
|
140
|
+
// 表情包处理
|
|
141
|
+
if (this.plugList.emoticons) {
|
|
142
|
+
content = (this.plugList.emoticons as EmoticonsPlug).transEmoticonImageText(content)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return content
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public getContentOriginal() {
|
|
149
|
+
return this.$textarea.value || '' // Tip: !!"0" === true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public getContentMarked() {
|
|
153
|
+
return Utils.marked(this.ctx, this.getFinalContent())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public setContent(val: string) {
|
|
157
|
+
this.$textarea.value = val
|
|
158
|
+
this.saveToLocalStorage()
|
|
159
|
+
if (this.plugList.preview) {
|
|
160
|
+
;(this.plugList.preview as PreviewPlug).updateContent()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 延迟执行防止无效
|
|
164
|
+
window.setTimeout(() => {
|
|
165
|
+
this.adjustTextareaHeight()
|
|
166
|
+
}, 80)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public insertContent(val: string) {
|
|
170
|
+
if ((document as any).selection) {
|
|
171
|
+
this.$textarea.focus();
|
|
172
|
+
(document as any).selection.createRange().text = val
|
|
173
|
+
this.$textarea.focus()
|
|
174
|
+
} else if (this.$textarea.selectionStart || this.$textarea.selectionStart === 0) {
|
|
175
|
+
const sStart = this.$textarea.selectionStart
|
|
176
|
+
const sEnd = this.$textarea.selectionEnd
|
|
177
|
+
const sT = this.$textarea.scrollTop
|
|
178
|
+
this.setContent(this.$textarea.value.substring(0, sStart) + val + this.$textarea.value.substring(sEnd, this.$textarea.value.length))
|
|
179
|
+
this.$textarea.focus()
|
|
180
|
+
this.$textarea.selectionStart = sStart + val.length
|
|
181
|
+
this.$textarea.selectionEnd = sStart + val.length
|
|
182
|
+
this.$textarea.scrollTop = sT
|
|
183
|
+
} else {
|
|
184
|
+
this.$textarea.focus()
|
|
185
|
+
this.$textarea.value += val
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
public clearEditor() {
|
|
190
|
+
this.setContent('')
|
|
191
|
+
this.cancelReply()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private adjustTextareaHeight() {
|
|
195
|
+
const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
|
|
196
|
+
this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
|
|
197
|
+
this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public setReply(commentData: CommentData, $comment: HTMLElement, scroll = true) {
|
|
201
|
+
if (this.replyComment !== null) {
|
|
202
|
+
this.cancelReply()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.$sendReply === null) {
|
|
206
|
+
this.$sendReply = Utils.createElement(`<div class="atk-send-reply">${this.$t('reply')} <span class="atk-text"></span><span class="atk-cancel">×</span></div>`);
|
|
207
|
+
this.$sendReply.querySelector<HTMLElement>('.atk-text')!.innerText = `@${commentData.nick}`
|
|
208
|
+
this.$sendReply.addEventListener('click', () => {
|
|
209
|
+
this.cancelReply()
|
|
210
|
+
})
|
|
211
|
+
this.$textareaWrap.append(this.$sendReply)
|
|
212
|
+
}
|
|
213
|
+
this.replyComment = commentData
|
|
214
|
+
|
|
215
|
+
if (this.ctx.conf.editorTravel === true) {
|
|
216
|
+
this.travel($comment)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (scroll) Ui.scrollIntoView(this.$el)
|
|
220
|
+
|
|
221
|
+
this.$textarea.focus()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public cancelReply() {
|
|
225
|
+
if (this.$sendReply !== null) {
|
|
226
|
+
this.$sendReply.remove()
|
|
227
|
+
this.$sendReply = null
|
|
228
|
+
}
|
|
229
|
+
this.replyComment = null
|
|
230
|
+
|
|
231
|
+
if (this.ctx.conf.editorTravel === true) {
|
|
232
|
+
this.travelBack()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
public showNotify(msg: string, type: "i"|"s"|"w"|"e") {
|
|
237
|
+
Ui.showNotify(this.$notifyWrap, msg, type)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public showLoading() {
|
|
241
|
+
Ui.showLoading(this.$el)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public hideLoading() {
|
|
245
|
+
Ui.hideLoading(this.$el)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 提交评论 */
|
|
249
|
+
public async submit() {
|
|
250
|
+
if (this.getFinalContent().trim() === '') {
|
|
251
|
+
this.$textarea.focus()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.ctx.trigger('editor-submit')
|
|
256
|
+
|
|
257
|
+
this.showLoading()
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const nComment = await this.ctx.getApi().add({
|
|
261
|
+
content: this.getFinalContent(),
|
|
262
|
+
nick: this.user.data.nick,
|
|
263
|
+
email: this.user.data.email,
|
|
264
|
+
link: this.user.data.link,
|
|
265
|
+
rid: (this.replyComment === null) ? 0 : this.replyComment.id,
|
|
266
|
+
page_key: (this.replyComment === null) ? this.ctx.conf.pageKey : this.replyComment.page_key,
|
|
267
|
+
page_title: (this.replyComment === null) ? this.ctx.conf.pageTitle : undefined,
|
|
268
|
+
site_name: (this.replyComment === null) ? this.ctx.conf.site : this.replyComment.site_name
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
// 回复不同页面的评论
|
|
272
|
+
if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
|
|
273
|
+
window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.ctx.insertComment(nComment)
|
|
277
|
+
this.clearEditor() // 清空编辑器
|
|
278
|
+
this.ctx.trigger('editor-submitted')
|
|
279
|
+
} catch (err: any) {
|
|
280
|
+
console.error(err)
|
|
281
|
+
this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
|
|
282
|
+
return
|
|
283
|
+
} finally {
|
|
284
|
+
this.hideLoading()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** 关闭评论 */
|
|
289
|
+
public close() {
|
|
290
|
+
if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
|
|
291
|
+
this.$textareaWrap.prepend(Utils.createElement(`<div class="atk-comment-closed">${this.$t('onlyAdminCanReply')}</div>`))
|
|
292
|
+
|
|
293
|
+
if (!this.user.data.isAdmin) {
|
|
294
|
+
this.$textarea.style.display = 'none'
|
|
295
|
+
this.closePlugPanel()
|
|
296
|
+
this.$bottom.style.display = 'none'
|
|
297
|
+
} else {
|
|
298
|
+
// 管理员一直打开评论
|
|
299
|
+
this.$textarea.style.display = ''
|
|
300
|
+
this.$bottom.style.display = ''
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** 打开评论 */
|
|
305
|
+
public open() {
|
|
306
|
+
this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
|
|
307
|
+
this.$textarea.style.display = ''
|
|
308
|
+
this.$bottom.style.display = ''
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** 移动评论框到置顶元素之后 */
|
|
312
|
+
public travel($afterEl: HTMLElement) {
|
|
313
|
+
if (this.isTraveling) return
|
|
314
|
+
this.isTraveling = true
|
|
315
|
+
this.$el.after(Utils.createElement('<div class="atk-editor-travel-placeholder"></div>'))
|
|
316
|
+
|
|
317
|
+
const $travelPlace = Utils.createElement('<div></div>')
|
|
318
|
+
$afterEl.after($travelPlace)
|
|
319
|
+
$travelPlace.replaceWith(this.$el)
|
|
320
|
+
|
|
321
|
+
this.$el.classList.add('atk-fade-in') // 添加渐入动画
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 评论框归位 */
|
|
325
|
+
public travelBack() {
|
|
326
|
+
if (!this.isTraveling) return
|
|
327
|
+
this.isTraveling = false
|
|
328
|
+
this.ctx.$root.querySelector('.atk-editor-travel-placeholder')?.replaceWith(this.$el)
|
|
329
|
+
|
|
330
|
+
// 取消回复
|
|
331
|
+
if (this.replyComment !== null) this.cancelReply()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** 插件初始化 */
|
|
335
|
+
private initPlugs() {
|
|
336
|
+
this.plugList = {}
|
|
337
|
+
this.$plugPanelWrap.innerHTML = ''
|
|
338
|
+
this.$plugPanelWrap.style.display = 'none'
|
|
339
|
+
this.openedPlugName = null
|
|
340
|
+
this.$plugBtnWrap.innerHTML = ''
|
|
341
|
+
|
|
342
|
+
const disabledPlugs: string[] = []
|
|
343
|
+
if (!this.conf.emoticons) disabledPlugs.push('emoticons')
|
|
344
|
+
if (!this.conf.preview) disabledPlugs.push('preview')
|
|
345
|
+
|
|
346
|
+
// 初始化 Editor 插件
|
|
347
|
+
this.ENABLED_PLUGS.forEach((Plug) => {
|
|
348
|
+
const plugName = Plug.Name
|
|
349
|
+
|
|
350
|
+
// 禁用的插件
|
|
351
|
+
if (disabledPlugs.includes(plugName)) return
|
|
352
|
+
|
|
353
|
+
// 插件对象实例化
|
|
354
|
+
const plug = new Plug(this)
|
|
355
|
+
this.plugList[plugName] = plug
|
|
356
|
+
|
|
357
|
+
// 插件按钮
|
|
358
|
+
const $btn = plug.getBtn()
|
|
359
|
+
if ($btn) {
|
|
360
|
+
this.$plugBtnWrap.appendChild($btn)
|
|
361
|
+
$btn.onclick = $btn.onclick || (() => {
|
|
362
|
+
// 其他按钮去除 Active
|
|
363
|
+
this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
|
|
364
|
+
|
|
365
|
+
// 若点击已打开的,则关闭打开的面板
|
|
366
|
+
if (plugName === this.openedPlugName) {
|
|
367
|
+
this.closePlugPanel()
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.openPlugPanel(plugName)
|
|
372
|
+
|
|
373
|
+
// 当前按钮添加 Active
|
|
374
|
+
$btn.classList.add('active')
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// 插件面板初始化
|
|
378
|
+
const $panel = plug.getPanel()
|
|
379
|
+
if ($panel) {
|
|
380
|
+
$panel.setAttribute('data-plug-name', plugName)
|
|
381
|
+
$panel.style.display = 'none'
|
|
382
|
+
this.$plugPanelWrap.appendChild($panel)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 展开插件面板 */
|
|
389
|
+
public openPlugPanel(plugName: string) {
|
|
390
|
+
Object.entries(this.plugList).forEach(([aPlugName, plug]) => {
|
|
391
|
+
const plugPanel = plug.getPanel()
|
|
392
|
+
if (!plugPanel) return
|
|
393
|
+
|
|
394
|
+
if (aPlugName === plugName) {
|
|
395
|
+
plugPanel.style.display = ''
|
|
396
|
+
if (plug.onPanelShow) plug.onPanelShow()
|
|
397
|
+
} else {
|
|
398
|
+
plugPanel.style.display = 'none'
|
|
399
|
+
if (plug.onPanelHide) plug.onPanelHide()
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
this.$plugPanelWrap.style.display = ''
|
|
404
|
+
this.openedPlugName = plugName
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** 收起插件面板 */
|
|
408
|
+
public closePlugPanel() {
|
|
409
|
+
if (!this.openedPlugName) return
|
|
410
|
+
|
|
411
|
+
const plug = this.plugList[this.openedPlugName]
|
|
412
|
+
if (!plug) return
|
|
413
|
+
|
|
414
|
+
if (plug.onPanelHide) plug.onPanelHide()
|
|
415
|
+
|
|
416
|
+
this.$plugPanelWrap.style.display = 'none'
|
|
417
|
+
this.openedPlugName = null
|
|
418
|
+
}
|
|
419
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Utils from '@/lib/utils'
|
|
2
|
+
import Editor from '../editor'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Editor 插件
|
|
6
|
+
*
|
|
7
|
+
* @desc 使用 Interface x Abstract 合并声明
|
|
8
|
+
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
|
|
9
|
+
*/
|
|
10
|
+
interface EditorPlug {
|
|
11
|
+
onPanelShow?(): void
|
|
12
|
+
onPanelHide?(): void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
abstract class EditorPlug {
|
|
16
|
+
protected editor: Editor
|
|
17
|
+
protected get ctx() { return this.editor.ctx }
|
|
18
|
+
protected $panel?: HTMLElement
|
|
19
|
+
protected $btn?: HTMLElement
|
|
20
|
+
public onHeaderInput?: (key: string, $input: HTMLElement) => void
|
|
21
|
+
|
|
22
|
+
public constructor(editor: Editor) {
|
|
23
|
+
this.editor = editor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public static Name: string
|
|
27
|
+
|
|
28
|
+
protected registerPanel(html: string = '<div></div>') {
|
|
29
|
+
this.$panel = Utils.createElement(html)
|
|
30
|
+
return this.$panel
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected registerBtn(html: string) {
|
|
34
|
+
this.$btn = Utils.createElement(`<span class="atk-plug-btn" data-plug-name="${this.constructor.name}">${html}</span>`)
|
|
35
|
+
return this.$btn
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected registerHeaderInputEvt(action: (key: string, $input: HTMLElement) => void) {
|
|
39
|
+
this.onHeaderInput = action
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getPanel() {
|
|
43
|
+
return this.$panel
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public getBtn() {
|
|
47
|
+
return this.$btn
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default EditorPlug
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
padding: 5px;
|
|
28
28
|
cursor: pointer;
|
|
29
29
|
user-select: none;
|
|
30
|
-
border-radius:
|
|
30
|
+
border-radius: (@block-radius / 2);
|
|
31
31
|
font-size: 16px;
|
|
32
32
|
min-width: 35px;
|
|
33
33
|
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
background: var(--at-color-bg-transl);
|
|
52
52
|
height: 30px;
|
|
53
53
|
border-top: 1px solid var(--at-color-border);
|
|
54
|
+
border-bottom: 1px solid var(--at-color-border);
|
|
54
55
|
|
|
55
56
|
& > span {
|
|
56
57
|
user-select: none;
|