artalk 2.1.3 → 2.1.6
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/CNAME +1 -0
- package/dist/Artalk.css +1 -1
- package/dist/Artalk.es.js +509 -1571
- package/dist/Artalk.iife.js +1 -1
- package/dist/Artalk.js +1 -1
- package/index.html +143 -0
- package/package.json +8 -32
- package/postcss.config.js +5 -0
- package/src/api/index.ts +16 -3
- package/src/api/request.ts +4 -2
- package/src/artalk.ts +30 -8
- package/src/components/action-btn.ts +1 -1
- package/src/components/comment.ts +88 -3
- package/src/components/editor-plugs/emoticons-plug.less +21 -11
- package/src/components/editor-plugs/emoticons-plug.ts +18 -18
- package/src/components/editor-plugs/preview-plug.less +1 -1
- package/src/components/editor.ts +15 -20
- package/src/components/html/comment.html +6 -13
- package/src/components/html/editor.html +8 -9
- package/src/components/html/list.html +1 -1
- package/src/components/html/sidebar-layer.html +8 -0
- package/src/components/list-lite.ts +203 -299
- package/src/components/list.ts +11 -9
- package/src/components/pagination.ts +2 -2
- package/src/components/read-more-btn.ts +27 -4
- package/src/components/sidebar-layer.ts +143 -0
- package/src/defaults.ts +2 -1
- package/src/lib/detect.js +3 -1
- package/src/lib/utils.ts +27 -1
- package/src/style/_color.less +19 -23
- package/src/style/_extend.less +0 -80
- package/src/style/_variables.less +0 -24
- package/src/style/comment.less +191 -197
- package/src/style/editor.less +69 -94
- package/src/style/list.less +13 -13
- package/src/style/main.less +32 -32
- package/src/style/pagination.less +6 -6
- package/src/style/sidebar-layer.less +92 -0
- package/types/artalk-config.d.ts +4 -1
- package/types/artalk-data.d.ts +2 -2
- package/vite-common.config.ts +15 -0
- package/vite-demo.config.ts +14 -0
- package/vite.config.ts +36 -0
- package/LICENSE +0 -674
- package/README.md +0 -84
- package/src/components/admin/page-list.ts +0 -194
- package/src/components/admin/site-list-floater.ts +0 -98
- package/src/components/admin/site-list.ts +0 -289
- package/src/components/html/sidebar.html +0 -22
- package/src/components/item-text-editor.ts +0 -136
- package/src/components/sidebar-view.ts +0 -26
- package/src/components/sidebar-views/comments-view.ts +0 -74
- package/src/components/sidebar-views/pages-view.ts +0 -61
- package/src/components/sidebar-views/setting-view.ts +0 -21
- package/src/components/sidebar-views/sites-view.ts +0 -36
- package/src/components/sidebar-views/transfer-view.ts +0 -218
- package/src/components/sidebar.ts +0 -293
- package/src/lib/md5.js +0 -6
- package/src/style/item-text-editor.less +0 -35
- package/src/style/page-list.less +0 -50
- package/src/style/sidebar.less +0 -420
- package/src/style/site-list.less +0 -208
package/index.html
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Artalk DEMO</title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi" />
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { padding:45px 10px;margin:0;background:#f4f4f4 }
|
|
10
|
+
body.dark-mode { background:#000; }
|
|
11
|
+
.card { margin:0 auto;max-width:700px;position:relative;overflow:hidden;background:#FFF;border-radius:4px;padding:20px;border: 1px solid transparent; }
|
|
12
|
+
body.dark-mode .card { background: #1e2223;border: 1px solid #2d3235; }
|
|
13
|
+
.card:not(:last-child) { margin-bottom:10px; }
|
|
14
|
+
.card.billboard { text-align: center; }
|
|
15
|
+
.title > a { display:block;text-decoration:none;color:#0083ff;font-size:35px;margin-top:0;margin-bottom:10px; }
|
|
16
|
+
.title > a:hover { margin-left:0 }
|
|
17
|
+
.title > a > span { opacity:0 }
|
|
18
|
+
.title > a:hover>span { opacity:1 }
|
|
19
|
+
.desc { font-size:12px;color:#757e91 }
|
|
20
|
+
.social { margin-top:20px }
|
|
21
|
+
.social > a:not(:last-child) { margin-right:2px }
|
|
22
|
+
@media only screen and (max-width: 768px) {
|
|
23
|
+
body { padding: 0;background: #FFF; }
|
|
24
|
+
.card.billboard { padding-top: 20px; }
|
|
25
|
+
.card { width: 100%;padding: 5px; }
|
|
26
|
+
}
|
|
27
|
+
.float-btn { user-select: none;bottom: 25px;display: block;cursor: pointer;text-align: center;width: 60px;height: 37px;line-height: 37px;font-size: 13px;color: #757e91;background: #fff;opacity: 0.8;border-radius: 4px;border: 1px solid #eceff2;position: fixed; }
|
|
28
|
+
.to-night { z-index: 99999;left: 25px;background: #1e2223;color: #fff; }
|
|
29
|
+
body.dark-mode .to-night { background: #fff;border: 1px solid #2d3235;color: #0083ff; }
|
|
30
|
+
.to-top { right: 25px; }
|
|
31
|
+
</style>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<div class="card billboard artalk-fade-in">
|
|
35
|
+
<div class="title"><a href="https://github.com/ArtalkJS/Artalk" target="_blank"><span>『 </span>Artalk<span> 』</span></a></div>
|
|
36
|
+
<div class="desc">一款简洁的自托管评论系统 <span id="ArtalkPV"></span></div>
|
|
37
|
+
<div class="social">
|
|
38
|
+
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/v/artalk.svg?style=flat-square"></a>
|
|
39
|
+
<a href="https://www.npmjs.com/package/artalk" target="_blank"><img src="https://img.shields.io/npm/dt/artalk.svg?style=flat-square"></a>
|
|
40
|
+
<a href="https://github.com/ArtalkJS/Artalk/commits/master" target="_blank"><img src="https://img.shields.io/github/last-commit/ArtalkJS/Artalk?style=flat-square"></a>
|
|
41
|
+
<a href="https://github.com/ArtalkJS/Artalk/blob/master/LICENSE" target="_blank"><img src="https://img.shields.io/github/license/ArtalkJS/Artalk.svg?style=flat-square"></a>
|
|
42
|
+
<a href="https://github.com/ArtalkJS/Artalk" target="_blank"><img src="https://img.shields.io/github/stars/ArtalkJS/Artalk?style=flat-square"></a>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card artalk-fade-in">
|
|
46
|
+
<div id="comments"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="float-btn to-night artalk-fade-in">Blink</div>
|
|
49
|
+
<div class="float-btn to-top artalk-fade-in" style="display: none;">TOP</div>
|
|
50
|
+
|
|
51
|
+
<script type="module">
|
|
52
|
+
import Artalk from './src/main'
|
|
53
|
+
var artalk = new Artalk({
|
|
54
|
+
el: '#comments',
|
|
55
|
+
pageKey: 'https://artalk.js.org/',
|
|
56
|
+
pageTitle: 'Artalk Home',
|
|
57
|
+
server: (import.meta.env.MODE === 'development') ? 'http://localhost:23366/api' : 'https://artalk.qwqaq.com/api',
|
|
58
|
+
site: 'ArtalkDemo',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
window.artalk = artalk;
|
|
62
|
+
|
|
63
|
+
;(function () {
|
|
64
|
+
const toTopElem = document.querySelector('.to-top')
|
|
65
|
+
toTopElem.addEventListener('click', () => {
|
|
66
|
+
window.scrollTo(0, 0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
checkShowToTop()
|
|
70
|
+
document.addEventListener('scroll', () => {
|
|
71
|
+
checkShowToTop()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function checkShowToTop() {
|
|
75
|
+
const scrollTop = window.scrollY
|
|
76
|
+
const whereShow = window.screen.height
|
|
77
|
+
const isNeedShow = (scrollTop > 0) && (scrollTop > whereShow)
|
|
78
|
+
|
|
79
|
+
if (isNeedShow) {
|
|
80
|
+
toTopElem.style.display = ''
|
|
81
|
+
} else {
|
|
82
|
+
toTopElem.style.display = 'none'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
;(function () {
|
|
88
|
+
// @Refer: https://stackoverflow.com/questions/50840168/how-to-detect-if-the-os-is-in-dark-mode-in-browsers
|
|
89
|
+
let darkMode = false
|
|
90
|
+
|
|
91
|
+
if (getLS() == "true") {
|
|
92
|
+
darkMode = true;
|
|
93
|
+
} else {
|
|
94
|
+
try {
|
|
95
|
+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
96
|
+
darkMode = true;
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getLS() {
|
|
102
|
+
return localStorage.getItem("dark_mode_on") || null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setLS(val) {
|
|
106
|
+
return localStorage.setItem("dark_mode_on", String(val))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function checkAndApplyDarkMode () {
|
|
110
|
+
artalk.setDarkMode(darkMode)
|
|
111
|
+
if (darkMode) {
|
|
112
|
+
document.body.classList.add('dark-mode')
|
|
113
|
+
} else {
|
|
114
|
+
document.body.classList.remove('dark-mode')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
checkAndApplyDarkMode()
|
|
119
|
+
|
|
120
|
+
const toNightElem = document.querySelector('.to-night')
|
|
121
|
+
toNightElem.addEventListener('click', () => {
|
|
122
|
+
darkMode = !darkMode
|
|
123
|
+
setLS(darkMode)
|
|
124
|
+
checkAndApplyDarkMode()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Listen
|
|
128
|
+
try {
|
|
129
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
|
130
|
+
const newColorScheme = e.matches ? "dark" : "light";
|
|
131
|
+
if (newColorScheme == "dark") {
|
|
132
|
+
darkMode = true
|
|
133
|
+
} else {
|
|
134
|
+
darkMode = false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
checkAndApplyDarkMode()
|
|
138
|
+
});
|
|
139
|
+
} catch {}
|
|
140
|
+
})()
|
|
141
|
+
</script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "artalk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
|
+
"license": "GPL-3.0",
|
|
4
5
|
"description": "A Selfhosted Comment System",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"comment-system",
|
|
@@ -9,24 +10,18 @@
|
|
|
9
10
|
"comments",
|
|
10
11
|
"blog"
|
|
11
12
|
],
|
|
12
|
-
"homepage": "https://artalk.js.org",
|
|
13
|
-
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
|
|
14
|
-
"license": "GPL-3.0",
|
|
15
13
|
"repository": {
|
|
16
14
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/ArtalkJS/Artalk.git"
|
|
15
|
+
"url": "https://github.com/ArtalkJS/Artalk.git",
|
|
16
|
+
"directory": "packages/artalk"
|
|
18
17
|
},
|
|
18
|
+
"homepage": "https://artalk.js.org",
|
|
19
|
+
"bugs": "https://github.com/ArtalkJS/Artalk/issue",
|
|
19
20
|
"main": "./dist/Artalk.js",
|
|
20
21
|
"unpkg": "./dist/Artalk.iife.js",
|
|
21
22
|
"jsdelivr": "./dist/Artalk.iife.js",
|
|
22
23
|
"module": "./dist/Artalk.es.js",
|
|
23
24
|
"types": "./types/index.d.ts",
|
|
24
|
-
"exports": {
|
|
25
|
-
".": {
|
|
26
|
-
"import": "./dist/Artalk.es.js",
|
|
27
|
-
"require": "./dist/Artalk.js"
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
25
|
"scripts": {
|
|
31
26
|
"dev": "vite",
|
|
32
27
|
"build": "yarn lint && vite build",
|
|
@@ -38,30 +33,11 @@
|
|
|
38
33
|
"lint": "eslint --ext .js,.ts src"
|
|
39
34
|
},
|
|
40
35
|
"devDependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@types/marked": "^4.0.1",
|
|
43
|
-
"@types/node": "^16.11.7",
|
|
44
|
-
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
|
45
|
-
"@typescript-eslint/parser": "^5.4.0",
|
|
46
|
-
"autoprefixer": "^10.4.0",
|
|
47
|
-
"eslint": "7.2.0",
|
|
48
|
-
"eslint-config-airbnb": "18.2.1",
|
|
49
|
-
"eslint-config-airbnb-typescript": "^14.0.0",
|
|
50
|
-
"eslint-config-prettier": "^8.3.0",
|
|
51
|
-
"eslint-import-resolver-typescript": "^2.4.0",
|
|
52
|
-
"eslint-plugin-import": "2.22.1",
|
|
53
|
-
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
54
|
-
"eslint-plugin-react": "^7.25.1",
|
|
55
|
-
"eslint-plugin-react-hooks": "^4.2.0",
|
|
56
|
-
"less": "^4.1.2",
|
|
57
|
-
"prettier": "^2.4.1",
|
|
58
|
-
"typescript": "^4.5.2",
|
|
59
|
-
"vite": "^2.6.14",
|
|
60
|
-
"vite-tsconfig-paths": "^3.3.17"
|
|
36
|
+
"@types/marked": "^4.0.3"
|
|
61
37
|
},
|
|
62
38
|
"dependencies": {
|
|
63
39
|
"hanabi": "^0.4.0",
|
|
64
40
|
"insane": "^2.6.2",
|
|
65
|
-
"marked": "^4.0.
|
|
41
|
+
"marked": "^4.0.14"
|
|
66
42
|
}
|
|
67
43
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Context from '../context'
|
|
2
2
|
import { CommentData, ListData, UserData, PageData, SiteData, NotifyData } from '~/types/artalk-data'
|
|
3
3
|
import { Fetch, ToFormData, POST, GET } from './request'
|
|
4
|
+
import * as Utils from '../lib/utils'
|
|
4
5
|
|
|
5
6
|
export default class Api {
|
|
6
7
|
private ctx: Context
|
|
@@ -16,15 +17,14 @@ export default class Api {
|
|
|
16
17
|
// ============================
|
|
17
18
|
|
|
18
19
|
/** 评论 · 获取 */
|
|
19
|
-
public get(offset: number,
|
|
20
|
+
public get(offset: number, pageSize: number, flatMode?: boolean, paramsEditor?: (params: any) => void) {
|
|
20
21
|
const params: any = {
|
|
21
22
|
page_key: this.ctx.conf.pageKey,
|
|
22
23
|
site_name: this.ctx.conf.site || '',
|
|
23
|
-
limit:
|
|
24
|
+
limit: pageSize,
|
|
24
25
|
offset,
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
if (type) params.type = type
|
|
28
28
|
if (flatMode) params.flat_mode = flatMode // 平铺模式
|
|
29
29
|
if (this.ctx.user.checkHasBasicUserInfo()) {
|
|
30
30
|
params.name = this.ctx.user.data.nick
|
|
@@ -45,6 +45,7 @@ export default class Api {
|
|
|
45
45
|
content: comment.content,
|
|
46
46
|
rid: comment.rid,
|
|
47
47
|
page_key: comment.page_key,
|
|
48
|
+
ua: await Utils.getCorrectUserAgent(), // 需要后端支持,获取修正后的 UA (针对 Win11)
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
if (comment.page_title) params.page_title = comment.page_title
|
|
@@ -251,6 +252,18 @@ export default class Api {
|
|
|
251
252
|
return POST(this.ctx, `${this.baseURL}/mark-read`, params)
|
|
252
253
|
}
|
|
253
254
|
|
|
255
|
+
/** PV */
|
|
256
|
+
public async pv() {
|
|
257
|
+
const params: any = {
|
|
258
|
+
site_name: this.ctx.conf.site || '',
|
|
259
|
+
page_key: this.ctx.conf.pageKey || '',
|
|
260
|
+
page_title: this.ctx.conf.pageTitle || ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const p = await POST<any>(this.ctx, `${this.baseURL}/pv`, params)
|
|
264
|
+
return p.pv as number
|
|
265
|
+
}
|
|
266
|
+
|
|
254
267
|
// ============================
|
|
255
268
|
// 验证码
|
|
256
269
|
// ============================
|
package/src/api/request.ts
CHANGED
|
@@ -21,7 +21,9 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
|
|
|
21
21
|
resp = await timeoutPromise(timeout || ctx.conf.reqTimeout || 15000, fetch(input, init))
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const noAccessCodes = [401, 400]
|
|
25
|
+
|
|
26
|
+
if (!resp.ok && !noAccessCodes.includes(resp.status)) throw new Error(`请求响应 ${resp.status}`)
|
|
25
27
|
|
|
26
28
|
// 解析获取响应的 json
|
|
27
29
|
let json: any = await resp.json()
|
|
@@ -49,7 +51,7 @@ export async function Fetch(ctx: Context, input: RequestInfo, init: RequestInit,
|
|
|
49
51
|
}
|
|
50
52
|
})
|
|
51
53
|
}))
|
|
52
|
-
} else if ((json.data && json.data.need_login) || resp.status
|
|
54
|
+
} else if ((json.data && json.data.need_login) || noAccessCodes.includes(resp.status)) {
|
|
53
55
|
// 请求需要管理员权限
|
|
54
56
|
json = await (new Promise<any>((resolve, reject) => {
|
|
55
57
|
ctx.trigger('checker-admin', {
|
package/src/artalk.ts
CHANGED
|
@@ -7,10 +7,11 @@ import defaults from './defaults'
|
|
|
7
7
|
import CheckerLauncher from './lib/checker'
|
|
8
8
|
import Editor from './components/editor'
|
|
9
9
|
import List from './components/list'
|
|
10
|
-
import
|
|
10
|
+
import SidebarLayer from './components/sidebar-layer'
|
|
11
11
|
|
|
12
12
|
import { GetLayerWrap } from './components/layer'
|
|
13
13
|
import { EventPayloadMap, Handler } from '~/types/event'
|
|
14
|
+
import Api from './api'
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Artalk
|
|
@@ -26,7 +27,7 @@ export default class Artalk {
|
|
|
26
27
|
public checkerLauncher: CheckerLauncher
|
|
27
28
|
public editor: Editor
|
|
28
29
|
public list: List
|
|
29
|
-
public
|
|
30
|
+
public sidebarLayer: SidebarLayer
|
|
30
31
|
|
|
31
32
|
constructor (customConf: ArtalkConfig) {
|
|
32
33
|
// 配置
|
|
@@ -68,14 +69,17 @@ export default class Artalk {
|
|
|
68
69
|
this.$root.appendChild(this.list.$el)
|
|
69
70
|
|
|
70
71
|
// 侧边栏
|
|
71
|
-
this.
|
|
72
|
-
this.$root.appendChild(this.
|
|
72
|
+
this.sidebarLayer = new SidebarLayer(this.ctx)
|
|
73
|
+
this.$root.appendChild(this.sidebarLayer.$el)
|
|
73
74
|
|
|
74
75
|
// 评论获取
|
|
75
|
-
this.list.
|
|
76
|
+
this.list.fetchComments(0)
|
|
76
77
|
|
|
77
78
|
// 事件绑定初始化
|
|
78
79
|
this.initEventBind()
|
|
80
|
+
|
|
81
|
+
// 其他
|
|
82
|
+
this.initPV()
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
/** 事件绑定 · 初始化 */
|
|
@@ -93,8 +97,12 @@ export default class Artalk {
|
|
|
93
97
|
|
|
94
98
|
// for layer
|
|
95
99
|
const { $wrap: $layerWrap } = GetLayerWrap(this.ctx)
|
|
96
|
-
if ($layerWrap)
|
|
97
|
-
|
|
100
|
+
if ($layerWrap) $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
|
|
101
|
+
|
|
102
|
+
// for sidebar
|
|
103
|
+
// TODO: 这个其实应该写在 packages/artalk-sidebar 里面的
|
|
104
|
+
const $sidebarEl = document.querySelector<HTMLElement>('.atk-sidebar')
|
|
105
|
+
if ($sidebarEl) $sidebarEl.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
|
|
98
106
|
|
|
99
107
|
items.forEach(($item: HTMLElement) => {
|
|
100
108
|
if (this.ctx.user.data.isAdmin)
|
|
@@ -113,7 +121,7 @@ export default class Artalk {
|
|
|
113
121
|
|
|
114
122
|
/** 重新加载 */
|
|
115
123
|
public reload() {
|
|
116
|
-
this.list.
|
|
124
|
+
this.list.fetchComments(0)
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/** 暗黑模式 · 初始化 */
|
|
@@ -143,6 +151,20 @@ export default class Artalk {
|
|
|
143
151
|
this.initDarkMode()
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/** PV */
|
|
155
|
+
public async initPV() {
|
|
156
|
+
if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
|
|
160
|
+
// $pv.innerText = '-' // 默认占位符
|
|
161
|
+
const pvNum = await new Api(this.ctx).pv()
|
|
162
|
+
if (Number.isNaN(Number(pvNum)))
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
$pv.innerText = String(pvNum)
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
/** 监听事件 */
|
|
147
169
|
public on<K extends keyof EventPayloadMap>(name: K, handler: Handler<EventPayloadMap[K]>) {
|
|
148
170
|
this.ctx.on(name, handler, 'external')
|
|
@@ -102,7 +102,7 @@ export default class ActionBtn {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** 设置加载状态 */
|
|
105
|
-
public setLoading(value
|
|
105
|
+
public setLoading(value: boolean, loadingText?: string) {
|
|
106
106
|
if (this.isLoading === value) return
|
|
107
107
|
this.isLoading = value
|
|
108
108
|
if (value) {
|
|
@@ -55,11 +55,11 @@ export default class Comment extends Component {
|
|
|
55
55
|
/** 渲染 UI */
|
|
56
56
|
public render() {
|
|
57
57
|
this.$el = Utils.createElement(CommentHTML)
|
|
58
|
-
this.$main = this.$el.querySelector('.atk-
|
|
58
|
+
this.$main = this.$el.querySelector('.atk-main')!
|
|
59
59
|
this.$header = this.$el.querySelector('.atk-header')!
|
|
60
60
|
this.$body = this.$el.querySelector('.atk-body')!
|
|
61
61
|
this.$content = this.$body.querySelector('.atk-content')!
|
|
62
|
-
this.$actions = this.$el.querySelector('.atk-
|
|
62
|
+
this.$actions = this.$el.querySelector('.atk-actions')!
|
|
63
63
|
this.$children = null
|
|
64
64
|
|
|
65
65
|
this.$el.setAttribute('data-comment-id', `${this.data.id}`)
|
|
@@ -229,7 +229,7 @@ export default class Comment extends Component {
|
|
|
229
229
|
|
|
230
230
|
// 绑定回复按钮事件
|
|
231
231
|
if (this.data.is_allow_reply) {
|
|
232
|
-
const replyBtn = Utils.createElement(`<span
|
|
232
|
+
const replyBtn = Utils.createElement(`<span>回复</span>`)
|
|
233
233
|
this.$actions.append(replyBtn)
|
|
234
234
|
replyBtn.addEventListener('click', (e) => {
|
|
235
235
|
e.stopPropagation() // 防止穿透
|
|
@@ -316,6 +316,9 @@ export default class Comment extends Component {
|
|
|
316
316
|
|
|
317
317
|
this.getChildrenEl().appendChild(childC.getEl())
|
|
318
318
|
childC.playFadeInAnim()
|
|
319
|
+
|
|
320
|
+
// 内容限高
|
|
321
|
+
childC.checkHeightLimitArea('content')
|
|
319
322
|
}
|
|
320
323
|
|
|
321
324
|
getChildrenEl() {
|
|
@@ -465,4 +468,86 @@ export default class Comment extends Component {
|
|
|
465
468
|
this.openURL = url
|
|
466
469
|
this.$el.classList.add('atk-openable')
|
|
467
470
|
}
|
|
471
|
+
|
|
472
|
+
/** 内容限高检测 */
|
|
473
|
+
checkHeightLimit() {
|
|
474
|
+
this.checkHeightLimitArea('content') // 评论内容限高
|
|
475
|
+
this.checkHeightLimitArea('children') // 子评论部分限高(嵌套模式)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** 目标内容限高检测 */
|
|
479
|
+
checkHeightLimitArea(area: 'children'|'content') {
|
|
480
|
+
// 参数准备
|
|
481
|
+
const childrenMaxH = this.ctx.conf.heightLimit?.children
|
|
482
|
+
const contentMaxH = this.ctx.conf.heightLimit?.content
|
|
483
|
+
|
|
484
|
+
if (area === 'children' && !childrenMaxH) return
|
|
485
|
+
if (area === 'content' && !contentMaxH) return
|
|
486
|
+
|
|
487
|
+
// 限高
|
|
488
|
+
let maxHeight: number
|
|
489
|
+
if (area === 'children') maxHeight = childrenMaxH!
|
|
490
|
+
if (area === 'content') maxHeight = contentMaxH!
|
|
491
|
+
|
|
492
|
+
// 检测指定元素
|
|
493
|
+
const checkEl = ($el?: HTMLElement|null) => {
|
|
494
|
+
if (!$el) return
|
|
495
|
+
|
|
496
|
+
// 是否超过高度
|
|
497
|
+
if (Utils.getHeight($el) > maxHeight) {
|
|
498
|
+
this.heightLimitAdd($el, maxHeight)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 执行限高检测
|
|
503
|
+
if (area === 'children') {
|
|
504
|
+
checkEl(this.$children)
|
|
505
|
+
} else if (area === 'content') {
|
|
506
|
+
checkEl(this.$content)
|
|
507
|
+
checkEl(this.$replyTo)
|
|
508
|
+
|
|
509
|
+
// 若有图片 · 图片加载完后再检测一次
|
|
510
|
+
Utils.onImagesLoaded(this.$content, () => {
|
|
511
|
+
checkEl(this.$content)
|
|
512
|
+
})
|
|
513
|
+
if (this.$replyTo) {
|
|
514
|
+
Utils.onImagesLoaded(this.$replyTo, () => {
|
|
515
|
+
checkEl(this.$replyTo)
|
|
516
|
+
})
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 操作 · 取消限高
|
|
522
|
+
heightLimitRemove($el: HTMLElement) {
|
|
523
|
+
if (!$el) return
|
|
524
|
+
if (!$el.classList.contains('atk-height-limit')) return
|
|
525
|
+
|
|
526
|
+
$el.classList.remove('atk-height-limit')
|
|
527
|
+
Array.from($el.children).forEach((e) => {
|
|
528
|
+
if (e.classList.contains('atk-height-limit-btn')) e.remove()
|
|
529
|
+
})
|
|
530
|
+
$el.style.height = ''
|
|
531
|
+
$el.style.overflow = ''
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 操作 · 内容限高
|
|
535
|
+
heightLimitAdd($el: HTMLElement, maxHeight: number) {
|
|
536
|
+
if (!$el) return
|
|
537
|
+
if ($el.classList.contains('atk-height-limit')) return
|
|
538
|
+
|
|
539
|
+
$el.classList.add('atk-height-limit')
|
|
540
|
+
$el.style.height = `${maxHeight}px`
|
|
541
|
+
$el.style.overflow = 'hidden'
|
|
542
|
+
const $hideMoreOpenBtn = Utils.createElement(`<div class="atk-height-limit-btn">阅读更多</span>`)
|
|
543
|
+
$hideMoreOpenBtn.onclick = (e) => {
|
|
544
|
+
e.stopPropagation()
|
|
545
|
+
this.heightLimitRemove($el)
|
|
546
|
+
|
|
547
|
+
// 子评论数等于 1,直接取消限高
|
|
548
|
+
const children = this.getChildren()
|
|
549
|
+
if (children.length === 1) children[0].heightLimitRemove(children[0].$content)
|
|
550
|
+
}
|
|
551
|
+
$el.append($hideMoreOpenBtn)
|
|
552
|
+
}
|
|
468
553
|
}
|
|
@@ -2,44 +2,55 @@
|
|
|
2
2
|
height: 100%;
|
|
3
3
|
width: 100%;
|
|
4
4
|
|
|
5
|
-
.atk-
|
|
5
|
+
& > .atk-grp-wrap {
|
|
6
6
|
.atk-slim-scrollbar();
|
|
7
7
|
overflow-y: scroll;
|
|
8
8
|
overflow-x: hidden;
|
|
9
9
|
height: 100%; // calc(100% - 30px);
|
|
10
10
|
width: 100%;
|
|
11
11
|
|
|
12
|
-
.atk-
|
|
12
|
+
& > .atk-grp {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
flex-direction: row;
|
|
13
16
|
padding: 5px 10px 35px 10px;
|
|
14
17
|
|
|
15
|
-
.atk-
|
|
16
|
-
|
|
18
|
+
&[data-type="image"] > .atk-item {
|
|
19
|
+
height: 63px;
|
|
20
|
+
width: 63px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
& > .atk-item {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
17
27
|
padding: 5px;
|
|
18
28
|
cursor: pointer;
|
|
19
29
|
user-select: none;
|
|
20
30
|
border-radius: 2px;
|
|
21
31
|
font-size: 16px;
|
|
32
|
+
min-width: 35px;
|
|
22
33
|
|
|
23
34
|
& > img {
|
|
24
|
-
max-height:
|
|
35
|
+
max-height: 100%;
|
|
25
36
|
width: auto;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
&:hover {
|
|
29
|
-
background:
|
|
40
|
+
background: var(--at-color-bg-grey);
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
.atk-
|
|
46
|
+
& > .atk-grp-switcher {
|
|
36
47
|
position: absolute;
|
|
37
48
|
bottom: 0;
|
|
38
49
|
left: 0;
|
|
39
50
|
width: 100%;
|
|
40
|
-
background:
|
|
51
|
+
background: var(--at-color-bg-transl);
|
|
41
52
|
height: 30px;
|
|
42
|
-
border-top: 1px solid
|
|
53
|
+
border-top: 1px solid var(--at-color-border);
|
|
43
54
|
|
|
44
55
|
& > span {
|
|
45
56
|
user-select: none;
|
|
@@ -52,9 +63,8 @@
|
|
|
52
63
|
|
|
53
64
|
&:hover,
|
|
54
65
|
&.active {
|
|
55
|
-
background:
|
|
66
|
+
background: var(--at-color-bg-grey);
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
|
-
|
|
60
70
|
}
|
|
@@ -17,8 +17,8 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
17
17
|
public $el!: HTMLElement
|
|
18
18
|
public emoticons: EmoticonListData = []
|
|
19
19
|
|
|
20
|
-
public $
|
|
21
|
-
public $
|
|
20
|
+
public $grpWrap!: HTMLElement
|
|
21
|
+
public $grpSwitcher!: HTMLElement
|
|
22
22
|
|
|
23
23
|
constructor (public editor: Editor) {
|
|
24
24
|
super(editor)
|
|
@@ -162,18 +162,18 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
162
162
|
/** 初始化表情列表界面 */
|
|
163
163
|
initEmoticonsList () {
|
|
164
164
|
// 表情列表
|
|
165
|
-
this.$
|
|
166
|
-
this.$el.append(this.$
|
|
165
|
+
this.$grpWrap = Utils.createElement(`<div class="atk-grp-wrap"></div>`)
|
|
166
|
+
this.$el.append(this.$grpWrap)
|
|
167
167
|
|
|
168
168
|
this.emoticons.forEach((grp, index) => {
|
|
169
|
-
const
|
|
170
|
-
this.$
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
const $grp = Utils.createElement(`<div class="atk-grp" style="display: none;"></div>`)
|
|
170
|
+
this.$grpWrap.append($grp)
|
|
171
|
+
$grp.setAttribute('data-index', String(index))
|
|
172
|
+
$grp.setAttribute('data-grp-name', grp.name)
|
|
173
|
+
$grp.setAttribute('data-type', grp.type)
|
|
174
174
|
grp.items.forEach((item) => {
|
|
175
|
-
const $item = Utils.createElement(`<span class="atk-
|
|
176
|
-
|
|
175
|
+
const $item = Utils.createElement(`<span class="atk-item"></span>`)
|
|
176
|
+
$grp.append($item)
|
|
177
177
|
|
|
178
178
|
if (!!item.key && !(new RegExp(`^(${grp.name})?\\s?[0-9]+$`).test(item.key)))
|
|
179
179
|
$item.setAttribute('title', item.key)
|
|
@@ -198,14 +198,14 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
198
198
|
})
|
|
199
199
|
|
|
200
200
|
// 表情分类
|
|
201
|
-
this.$
|
|
202
|
-
this.$el.append(this.$
|
|
201
|
+
this.$grpSwitcher = Utils.createElement(`<div class="atk-grp-switcher"></div>`)
|
|
202
|
+
this.$el.append(this.$grpSwitcher)
|
|
203
203
|
this.emoticons.forEach((grp, index) => {
|
|
204
204
|
const $item = Utils.createElement('<span />')
|
|
205
205
|
$item.innerText = grp.name
|
|
206
206
|
$item.setAttribute('data-index', String(index))
|
|
207
207
|
$item.onclick = () => (this.openGrp(index))
|
|
208
|
-
this.$
|
|
208
|
+
this.$grpSwitcher.append($item)
|
|
209
209
|
})
|
|
210
210
|
|
|
211
211
|
// 默认打开第一个分类
|
|
@@ -215,7 +215,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
215
215
|
|
|
216
216
|
/** 打开一个表情组 */
|
|
217
217
|
openGrp (index: number) {
|
|
218
|
-
Array.from(this.$
|
|
218
|
+
Array.from(this.$grpWrap.children).forEach((item) => {
|
|
219
219
|
const el = item as HTMLElement
|
|
220
220
|
if (el.getAttribute('data-index') !== String(index)) {
|
|
221
221
|
el.style.display = 'none'
|
|
@@ -224,8 +224,8 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
224
224
|
}
|
|
225
225
|
})
|
|
226
226
|
|
|
227
|
-
this.$
|
|
228
|
-
this.$
|
|
227
|
+
this.$grpSwitcher.querySelectorAll('span.active').forEach(item => item.classList.remove('active'))
|
|
228
|
+
this.$grpSwitcher.querySelector(`span[data-index="${index}"]`)?.classList.add('active')
|
|
229
229
|
|
|
230
230
|
this.changeListHeight()
|
|
231
231
|
}
|
|
@@ -238,7 +238,7 @@ export default class EmoticonsPlug extends EditorPlug {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
changeListHeight () {
|
|
241
|
-
/* const listWrapHeight = Utils.getHeight(this.$
|
|
241
|
+
/* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
|
|
242
242
|
this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
|
|
243
243
|
}
|
|
244
244
|
|