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,191 @@
|
|
|
1
|
+
import { CommentData } from '~/types/artalk-data'
|
|
2
|
+
import Context from '~/types/context'
|
|
3
|
+
import Component from '../lib/component'
|
|
4
|
+
import * as Utils from '../lib/utils'
|
|
5
|
+
import UADetect from '../lib/detect'
|
|
6
|
+
import CommentRender from './render'
|
|
7
|
+
import CommentActions from './actions'
|
|
8
|
+
|
|
9
|
+
export interface CommentConf {
|
|
10
|
+
isUnread?: boolean
|
|
11
|
+
openURL?: string
|
|
12
|
+
isFlatMode: boolean
|
|
13
|
+
replyTo?: CommentData
|
|
14
|
+
afterRender?: () => void
|
|
15
|
+
openEvt?: () => void
|
|
16
|
+
onReplyBtnClick?: Function
|
|
17
|
+
onDelete?: Function
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default class Comment extends Component {
|
|
21
|
+
private renderInstance: CommentRender
|
|
22
|
+
private actionInstance: CommentActions
|
|
23
|
+
|
|
24
|
+
private data: CommentData
|
|
25
|
+
private cConf: CommentConf
|
|
26
|
+
|
|
27
|
+
private parent: Comment|null
|
|
28
|
+
private children: Comment[] = []
|
|
29
|
+
|
|
30
|
+
private nestCurt: number // 当前嵌套层数
|
|
31
|
+
private nestMax: number // 最大嵌套层数
|
|
32
|
+
|
|
33
|
+
constructor(ctx: Context, data: CommentData, conf: CommentConf) {
|
|
34
|
+
super(ctx)
|
|
35
|
+
|
|
36
|
+
// 最大嵌套数
|
|
37
|
+
this.nestMax = ctx.conf.nestMax || 3
|
|
38
|
+
|
|
39
|
+
this.cConf = conf
|
|
40
|
+
this.data = { ...data }
|
|
41
|
+
this.data.date = this.data.date.replace(/-/g, '/') // 解决 Safari 日期解析 NaN 问题
|
|
42
|
+
|
|
43
|
+
this.parent = null
|
|
44
|
+
this.nestCurt = 1 // 现在已嵌套 n 层
|
|
45
|
+
|
|
46
|
+
this.actionInstance = new CommentActions(this)
|
|
47
|
+
this.renderInstance = new CommentRender(this)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 渲染 UI */
|
|
51
|
+
public render() {
|
|
52
|
+
const newEl = this.renderInstance.render()
|
|
53
|
+
|
|
54
|
+
if (this.$el) this.$el.replaceWith(newEl)
|
|
55
|
+
this.$el = newEl
|
|
56
|
+
|
|
57
|
+
if (this.cConf.afterRender) this.cConf.afterRender()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 获取评论操作实例对象 */
|
|
61
|
+
public getActions() {
|
|
62
|
+
return this.actionInstance
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 获取评论渲染器实例对象 */
|
|
66
|
+
public getRender() {
|
|
67
|
+
return this.renderInstance
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 获取评论数据 */
|
|
71
|
+
public getData() {
|
|
72
|
+
return this.data
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 设置数据 */
|
|
76
|
+
public setData(data: CommentData) {
|
|
77
|
+
this.data = data
|
|
78
|
+
|
|
79
|
+
this.render()
|
|
80
|
+
this.getRender().playFadeAnimForBody()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 获取父评论 */
|
|
84
|
+
public getParent() {
|
|
85
|
+
return this.parent
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 获取所有子评论 */
|
|
89
|
+
public getChildren() {
|
|
90
|
+
return this.children
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 获取当前嵌套层数 */
|
|
94
|
+
public getNestCurt() {
|
|
95
|
+
return this.nestCurt
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 判断是否为根评论 */
|
|
99
|
+
public getIsRoot() {
|
|
100
|
+
return this.data.rid === 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 获取评论 ID */
|
|
104
|
+
public getID() {
|
|
105
|
+
return this.data.id
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 置入子评论 */
|
|
109
|
+
public putChild(childC: Comment, insertMode: 'append'|'prepend' = 'append') {
|
|
110
|
+
childC.parent = this
|
|
111
|
+
childC.nestCurt = this.nestCurt + 1 // 嵌套层数 +1
|
|
112
|
+
|
|
113
|
+
this.children.push(childC)
|
|
114
|
+
|
|
115
|
+
const $children = this.getChildrenEl()
|
|
116
|
+
if (insertMode === 'append') $children.append(childC.getEl())
|
|
117
|
+
else if (insertMode === 'prepend') $children.prepend(childC.getEl())
|
|
118
|
+
|
|
119
|
+
childC.getRender().playFadeAnim()
|
|
120
|
+
|
|
121
|
+
// 内容限高
|
|
122
|
+
childC.getRender().checkHeightLimitArea('content')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 获取存放子评论的元素对象 */
|
|
126
|
+
public getChildrenEl(): HTMLElement {
|
|
127
|
+
let $children = this.getRender().getChildrenWrap()
|
|
128
|
+
|
|
129
|
+
if (!$children) {
|
|
130
|
+
// console.log(this.nestCurt)
|
|
131
|
+
if (this.nestCurt < this.nestMax) {
|
|
132
|
+
$children = this.getRender().renderChildrenWrap()
|
|
133
|
+
} else {
|
|
134
|
+
$children = this.parent!.getChildrenEl()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return $children
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 获取所有父评论 */
|
|
142
|
+
public getParents() {
|
|
143
|
+
const parents: Comment[] = []
|
|
144
|
+
const once = (c: Comment) => {
|
|
145
|
+
if (c.parent) {
|
|
146
|
+
parents.push(c.parent)
|
|
147
|
+
once(c.parent)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
once(this)
|
|
152
|
+
return parents
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 获取评论元素对象 */
|
|
156
|
+
public getEl() {
|
|
157
|
+
return this.$el
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 获取 Gravatar 头像 URL */
|
|
161
|
+
public getGravatarURL() {
|
|
162
|
+
return Utils.getGravatarURL(this.ctx, this.data.email_encrypted)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 获取评论 markdown 解析后的内容 */
|
|
166
|
+
public getContentMarked() {
|
|
167
|
+
return Utils.marked(this.ctx, this.data.content)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 获取格式化后的日期 */
|
|
171
|
+
public getDateFormatted() {
|
|
172
|
+
return Utils.timeAgo(new Date(this.data.date), this.ctx)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 获取用户 UserAgent 浏览器 */
|
|
176
|
+
public getUserUaBrowser() {
|
|
177
|
+
const info = UADetect(this.data.ua)
|
|
178
|
+
return `${info.browser} ${info.version}`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 获取用户 UserAgent 系统 */
|
|
182
|
+
public getUserUaOS() {
|
|
183
|
+
const info = UADetect(this.data.ua)
|
|
184
|
+
return `${info.os} ${info.osVersion}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** 获取配置 */
|
|
188
|
+
public getConf() {
|
|
189
|
+
return this.cConf
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import '../style/comment.less'
|
|
2
|
+
|
|
3
|
+
import * as Utils from '../lib/utils'
|
|
4
|
+
import * as Ui from '../lib/ui'
|
|
5
|
+
import ActionBtn from '../components/action-btn'
|
|
6
|
+
import CommentHTML from './comment.html?raw'
|
|
7
|
+
import Comment from './comment'
|
|
8
|
+
|
|
9
|
+
export default class CommentRender {
|
|
10
|
+
private comment: Comment
|
|
11
|
+
|
|
12
|
+
private get ctx() { return this.comment.ctx }
|
|
13
|
+
private get data() { return this.comment.getData() }
|
|
14
|
+
private get cConf() { return this.comment.getConf() }
|
|
15
|
+
|
|
16
|
+
public $el!: HTMLElement
|
|
17
|
+
public $main!: HTMLElement
|
|
18
|
+
public $header!: HTMLElement
|
|
19
|
+
public $headerNick!: HTMLElement
|
|
20
|
+
public $headerBadgeWrap!: HTMLElement
|
|
21
|
+
public $body!: HTMLElement
|
|
22
|
+
public $content!: HTMLElement
|
|
23
|
+
private $childrenWrap!: HTMLElement|null
|
|
24
|
+
public $actions!: HTMLElement
|
|
25
|
+
public voteBtnUp?: ActionBtn
|
|
26
|
+
public voteBtnDown?: ActionBtn
|
|
27
|
+
|
|
28
|
+
public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
|
|
29
|
+
public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
|
|
30
|
+
|
|
31
|
+
public constructor(comment: Comment) {
|
|
32
|
+
this.comment = comment
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public render() {
|
|
36
|
+
this.$el = Utils.createElement(CommentHTML)
|
|
37
|
+
|
|
38
|
+
this.$main = this.$el.querySelector('.atk-main')!
|
|
39
|
+
this.$header = this.$el.querySelector('.atk-header')!
|
|
40
|
+
this.$body = this.$el.querySelector('.atk-body')!
|
|
41
|
+
this.$content = this.$body.querySelector('.atk-content')!
|
|
42
|
+
this.$actions = this.$el.querySelector('.atk-actions')!
|
|
43
|
+
|
|
44
|
+
this.$el.setAttribute('data-comment-id', `${this.data.id}`)
|
|
45
|
+
|
|
46
|
+
this.renderAvatar()
|
|
47
|
+
this.renderHeader()
|
|
48
|
+
this.renderContent()
|
|
49
|
+
this.renderReplyAt()
|
|
50
|
+
this.renderReplyTo()
|
|
51
|
+
this.renderPending()
|
|
52
|
+
this.renderActionBtn()
|
|
53
|
+
|
|
54
|
+
this.recoveryChildrenWrap()
|
|
55
|
+
|
|
56
|
+
return this.$el
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 初始化 - 评论头像 */
|
|
60
|
+
private renderAvatar() {
|
|
61
|
+
const $avatar = this.$el.querySelector<HTMLElement>('.atk-avatar')!
|
|
62
|
+
const $avatarImg = Utils.createElement<HTMLImageElement>('<img />')
|
|
63
|
+
$avatarImg.src = this.comment.getGravatarURL()
|
|
64
|
+
if (this.data.link) {
|
|
65
|
+
const $avatarA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
66
|
+
$avatarA.href = this.data.link
|
|
67
|
+
$avatarA.append($avatarImg)
|
|
68
|
+
$avatar.append($avatarA)
|
|
69
|
+
} else {
|
|
70
|
+
$avatar.append($avatarImg)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 初始化 - 评论信息 */
|
|
75
|
+
private renderHeader() {
|
|
76
|
+
this.$headerNick = this.$el.querySelector<HTMLElement>('.atk-nick')!
|
|
77
|
+
|
|
78
|
+
if (this.data.link) {
|
|
79
|
+
const $nickA = Utils.createElement<HTMLLinkElement>('<a target="_blank" rel="noreferrer noopener nofollow"></a>')
|
|
80
|
+
$nickA.innerText = this.data.nick
|
|
81
|
+
$nickA.href = this.data.link
|
|
82
|
+
this.$headerNick.append($nickA)
|
|
83
|
+
} else {
|
|
84
|
+
this.$headerNick.innerText = this.data.nick
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.$headerBadgeWrap = this.$el.querySelector<HTMLElement>('.atk-badge-wrap')!
|
|
88
|
+
this.$headerBadgeWrap.innerHTML = ''
|
|
89
|
+
|
|
90
|
+
const badgeText = this.data.badge_name
|
|
91
|
+
const badgeColor = this.data.badge_color
|
|
92
|
+
if (badgeText) {
|
|
93
|
+
const $badge = Utils.createElement(`<span class="atk-badge"></span>`)
|
|
94
|
+
$badge.innerText = badgeText.replace('管理员', this.ctx.$t('admin')) // i18n patch
|
|
95
|
+
$badge.style.backgroundColor = badgeColor || ''
|
|
96
|
+
this.$headerBadgeWrap.append($badge)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.data.is_pinned) {
|
|
100
|
+
const $pinnedBadge = Utils.createElement(`<span class="atk-pinned-badge">${this.ctx.$t('pin')}</span>`) // 置顶徽章
|
|
101
|
+
this.$headerBadgeWrap.append($pinnedBadge)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const $date = this.$el.querySelector<HTMLElement>('.atk-date')!
|
|
105
|
+
$date.innerText = this.comment.getDateFormatted()
|
|
106
|
+
$date.setAttribute('data-atk-comment-date', String(+new Date(this.data.date)))
|
|
107
|
+
|
|
108
|
+
if (this.ctx.conf.uaBadge) {
|
|
109
|
+
let $uaWrap = this.$header.querySelector('atk-ua-wrap')
|
|
110
|
+
if (!$uaWrap) {
|
|
111
|
+
$uaWrap = Utils.createElement(`<span class="atk-ua-wrap"></span>`)
|
|
112
|
+
this.$header.append($uaWrap)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
$uaWrap.innerHTML = ''
|
|
116
|
+
const $uaBrowser = Utils.createElement(`<span class="atk-ua ua-browser"></span>`)
|
|
117
|
+
const $usOS = Utils.createElement(`<span class="atk-ua ua-os"></span>`)
|
|
118
|
+
$uaBrowser.innerText = this.comment.getUserUaBrowser()
|
|
119
|
+
$usOS.innerText = this.comment.getUserUaOS()
|
|
120
|
+
$uaWrap.append($uaBrowser)
|
|
121
|
+
$uaWrap.append($usOS)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 初始化 - 评论内容 */
|
|
126
|
+
private renderContent() {
|
|
127
|
+
if (!this.data.is_collapsed) {
|
|
128
|
+
this.$content.innerHTML = this.comment.getContentMarked()
|
|
129
|
+
this.$content.classList.remove('atk-hide', 'atk-collapsed')
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 内容 & 折叠
|
|
134
|
+
this.$content.classList.add('atk-hide', 'atk-type-collapsed')
|
|
135
|
+
const collapsedInfoEl = Utils.createElement(`
|
|
136
|
+
<div class="atk-collapsed">
|
|
137
|
+
<span class="atk-text">${this.ctx.$t('collapsedMsg')}</span>
|
|
138
|
+
<span class="atk-show-btn">${this.ctx.$t('expand')}</span>
|
|
139
|
+
</div>`)
|
|
140
|
+
this.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
|
|
141
|
+
|
|
142
|
+
const contentShowBtn = collapsedInfoEl.querySelector('.atk-show-btn')!
|
|
143
|
+
contentShowBtn.addEventListener('click', (e) => {
|
|
144
|
+
e.stopPropagation() // 防止穿透
|
|
145
|
+
|
|
146
|
+
if (this.$content.classList.contains('atk-hide')) {
|
|
147
|
+
this.$content.innerHTML = this.comment.getContentMarked()
|
|
148
|
+
this.$content.classList.remove('atk-hide')
|
|
149
|
+
Ui.playFadeInAnim(this.$content)
|
|
150
|
+
contentShowBtn.innerHTML = this.ctx.$t('collapse')
|
|
151
|
+
} else {
|
|
152
|
+
this.$content.innerHTML = ''
|
|
153
|
+
this.$content.classList.add('atk-hide')
|
|
154
|
+
contentShowBtn.innerHTML = this.ctx.$t('expand')
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 初始化 - 层级嵌套模式显示 At */
|
|
160
|
+
private renderReplyAt() {
|
|
161
|
+
if (this.cConf.isFlatMode || this.data.rid === 0) return // not 平铺模式 或 根评论
|
|
162
|
+
if (!this.cConf.replyTo) return
|
|
163
|
+
|
|
164
|
+
this.$replyAt = Utils.createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`)
|
|
165
|
+
this.$replyAt.querySelector<HTMLElement>('.atk-nick')!.innerText = `${this.cConf.replyTo.nick}`
|
|
166
|
+
this.$replyAt.onclick = () => { this.comment.getActions().goToReplyComment() }
|
|
167
|
+
|
|
168
|
+
this.$headerBadgeWrap.insertAdjacentElement('afterend', this.$replyAt)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 初始化 - 回复的对象 */
|
|
172
|
+
private renderReplyTo() {
|
|
173
|
+
if (!this.cConf.isFlatMode) return // 仅平铺模式显示
|
|
174
|
+
if (!this.cConf.replyTo) return
|
|
175
|
+
|
|
176
|
+
this.$replyTo = Utils.createElement(`
|
|
177
|
+
<div class="atk-reply-to">
|
|
178
|
+
<div class="atk-meta">${this.ctx.$t('reply')} <span class="atk-nick"></span>:</div>
|
|
179
|
+
<div class="atk-content"></div>
|
|
180
|
+
</div>`)
|
|
181
|
+
const $nick = this.$replyTo.querySelector<HTMLElement>('.atk-nick')!
|
|
182
|
+
$nick.innerText = `@${this.cConf.replyTo.nick}`
|
|
183
|
+
$nick.onclick = () => { this.comment.getActions().goToReplyComment() }
|
|
184
|
+
let replyContent = Utils.marked(this.ctx, this.cConf.replyTo.content)
|
|
185
|
+
if (this.cConf.replyTo.is_collapsed) replyContent = `[${this.ctx.$t('collapsed')}]`
|
|
186
|
+
this.$replyTo.querySelector<HTMLElement>('.atk-content')!.innerHTML = replyContent
|
|
187
|
+
this.$body.prepend(this.$replyTo)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 初始化 - 待审核状态 */
|
|
191
|
+
private renderPending() {
|
|
192
|
+
if (!this.data.is_pending) return
|
|
193
|
+
|
|
194
|
+
const pendingEl = Utils.createElement(`<div class="atk-pending">${this.ctx.$t('pendingMsg')}</div>`)
|
|
195
|
+
this.$body.prepend(pendingEl)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 初始化 - 评论操作按钮 */
|
|
199
|
+
private renderActionBtn() {
|
|
200
|
+
// 投票功能
|
|
201
|
+
if (this.ctx.conf.vote) {
|
|
202
|
+
// 赞同按钮
|
|
203
|
+
this.voteBtnUp = new ActionBtn(this.ctx, () => `${this.ctx.$t('voteUp')} (${this.data.vote_up || 0})`).appendTo(this.$actions)
|
|
204
|
+
this.voteBtnUp.setClick(() => {
|
|
205
|
+
this.comment.getActions().vote('up')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// 反对按钮
|
|
209
|
+
if (this.ctx.conf.voteDown) {
|
|
210
|
+
this.voteBtnDown = new ActionBtn(this.ctx, () => `${this.ctx.$t('voteDown')} (${this.data.vote_down || 0})`).appendTo(this.$actions)
|
|
211
|
+
this.voteBtnDown.setClick(() => {
|
|
212
|
+
this.comment.getActions().vote('down')
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 绑定回复按钮事件
|
|
218
|
+
if (this.data.is_allow_reply) {
|
|
219
|
+
const replyBtn = Utils.createElement(`<span>${this.ctx.$t('reply')}</span>`)
|
|
220
|
+
this.$actions.append(replyBtn)
|
|
221
|
+
replyBtn.addEventListener('click', (e) => {
|
|
222
|
+
e.stopPropagation() // 防止穿透
|
|
223
|
+
if (!this.cConf.onReplyBtnClick) {
|
|
224
|
+
this.ctx.replyComment(this.data, this.$el)
|
|
225
|
+
} else {
|
|
226
|
+
this.cConf.onReplyBtnClick()
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 绑定折叠按钮事件
|
|
232
|
+
const collapseBtn = new ActionBtn(this.ctx, {
|
|
233
|
+
text: () => (this.data.is_collapsed ? this.ctx.$t('expand') : this.ctx.$t('collapse')),
|
|
234
|
+
adminOnly: true
|
|
235
|
+
})
|
|
236
|
+
collapseBtn.appendTo(this.$actions)
|
|
237
|
+
collapseBtn.setClick(() => {
|
|
238
|
+
this.comment.getActions().adminEdit('collapsed', collapseBtn)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// 绑定待审核按钮事件
|
|
242
|
+
const pendingBtn = new ActionBtn(this.ctx, {
|
|
243
|
+
text: () => (this.data.is_pending ? this.ctx.$t('pending') : this.ctx.$t('approved')),
|
|
244
|
+
adminOnly: true
|
|
245
|
+
})
|
|
246
|
+
pendingBtn.appendTo(this.$actions)
|
|
247
|
+
pendingBtn.setClick(() => {
|
|
248
|
+
this.comment.getActions().adminEdit('pending', pendingBtn)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// 绑定删除按钮事件
|
|
252
|
+
const delBtn = new ActionBtn(this.ctx, {
|
|
253
|
+
text: this.ctx.$t('delete'),
|
|
254
|
+
confirm: true,
|
|
255
|
+
confirmText: this.ctx.$t('deleteConfirm'),
|
|
256
|
+
adminOnly: true,
|
|
257
|
+
})
|
|
258
|
+
delBtn.appendTo(this.$actions)
|
|
259
|
+
delBtn.setClick(() => {
|
|
260
|
+
this.comment.getActions().adminDelete(delBtn)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// 绑定置顶按钮事件
|
|
264
|
+
const pinnedBtn = new ActionBtn(this.ctx, {
|
|
265
|
+
text: () => (this.data.is_pinned ? this.ctx.$t('unpin') : this.ctx.$t('pin')),
|
|
266
|
+
adminOnly: true
|
|
267
|
+
})
|
|
268
|
+
pinnedBtn.appendTo(this.$actions)
|
|
269
|
+
pinnedBtn.setClick(() => {
|
|
270
|
+
this.comment.getActions().adminEdit('pinned', pendingBtn)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 内容限高检测 */
|
|
275
|
+
public checkHeightLimit() {
|
|
276
|
+
this.checkHeightLimitArea('content') // 评论内容限高
|
|
277
|
+
this.checkHeightLimitArea('children') // 子评论部分限高(嵌套模式)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 目标内容限高检测 */
|
|
281
|
+
public checkHeightLimitArea(area: 'children'|'content') {
|
|
282
|
+
// 参数准备
|
|
283
|
+
const childrenMaxH = this.ctx.conf.heightLimit.children
|
|
284
|
+
const contentMaxH = this.ctx.conf.heightLimit.content
|
|
285
|
+
|
|
286
|
+
if (area === 'children' && !childrenMaxH) return
|
|
287
|
+
if (area === 'content' && !contentMaxH) return
|
|
288
|
+
|
|
289
|
+
// 限高
|
|
290
|
+
let maxHeight: number
|
|
291
|
+
if (area === 'children') maxHeight = childrenMaxH!
|
|
292
|
+
if (area === 'content') maxHeight = contentMaxH!
|
|
293
|
+
|
|
294
|
+
// 检测指定元素
|
|
295
|
+
const checkEl = ($el?: HTMLElement|null) => {
|
|
296
|
+
if (!$el) return
|
|
297
|
+
|
|
298
|
+
// 是否超过高度
|
|
299
|
+
if (Utils.getHeight($el) > maxHeight) {
|
|
300
|
+
this.heightLimitAdd($el, maxHeight)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 执行限高检测
|
|
305
|
+
if (area === 'children') {
|
|
306
|
+
checkEl(this.$childrenWrap)
|
|
307
|
+
} else if (area === 'content') {
|
|
308
|
+
checkEl(this.$content)
|
|
309
|
+
checkEl(this.$replyTo)
|
|
310
|
+
|
|
311
|
+
// 若有图片 · 图片加载完后再检测一次
|
|
312
|
+
Utils.onImagesLoaded(this.$content, () => {
|
|
313
|
+
checkEl(this.$content)
|
|
314
|
+
})
|
|
315
|
+
if (this.$replyTo) {
|
|
316
|
+
Utils.onImagesLoaded(this.$replyTo, () => {
|
|
317
|
+
checkEl(this.$replyTo)
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** 移除限高 */
|
|
324
|
+
private heightLimitRemove($el: HTMLElement) {
|
|
325
|
+
if (!$el) return
|
|
326
|
+
if (!$el.classList.contains('atk-height-limit')) return
|
|
327
|
+
|
|
328
|
+
$el.classList.remove('atk-height-limit')
|
|
329
|
+
Array.from($el.children).forEach((e) => {
|
|
330
|
+
if (e.classList.contains('atk-height-limit-btn')) e.remove()
|
|
331
|
+
})
|
|
332
|
+
$el.style.height = ''
|
|
333
|
+
$el.style.overflow = ''
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** 子评论区域移除限高 */
|
|
337
|
+
public heightLimitRemoveForChildren() {
|
|
338
|
+
if (!this.$childrenWrap) return
|
|
339
|
+
this.heightLimitRemove(this.$childrenWrap)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 内容限高区域新增 */
|
|
343
|
+
private heightLimitAdd($el: HTMLElement, maxHeight: number) {
|
|
344
|
+
if (!$el) return
|
|
345
|
+
if ($el.classList.contains('atk-height-limit')) return
|
|
346
|
+
|
|
347
|
+
$el.classList.add('atk-height-limit')
|
|
348
|
+
$el.style.height = `${maxHeight}px`
|
|
349
|
+
$el.style.overflow = 'hidden'
|
|
350
|
+
const $hideMoreOpenBtn = Utils.createElement(`<div class="atk-height-limit-btn">${this.ctx.$t('readMore')}</span>`)
|
|
351
|
+
$hideMoreOpenBtn.onclick = (e) => {
|
|
352
|
+
e.stopPropagation()
|
|
353
|
+
this.heightLimitRemove($el)
|
|
354
|
+
|
|
355
|
+
// 子评论数等于 1,直接取消限高
|
|
356
|
+
const children = this.comment.getChildren()
|
|
357
|
+
if (children.length === 1) children[0].getRender().heightLimitRemove(children[0].getRender().$content)
|
|
358
|
+
}
|
|
359
|
+
$el.append($hideMoreOpenBtn)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** 渐出动画 */
|
|
363
|
+
playFadeAnim() {
|
|
364
|
+
Ui.playFadeInAnim(this.comment.getRender().$el)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** 渐出动画 · 评论内容区域 */
|
|
368
|
+
playFadeAnimForBody() {
|
|
369
|
+
Ui.playFadeInAnim(this.comment.getRender().$body)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 获取子评论 Wrap */
|
|
373
|
+
public getChildrenWrap() {
|
|
374
|
+
return this.$childrenWrap
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** 初始化子评论区域 Wrap */
|
|
378
|
+
public renderChildrenWrap() {
|
|
379
|
+
if (!this.$childrenWrap) {
|
|
380
|
+
this.$childrenWrap = Utils.createElement('<div class="atk-comment-children"></div>')
|
|
381
|
+
this.$main.append(this.$childrenWrap)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return this.$childrenWrap
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** 恢复原有的子评论区域 Wrap */
|
|
388
|
+
public recoveryChildrenWrap() {
|
|
389
|
+
if (this.$childrenWrap) {
|
|
390
|
+
this.$main.append(this.$childrenWrap)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 设置已读 */
|
|
395
|
+
public setUnread(val: boolean) {
|
|
396
|
+
if (val) this.$el.classList.add('atk-unread')
|
|
397
|
+
else this.$el.classList.remove('atk-unread')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 设置为可点击的评论 */
|
|
401
|
+
public setOpenable(val: boolean) {
|
|
402
|
+
if (val) this.$el.classList.add('atk-openable')
|
|
403
|
+
else this.$el.classList.remove('atk-openable')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** 设置点击评论打开置顶 URL */
|
|
407
|
+
public setOpenURL(url: string) {
|
|
408
|
+
this.setOpenable(true)
|
|
409
|
+
this.$el.onclick = (evt) => {
|
|
410
|
+
evt.preventDefault()
|
|
411
|
+
window.open(url)
|
|
412
|
+
|
|
413
|
+
if (this.cConf.openEvt) this.cConf.openEvt()
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 设置点击评论时的操作 */
|
|
418
|
+
public setOpenAction(action: () => void) {
|
|
419
|
+
this.setOpenable(true)
|
|
420
|
+
this.$el.onclick = (evt) => {
|
|
421
|
+
evt.preventDefault()
|
|
422
|
+
action()
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Context from '~/types/context'
|
|
1
2
|
import * as Utils from '../lib/utils'
|
|
2
3
|
|
|
3
4
|
interface ActionBtnConf {
|
|
@@ -18,6 +19,7 @@ interface ActionBtnConf {
|
|
|
18
19
|
* 通用操作按钮
|
|
19
20
|
*/
|
|
20
21
|
export default class ActionBtn {
|
|
22
|
+
private ctx: Context
|
|
21
23
|
private conf: ActionBtnConf
|
|
22
24
|
public $el: HTMLElement
|
|
23
25
|
|
|
@@ -31,7 +33,8 @@ export default class ActionBtn {
|
|
|
31
33
|
public confirmRecTimer?: number // 确认消息复原定时器
|
|
32
34
|
|
|
33
35
|
/** 构造函数 */
|
|
34
|
-
constructor(conf: ActionBtnConf|string|(() => string)) {
|
|
36
|
+
constructor(ctx: Context, conf: ActionBtnConf|string|(() => string)) {
|
|
37
|
+
this.ctx = ctx
|
|
35
38
|
this.$el = Utils.createElement(`<span class="atk-common-action-btn"></span>`)
|
|
36
39
|
|
|
37
40
|
this.conf = (typeof conf !== 'object') ? ({ text: conf }) : conf
|
|
@@ -73,7 +76,7 @@ export default class ActionBtn {
|
|
|
73
76
|
if (!this.isConfirming) {
|
|
74
77
|
this.isConfirming = true
|
|
75
78
|
this.$el.classList.add('atk-btn-confirm')
|
|
76
|
-
this.$el.innerText = this.conf.confirmText || '
|
|
79
|
+
this.$el.innerText = this.conf.confirmText || this.ctx.$t('actionConfirm')
|
|
77
80
|
this.confirmRecTimer = window.setTimeout(() => confirmRestore(), 5000)
|
|
78
81
|
return
|
|
79
82
|
}
|
|
@@ -107,7 +110,7 @@ export default class ActionBtn {
|
|
|
107
110
|
this.isLoading = value
|
|
108
111
|
if (value) {
|
|
109
112
|
this.$el.classList.add('atk-btn-loading')
|
|
110
|
-
this.$el.innerText = loadingText || '
|
|
113
|
+
this.$el.innerText = loadingText || `${this.ctx.$t('loading')}...`
|
|
111
114
|
} else {
|
|
112
115
|
this.$el.classList.remove('atk-btn-loading')
|
|
113
116
|
this.$el.innerText = this.getText()
|
package/src/components/dialog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Context from '~/types/context'
|
|
1
2
|
import * as Utils from '../lib/utils'
|
|
2
3
|
|
|
3
4
|
type BtnClickHandler = (btnEl: HTMLElement, dialog: Dialog) => boolean|void
|
|
@@ -6,11 +7,14 @@ type BtnClickHandler = (btnEl: HTMLElement, dialog: Dialog) => boolean|void
|
|
|
6
7
|
* 对话框
|
|
7
8
|
*/
|
|
8
9
|
export default class Dialog {
|
|
10
|
+
public ctx: Context
|
|
9
11
|
public $el: HTMLElement
|
|
10
12
|
public $content: HTMLElement
|
|
11
13
|
public $actions: HTMLElement
|
|
12
14
|
|
|
13
|
-
constructor (contentEl: HTMLElement) {
|
|
15
|
+
constructor (ctx: Context, contentEl: HTMLElement) {
|
|
16
|
+
this.ctx = ctx
|
|
17
|
+
|
|
14
18
|
this.$el = Utils.createElement(
|
|
15
19
|
`<div class="atk-layer-dialog-wrap">
|
|
16
20
|
<div class="atk-layer-dialog">
|
|
@@ -33,7 +37,7 @@ export default class Dialog {
|
|
|
33
37
|
/** 按钮 · 确定 */
|
|
34
38
|
public setYes(handler: BtnClickHandler) {
|
|
35
39
|
const btn = Utils.createElement<HTMLButtonElement>(
|
|
36
|
-
|
|
40
|
+
`<button data-action="confirm">${this.ctx.$t('confirm')}</button>`
|
|
37
41
|
)
|
|
38
42
|
btn.onclick = this.onBtnClick(handler)
|
|
39
43
|
this.$actions.appendChild(btn)
|
|
@@ -44,7 +48,7 @@ export default class Dialog {
|
|
|
44
48
|
/** 按钮 · 取消 */
|
|
45
49
|
public setNo(handler: BtnClickHandler) {
|
|
46
50
|
const btn = Utils.createElement<HTMLButtonElement>(
|
|
47
|
-
|
|
51
|
+
`<button data-action="cancel">${this.ctx.$t('cancel')}</button>`
|
|
48
52
|
)
|
|
49
53
|
btn.onclick = this.onBtnClick(handler)
|
|
50
54
|
this.$actions.appendChild(btn)
|