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
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
import '../style/comment.less'
|
|
2
|
-
|
|
3
|
-
import { CommentData } from '~/types/artalk-data'
|
|
4
|
-
import Context from '../context'
|
|
5
|
-
import Component from '../lib/component'
|
|
6
|
-
import * as Utils from '../lib/utils'
|
|
7
|
-
import * as Ui from '../lib/ui'
|
|
8
|
-
import UADetect from '../lib/detect'
|
|
9
|
-
import CommentHTML from './html/comment.html?raw'
|
|
10
|
-
import Api from '../api'
|
|
11
|
-
import ActionBtn from './action-btn'
|
|
12
|
-
|
|
13
|
-
export default class Comment extends Component {
|
|
14
|
-
public data: CommentData
|
|
15
|
-
|
|
16
|
-
public $main!: HTMLElement
|
|
17
|
-
public $header!: HTMLElement
|
|
18
|
-
public $headerNick!: HTMLElement
|
|
19
|
-
public $headerBadge?: HTMLElement
|
|
20
|
-
public $body!: HTMLElement
|
|
21
|
-
public $content!: HTMLElement
|
|
22
|
-
public $children!: HTMLElement|null
|
|
23
|
-
public $actions!: HTMLElement
|
|
24
|
-
public voteBtnUp?: ActionBtn
|
|
25
|
-
public voteBtnDown?: ActionBtn
|
|
26
|
-
|
|
27
|
-
public parent: Comment|null
|
|
28
|
-
public nestCurt: number
|
|
29
|
-
private nestMax: number // 最大嵌套层数
|
|
30
|
-
private children: Comment[] = []
|
|
31
|
-
|
|
32
|
-
public flatMode: boolean = false
|
|
33
|
-
public replyTo?: CommentData // 回复对象
|
|
34
|
-
|
|
35
|
-
public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
|
|
36
|
-
public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
|
|
37
|
-
|
|
38
|
-
public afterRender?: () => void
|
|
39
|
-
|
|
40
|
-
private unread = false
|
|
41
|
-
private openable = false
|
|
42
|
-
private openURL?: string
|
|
43
|
-
public openEvt?: () => void
|
|
44
|
-
|
|
45
|
-
public onReplyBtnClick?: Function
|
|
46
|
-
|
|
47
|
-
constructor(ctx: Context, data: CommentData) {
|
|
48
|
-
super(ctx)
|
|
49
|
-
|
|
50
|
-
// 最大嵌套数
|
|
51
|
-
this.nestMax = ctx.conf.nestMax || 3
|
|
52
|
-
|
|
53
|
-
this.data = { ...data }
|
|
54
|
-
this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
|
|
55
|
-
|
|
56
|
-
this.parent = null
|
|
57
|
-
this.nestCurt = 1 // 现在已嵌套 n 层
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 渲染 UI */
|
|
61
|
-
public render() {
|
|
62
|
-
this.$el = Utils.createElement(CommentHTML)
|
|
63
|
-
this.$main = this.$el.querySelector('.atk-main')!
|
|
64
|
-
this.$header = this.$el.querySelector('.atk-header')!
|
|
65
|
-
this.$body = this.$el.querySelector('.atk-body')!
|
|
66
|
-
this.$content = this.$body.querySelector('.atk-content')!
|
|
67
|
-
this.$actions = this.$el.querySelector('.atk-actions')!
|
|
68
|
-
this.$children = null
|
|
69
|
-
|
|
70
|
-
this.$el.setAttribute('data-comment-id', `${this.data.id}`)
|
|
71
|
-
|
|
72
|
-
this.renderCheckUnread()
|
|
73
|
-
this.renderCheckClickable()
|
|
74
|
-
|
|
75
|
-
this.renderAvatar()
|
|
76
|
-
this.renderHeader()
|
|
77
|
-
this.renderContent()
|
|
78
|
-
this.renderReplyAt()
|
|
79
|
-
this.renderReplyTo()
|
|
80
|
-
this.renderPending()
|
|
81
|
-
this.renderActionBtn()
|
|
82
|
-
|
|
83
|
-
if (this.afterRender) this.afterRender()
|
|
84
|
-
|
|
85
|
-
return this.$el
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
//#region Renders
|
|
89
|
-
private renderCheckUnread() {
|
|
90
|
-
if (this.unread) this.$el.classList.add('atk-unread')
|
|
91
|
-
else this.$el.classList.remove('atk-unread')
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
private renderCheckClickable() {
|
|
95
|
-
if (this.openable) {
|
|
96
|
-
this.$el.classList.add('atk-openable')
|
|
97
|
-
} else {
|
|
98
|
-
this.$el.classList.remove('atk-openable')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.$el.addEventListener('click', (evt) => {
|
|
102
|
-
if (this.openable && this.openURL) {
|
|
103
|
-
evt.preventDefault()
|
|
104
|
-
window.open(this.openURL)
|
|
105
|
-
}
|
|
106
|
-
if (this.openEvt)
|
|
107
|
-
this.openEvt()
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private renderAvatar() {
|
|
112
|
-
const $avatar = this.$el.querySelector<HTMLElement>('.atk-avatar')!
|
|
113
|
-
const $avatarImg = Utils.createElement<HTMLImageElement>('<img />')
|
|
114
|
-
$avatarImg.src = this.getGravatarUrl()
|
|
115
|
-
if (this.data.link) {
|
|
116
|
-
const $avatarA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
117
|
-
$avatarA.href = this.data.link
|
|
118
|
-
$avatarA.append($avatarImg)
|
|
119
|
-
$avatar.append($avatarA)
|
|
120
|
-
} else {
|
|
121
|
-
$avatar.append($avatarImg)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private renderHeader() {
|
|
126
|
-
this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
|
|
127
|
-
if (this.data.link) {
|
|
128
|
-
const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
129
|
-
$nickA.innerText = this.data.nick
|
|
130
|
-
$nickA.href = this.data.link
|
|
131
|
-
this.$headerNick.append($nickA)
|
|
132
|
-
} else {
|
|
133
|
-
this.$headerNick.innerText = this.data.nick
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this.$headerBadge = this.$el.querySelector<HTMLElement>('.atk-badge')!
|
|
137
|
-
if (this.data.badge_name) {
|
|
138
|
-
this.$headerBadge.innerText = this.data.badge_name
|
|
139
|
-
if (this.data.badge_color)
|
|
140
|
-
this.$headerBadge.style.backgroundColor = this.data.badge_color
|
|
141
|
-
} else {
|
|
142
|
-
this.$headerBadge.remove()
|
|
143
|
-
this.$headerBadge = undefined
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (this.data.is_pinned) {
|
|
147
|
-
const $pinnedBadge = Utils.createElement(`<span class="atk-item atk-pinned-badge">置顶</span>`) // 置顶徽章
|
|
148
|
-
this.$headerNick.insertAdjacentElement('afterend', $pinnedBadge)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
152
|
-
$date.innerText = this.getDateFormatted()
|
|
153
|
-
$date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
|
|
154
|
-
|
|
155
|
-
if (this.conf.uaBadge) {
|
|
156
|
-
const $uaWrap = Utils.createElement(`<span class="atk-ua-wrap"></span>`)
|
|
157
|
-
const $uaBrowser = Utils.createElement(`<span class="atk-ua ua-browser"></span>`)
|
|
158
|
-
const $usOS = Utils.createElement(`<span class="atk-ua ua-os"></span>`)
|
|
159
|
-
$uaBrowser.innerText = this.getUserUaBrowser()
|
|
160
|
-
$usOS.innerText = this.getUserUaOS()
|
|
161
|
-
$uaWrap.append($uaBrowser)
|
|
162
|
-
$uaWrap.append($usOS)
|
|
163
|
-
this.$header.append($uaWrap)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private renderContent() {
|
|
168
|
-
// 内容 & 折叠
|
|
169
|
-
if (!this.data.is_collapsed) {
|
|
170
|
-
this.$content.innerHTML = this.getContentMarked()
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
this.$content.classList.add('atk-hide', 'atk-type-collapsed')
|
|
175
|
-
const collapsedInfoEl = Utils.createElement(`
|
|
176
|
-
<div class="atk-collapsed">
|
|
177
|
-
<span class="atk-text">该评论已被系统或管理员折叠</span>
|
|
178
|
-
<span class="atk-show-btn">查看内容</span>
|
|
179
|
-
</div>`)
|
|
180
|
-
this.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
|
|
181
|
-
|
|
182
|
-
const contentShowBtn = collapsedInfoEl.querySelector('.atk-show-btn')!
|
|
183
|
-
contentShowBtn.addEventListener('click', (e) => {
|
|
184
|
-
e.stopPropagation() // 防止穿透
|
|
185
|
-
|
|
186
|
-
if (this.$content.classList.contains('atk-hide')) {
|
|
187
|
-
this.$content.innerHTML = this.getContentMarked()
|
|
188
|
-
this.$content.classList.remove('atk-hide')
|
|
189
|
-
Ui.playFadeInAnim(this.$content)
|
|
190
|
-
contentShowBtn.innerHTML = '收起内容'
|
|
191
|
-
} else {
|
|
192
|
-
this.$content.innerHTML = ''
|
|
193
|
-
this.$content.classList.add('atk-hide')
|
|
194
|
-
contentShowBtn.innerHTML = '查看内容'
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 层级嵌套模式显示 At
|
|
200
|
-
private renderReplyAt() {
|
|
201
|
-
if (this.flatMode || this.data.rid === 0) return // not 平铺模式 或 根评论
|
|
202
|
-
if (this.$replyAt) return // not 关闭显示 或 已经显示
|
|
203
|
-
if (!this.replyTo) return
|
|
204
|
-
|
|
205
|
-
this.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
|
|
206
|
-
this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.replyTo.nick}`
|
|
207
|
-
this.$replyAt.onclick = () => { this.goToReplyComment() }
|
|
208
|
-
|
|
209
|
-
const $after = this.$headerBadge || this.$headerNick
|
|
210
|
-
$after.insertAdjacentElement('afterend', this.$replyAt)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// 回复的对象
|
|
214
|
-
private renderReplyTo() {
|
|
215
|
-
if (!this.flatMode) return // 仅平铺模式显示
|
|
216
|
-
if (!this.replyTo) return
|
|
217
|
-
|
|
218
|
-
this.$replyTo = Utils.createElement(`
|
|
219
|
-
<div class="atk-reply-to">
|
|
220
|
-
<div class="atk-meta">回复 <span class="atk-nick"></span>:</div>
|
|
221
|
-
<div class="atk-content"></div>
|
|
222
|
-
</div>`)
|
|
223
|
-
const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
|
|
224
|
-
$nick.innerText = `@${this.replyTo.nick}`
|
|
225
|
-
$nick.onclick = () => { this.goToReplyComment() }
|
|
226
|
-
let replyContent = Utils.marked(this.ctx, this.replyTo.content)
|
|
227
|
-
if (this.replyTo.is_collapsed) replyContent = '[已折叠]'
|
|
228
|
-
this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
|
|
229
|
-
this.$body.prepend(this.$replyTo)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
public goToReplyComment() {
|
|
233
|
-
const origHash = window.location.hash
|
|
234
|
-
const modifyHash = `#atk-comment-${this.data.rid}`
|
|
235
|
-
|
|
236
|
-
window.location.hash = modifyHash
|
|
237
|
-
if (modifyHash === origHash) window.dispatchEvent(new Event('hashchange')) // 强制触发事件
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// 待审核状态
|
|
241
|
-
private renderPending() {
|
|
242
|
-
if (!this.data.is_pending) return
|
|
243
|
-
|
|
244
|
-
const pendingEl = Utils.createElement(`<div class="atk-pending">审核中,仅本人可见。</div>`)
|
|
245
|
-
this.$body.prepend(pendingEl)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** 初始化评论操作按钮 */
|
|
249
|
-
private renderActionBtn() {
|
|
250
|
-
// 投票功能
|
|
251
|
-
if (this.ctx.conf.vote) {
|
|
252
|
-
// 赞同按钮
|
|
253
|
-
this.voteBtnUp = new ActionBtn(() => `赞同 (${this.data.vote_up || 0})`).appendTo(this.$actions)
|
|
254
|
-
this.voteBtnUp.setClick(() => {
|
|
255
|
-
this.vote('up')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
// 反对按钮
|
|
259
|
-
if (this.ctx.conf.voteDown) {
|
|
260
|
-
this.voteBtnDown = new ActionBtn(() => `反对 (${this.data.vote_down || 0})`).appendTo(this.$actions)
|
|
261
|
-
this.voteBtnDown.setClick(() => {
|
|
262
|
-
this.vote('down')
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 绑定回复按钮事件
|
|
268
|
-
if (this.data.is_allow_reply) {
|
|
269
|
-
const replyBtn = Utils.createElement(`<span>回复</span>`)
|
|
270
|
-
this.$actions.append(replyBtn)
|
|
271
|
-
replyBtn.addEventListener('click', (e) => {
|
|
272
|
-
e.stopPropagation() // 防止穿透
|
|
273
|
-
if (!this.onReplyBtnClick) {
|
|
274
|
-
this.ctx.trigger('editor-reply', {data: this.data, $el: this.$el})
|
|
275
|
-
} else {
|
|
276
|
-
this.onReplyBtnClick()
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// 绑定折叠按钮事件
|
|
282
|
-
const collapseBtn = new ActionBtn({
|
|
283
|
-
text: () => (this.data.is_collapsed ? '取消折叠' : '折叠'),
|
|
284
|
-
adminOnly: true
|
|
285
|
-
})
|
|
286
|
-
collapseBtn.appendTo(this.$actions)
|
|
287
|
-
collapseBtn.setClick(() => {
|
|
288
|
-
this.adminEdit('collapsed', collapseBtn)
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
// 绑定待审核按钮事件
|
|
292
|
-
const pendingBtn = new ActionBtn({
|
|
293
|
-
text: () => (this.data.is_pending ? '待审' : '已审'),
|
|
294
|
-
adminOnly: true
|
|
295
|
-
})
|
|
296
|
-
pendingBtn.appendTo(this.$actions)
|
|
297
|
-
pendingBtn.setClick(() => {
|
|
298
|
-
this.adminEdit('pending', pendingBtn)
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// 绑定删除按钮事件
|
|
302
|
-
const delBtn = new ActionBtn({
|
|
303
|
-
text: '删除',
|
|
304
|
-
confirm: true,
|
|
305
|
-
confirmText: '确认删除',
|
|
306
|
-
adminOnly: true,
|
|
307
|
-
})
|
|
308
|
-
delBtn.appendTo(this.$actions)
|
|
309
|
-
delBtn.setClick(() => {
|
|
310
|
-
this.adminDelete(delBtn)
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
// 绑定置顶按钮事件
|
|
314
|
-
const pinnedBtn = new ActionBtn({
|
|
315
|
-
text: () => (this.data.is_pinned ? '取消置顶' : '置顶'),
|
|
316
|
-
adminOnly: true
|
|
317
|
-
})
|
|
318
|
-
pinnedBtn.appendTo(this.$actions)
|
|
319
|
-
pinnedBtn.setClick(() => {
|
|
320
|
-
this.adminEdit('pinned', pendingBtn)
|
|
321
|
-
})
|
|
322
|
-
}
|
|
323
|
-
//#endregion
|
|
324
|
-
|
|
325
|
-
/** 刷新评论 UI */
|
|
326
|
-
public refreshUI() {
|
|
327
|
-
const originalEl = this.$el
|
|
328
|
-
const newEl = this.render()
|
|
329
|
-
originalEl.replaceWith(newEl) // 替换 document 中的的 elem
|
|
330
|
-
this.playFadeInAnim()
|
|
331
|
-
|
|
332
|
-
// 重建子评论元素
|
|
333
|
-
this.eachComment(this.children, (child) => {
|
|
334
|
-
child.parent?.getChildrenEl().appendChild(child.render())
|
|
335
|
-
child.playFadeInAnim()
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
this.ctx.trigger('comments-loaded')
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** 遍历评论 */
|
|
342
|
-
private eachComment(commentList: Comment[], action: (comment: Comment, levelList: Comment[]) => boolean|void) {
|
|
343
|
-
if (commentList.length === 0) return
|
|
344
|
-
commentList.every((item) => {
|
|
345
|
-
if (action(item, commentList) === false) return false
|
|
346
|
-
this.eachComment(item.getChildren(), action)
|
|
347
|
-
return true
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
getIsRoot() {
|
|
352
|
-
return this.data.rid === 0
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
getChildren() {
|
|
356
|
-
return this.children
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
|
360
|
-
childC.parent = this
|
|
361
|
-
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
362
|
-
this.children.push(childC)
|
|
363
|
-
|
|
364
|
-
const $children = this.getChildrenEl()
|
|
365
|
-
if (insertMode === 'append') $children.append(childC.getEl())
|
|
366
|
-
else if (insertMode === 'prepend') $children.prepend(childC.getEl())
|
|
367
|
-
|
|
368
|
-
childC.playFadeInAnim()
|
|
369
|
-
|
|
370
|
-
// 内容限高
|
|
371
|
-
childC.checkHeightLimitArea('content')
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
getChildrenEl(): HTMLElement {
|
|
375
|
-
if (!this.$children) {
|
|
376
|
-
// console.log(this.nestCurt)
|
|
377
|
-
if (this.nestCurt < this.nestMax) {
|
|
378
|
-
this.$children = Utils.createElement('<div class="atk-comment-children"></div>')
|
|
379
|
-
this.$main.append(this.$children)
|
|
380
|
-
} else if (this.parent) {
|
|
381
|
-
this.$children = this.parent.getChildrenEl()
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return this.$children!
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
getParent() {
|
|
389
|
-
return this.parent
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
getParents() {
|
|
393
|
-
const parents: Comment[] = []
|
|
394
|
-
const once = (c: Comment) => {
|
|
395
|
-
if (c.parent) {
|
|
396
|
-
parents.push(c.parent)
|
|
397
|
-
once(c.parent)
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
once(this)
|
|
402
|
-
return parents
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
getEl() {
|
|
406
|
-
return this.$el
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
getData() {
|
|
410
|
-
return this.data
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
getGravatarUrl() {
|
|
414
|
-
return Utils.getGravatarURL(this.ctx, this.data.email_encrypted)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
getContentMarked() {
|
|
418
|
-
return Utils.marked(this.ctx, this.data.content)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
getDateFormatted() {
|
|
422
|
-
return Utils.timeAgo(new Date(this.data.date))
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
getUserUaBrowser() {
|
|
426
|
-
const info = UADetect(this.data.ua)
|
|
427
|
-
return `${info.browser} ${info.version}`
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
getUserUaOS() {
|
|
431
|
-
const info = UADetect(this.data.ua)
|
|
432
|
-
return `${info.os} ${info.osVersion}`
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** 渐出动画 */
|
|
436
|
-
playFadeInAnim() {
|
|
437
|
-
Ui.playFadeInAnim(this.$el)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/** 投票操作 */
|
|
441
|
-
vote(type: 'up'|'down') {
|
|
442
|
-
const actionBtn = type === 'up' ? this.voteBtnUp : this.voteBtnDown
|
|
443
|
-
|
|
444
|
-
new Api(this.ctx).vote(this.data.id, `comment_${type}`)
|
|
445
|
-
.then((v) => {
|
|
446
|
-
this.data.vote_up = v.up
|
|
447
|
-
this.data.vote_down = v.down
|
|
448
|
-
this.voteBtnUp?.updateText()
|
|
449
|
-
this.voteBtnDown?.updateText()
|
|
450
|
-
})
|
|
451
|
-
.catch((err) => {
|
|
452
|
-
actionBtn?.setError(`投票失败`)
|
|
453
|
-
console.log(err)
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** 管理员 - 评论状态修改 */
|
|
458
|
-
adminEdit(type: 'collapsed'|'pending'|'pinned', btnElem: ActionBtn) {
|
|
459
|
-
if (btnElem.isLoading) return // 若正在修改中
|
|
460
|
-
|
|
461
|
-
btnElem.setLoading(true, '修改中...')
|
|
462
|
-
|
|
463
|
-
// 克隆并修改当前数据
|
|
464
|
-
const modify = { ...this.data }
|
|
465
|
-
if (type === 'collapsed') {
|
|
466
|
-
modify.is_collapsed = !modify.is_collapsed
|
|
467
|
-
} else if (type === 'pending') {
|
|
468
|
-
modify.is_pending = !modify.is_pending
|
|
469
|
-
} else if (type === 'pinned') {
|
|
470
|
-
modify.is_pinned = !modify.is_pinned
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
new Api(this.ctx).commentEdit(modify).then((comment) => {
|
|
474
|
-
btnElem.setLoading(false)
|
|
475
|
-
|
|
476
|
-
// 刷新当前 Comment UI
|
|
477
|
-
this.data = comment
|
|
478
|
-
this.refreshUI()
|
|
479
|
-
Ui.playFadeInAnim(this.$body)
|
|
480
|
-
|
|
481
|
-
// 刷新 List UI
|
|
482
|
-
this.ctx.trigger('list-refresh-ui')
|
|
483
|
-
}).catch((err) => {
|
|
484
|
-
console.error(err)
|
|
485
|
-
btnElem.setError('修改失败')
|
|
486
|
-
})
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
public onDelete?: (comment: Comment) => void
|
|
490
|
-
|
|
491
|
-
/** 管理员 - 评论删除 */
|
|
492
|
-
adminDelete(btnElem: ActionBtn) {
|
|
493
|
-
if (btnElem.isLoading) return // 若正在删除中
|
|
494
|
-
|
|
495
|
-
btnElem.setLoading(true, '删除中...')
|
|
496
|
-
new Api(this.ctx).commentDel(this.data.id, this.data.site_name)
|
|
497
|
-
.then(() => {
|
|
498
|
-
btnElem.setLoading(false)
|
|
499
|
-
if (this.onDelete) this.onDelete(this)
|
|
500
|
-
})
|
|
501
|
-
.catch((e) => {
|
|
502
|
-
console.error(e)
|
|
503
|
-
btnElem.setError('删除失败')
|
|
504
|
-
})
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
public setUnread(val: boolean) {
|
|
508
|
-
this.unread = val
|
|
509
|
-
if (this.unread) this.$el.classList.add('atk-unread')
|
|
510
|
-
else this.$el.classList.remove('atk-unread')
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
public setOpenURL(url: string) {
|
|
514
|
-
if (!url) {
|
|
515
|
-
this.openable = false
|
|
516
|
-
this.$el.classList.remove('atk-openable')
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
this.openable = true
|
|
520
|
-
this.openURL = url
|
|
521
|
-
this.$el.classList.add('atk-openable')
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/** 内容限高检测 */
|
|
525
|
-
checkHeightLimit() {
|
|
526
|
-
this.checkHeightLimitArea('content') // 评论内容限高
|
|
527
|
-
this.checkHeightLimitArea('children') // 子评论部分限高(嵌套模式)
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/** 目标内容限高检测 */
|
|
531
|
-
checkHeightLimitArea(area: 'children'|'content') {
|
|
532
|
-
// 参数准备
|
|
533
|
-
const childrenMaxH = this.ctx.conf.heightLimit?.children
|
|
534
|
-
const contentMaxH = this.ctx.conf.heightLimit?.content
|
|
535
|
-
|
|
536
|
-
if (area === 'children' && !childrenMaxH) return
|
|
537
|
-
if (area === 'content' && !contentMaxH) return
|
|
538
|
-
|
|
539
|
-
// 限高
|
|
540
|
-
let maxHeight: number
|
|
541
|
-
if (area === 'children') maxHeight = childrenMaxH!
|
|
542
|
-
if (area === 'content') maxHeight = contentMaxH!
|
|
543
|
-
|
|
544
|
-
// 检测指定元素
|
|
545
|
-
const checkEl = ($el?: HTMLElement|null) => {
|
|
546
|
-
if (!$el) return
|
|
547
|
-
|
|
548
|
-
// 是否超过高度
|
|
549
|
-
if (Utils.getHeight($el) > maxHeight) {
|
|
550
|
-
this.heightLimitAdd($el, maxHeight)
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// 执行限高检测
|
|
555
|
-
if (area === 'children') {
|
|
556
|
-
checkEl(this.$children)
|
|
557
|
-
} else if (area === 'content') {
|
|
558
|
-
checkEl(this.$content)
|
|
559
|
-
checkEl(this.$replyTo)
|
|
560
|
-
|
|
561
|
-
// 若有图片 · 图片加载完后再检测一次
|
|
562
|
-
Utils.onImagesLoaded(this.$content, () => {
|
|
563
|
-
checkEl(this.$content)
|
|
564
|
-
})
|
|
565
|
-
if (this.$replyTo) {
|
|
566
|
-
Utils.onImagesLoaded(this.$replyTo, () => {
|
|
567
|
-
checkEl(this.$replyTo)
|
|
568
|
-
})
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// 操作 · 取消限高
|
|
574
|
-
heightLimitRemove($el: HTMLElement) {
|
|
575
|
-
if (!$el) return
|
|
576
|
-
if (!$el.classList.contains('atk-height-limit')) return
|
|
577
|
-
|
|
578
|
-
$el.classList.remove('atk-height-limit')
|
|
579
|
-
Array.from($el.children).forEach((e) => {
|
|
580
|
-
if (e.classList.contains('atk-height-limit-btn')) e.remove()
|
|
581
|
-
})
|
|
582
|
-
$el.style.height = ''
|
|
583
|
-
$el.style.overflow = ''
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// 操作 · 内容限高
|
|
587
|
-
heightLimitAdd($el: HTMLElement, maxHeight: number) {
|
|
588
|
-
if (!$el) return
|
|
589
|
-
if ($el.classList.contains('atk-height-limit')) return
|
|
590
|
-
|
|
591
|
-
$el.classList.add('atk-height-limit')
|
|
592
|
-
$el.style.height = `${maxHeight}px`
|
|
593
|
-
$el.style.overflow = 'hidden'
|
|
594
|
-
const $hideMoreOpenBtn = Utils.createElement(`<div class="atk-height-limit-btn">阅读更多</span>`)
|
|
595
|
-
$hideMoreOpenBtn.onclick = (e) => {
|
|
596
|
-
e.stopPropagation()
|
|
597
|
-
this.heightLimitRemove($el)
|
|
598
|
-
|
|
599
|
-
// 子评论数等于 1,直接取消限高
|
|
600
|
-
const children = this.getChildren()
|
|
601
|
-
if (children.length === 1) children[0].heightLimitRemove(children[0].$content)
|
|
602
|
-
}
|
|
603
|
-
$el.append($hideMoreOpenBtn)
|
|
604
|
-
}
|
|
605
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import './_PreviewPlug/PreviewPlug.less'
|
|
2
|
-
import Editor from '../Editor'
|
|
3
|
-
import ArtalkContext from '~/src/ArtalkContext'
|
|
4
|
-
import Utils from '~/src/utils'
|
|
5
|
-
|
|
6
|
-
export default class PreviewPlug extends ArtalkContext {
|
|
7
|
-
public elem: HTMLElement
|
|
8
|
-
|
|
9
|
-
constructor (public editor: Editor) {
|
|
10
|
-
super(editor.artalk)
|
|
11
|
-
|
|
12
|
-
this.initElem()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
initElem () {
|
|
16
|
-
this.elem = Utils.createElement('<div class="atk-editor-plug-example"></div>')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
static Name = 'example'
|
|
20
|
-
static BtnHTML = '栗子'
|
|
21
|
-
|
|
22
|
-
getEl () {
|
|
23
|
-
return this.elem
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
onShow () {}
|
|
27
|
-
|
|
28
|
-
onHide () {}
|
|
29
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import Context from "~/src/context"
|
|
2
|
-
import Editor from "../editor"
|
|
3
|
-
|
|
4
|
-
export default abstract class EditorPlug {
|
|
5
|
-
protected editor: Editor
|
|
6
|
-
protected ctx: Context
|
|
7
|
-
public abstract $el: HTMLElement
|
|
8
|
-
|
|
9
|
-
constructor (editor: Editor) {
|
|
10
|
-
this.editor = editor
|
|
11
|
-
this.ctx = editor.ctx
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
public static Name: string
|
|
15
|
-
public static BtnHTML: string
|
|
16
|
-
|
|
17
|
-
public abstract getEl(): HTMLElement
|
|
18
|
-
public abstract onShow(): void
|
|
19
|
-
public abstract onHide(): void
|
|
20
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import './preview-plug.less'
|
|
2
|
-
|
|
3
|
-
import * as Utils from '~/src/lib/utils'
|
|
4
|
-
import Editor from '../editor'
|
|
5
|
-
import EditorPlug from './editor-plug'
|
|
6
|
-
|
|
7
|
-
export default class PreviewPlug extends EditorPlug {
|
|
8
|
-
$el!: HTMLElement
|
|
9
|
-
binded: boolean = false
|
|
10
|
-
|
|
11
|
-
constructor (editor: Editor) {
|
|
12
|
-
super(editor)
|
|
13
|
-
|
|
14
|
-
this.initEl()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
initEl () {
|
|
18
|
-
this.$el = Utils.createElement('<div class="atk-editor-plug-preview"></div>')
|
|
19
|
-
this.binded = false
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static Name = 'preview'
|
|
23
|
-
static BtnHTML = '预览 <i title="Markdown is supported"><svg class="markdown" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></i>'
|
|
24
|
-
|
|
25
|
-
getEl () {
|
|
26
|
-
return this.$el
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
onShow () {
|
|
30
|
-
this.updateContent()
|
|
31
|
-
if (!this.binded) {
|
|
32
|
-
const event = () => {
|
|
33
|
-
this.updateContent()
|
|
34
|
-
}
|
|
35
|
-
this.editor.$textarea.addEventListener('input', event)
|
|
36
|
-
this.editor.$textarea.addEventListener('change', event)
|
|
37
|
-
this.binded = true
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
onHide () {}
|
|
42
|
-
|
|
43
|
-
updateContent () {
|
|
44
|
-
if (this.$el.style.display !== 'none') {
|
|
45
|
-
this.$el.innerHTML = this.editor.getContentMarked()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|