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.
Files changed (62) hide show
  1. package/CNAME +1 -0
  2. package/dist/Artalk.css +1 -1
  3. package/dist/Artalk.es.js +509 -1571
  4. package/dist/Artalk.iife.js +1 -1
  5. package/dist/Artalk.js +1 -1
  6. package/index.html +143 -0
  7. package/package.json +8 -32
  8. package/postcss.config.js +5 -0
  9. package/src/api/index.ts +16 -3
  10. package/src/api/request.ts +4 -2
  11. package/src/artalk.ts +30 -8
  12. package/src/components/action-btn.ts +1 -1
  13. package/src/components/comment.ts +88 -3
  14. package/src/components/editor-plugs/emoticons-plug.less +21 -11
  15. package/src/components/editor-plugs/emoticons-plug.ts +18 -18
  16. package/src/components/editor-plugs/preview-plug.less +1 -1
  17. package/src/components/editor.ts +15 -20
  18. package/src/components/html/comment.html +6 -13
  19. package/src/components/html/editor.html +8 -9
  20. package/src/components/html/list.html +1 -1
  21. package/src/components/html/sidebar-layer.html +8 -0
  22. package/src/components/list-lite.ts +203 -299
  23. package/src/components/list.ts +11 -9
  24. package/src/components/pagination.ts +2 -2
  25. package/src/components/read-more-btn.ts +27 -4
  26. package/src/components/sidebar-layer.ts +143 -0
  27. package/src/defaults.ts +2 -1
  28. package/src/lib/detect.js +3 -1
  29. package/src/lib/utils.ts +27 -1
  30. package/src/style/_color.less +19 -23
  31. package/src/style/_extend.less +0 -80
  32. package/src/style/_variables.less +0 -24
  33. package/src/style/comment.less +191 -197
  34. package/src/style/editor.less +69 -94
  35. package/src/style/list.less +13 -13
  36. package/src/style/main.less +32 -32
  37. package/src/style/pagination.less +6 -6
  38. package/src/style/sidebar-layer.less +92 -0
  39. package/types/artalk-config.d.ts +4 -1
  40. package/types/artalk-data.d.ts +2 -2
  41. package/vite-common.config.ts +15 -0
  42. package/vite-demo.config.ts +14 -0
  43. package/vite.config.ts +36 -0
  44. package/LICENSE +0 -674
  45. package/README.md +0 -84
  46. package/src/components/admin/page-list.ts +0 -194
  47. package/src/components/admin/site-list-floater.ts +0 -98
  48. package/src/components/admin/site-list.ts +0 -289
  49. package/src/components/html/sidebar.html +0 -22
  50. package/src/components/item-text-editor.ts +0 -136
  51. package/src/components/sidebar-view.ts +0 -26
  52. package/src/components/sidebar-views/comments-view.ts +0 -74
  53. package/src/components/sidebar-views/pages-view.ts +0 -61
  54. package/src/components/sidebar-views/setting-view.ts +0 -21
  55. package/src/components/sidebar-views/sites-view.ts +0 -36
  56. package/src/components/sidebar-views/transfer-view.ts +0 -218
  57. package/src/components/sidebar.ts +0 -293
  58. package/src/lib/md5.js +0 -6
  59. package/src/style/item-text-editor.less +0 -35
  60. package/src/style/page-list.less +0 -50
  61. package/src/style/sidebar.less +0 -420
  62. 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",
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
- "@rollup/plugin-dynamic-import-vars": "^1.4.1",
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.3"
41
+ "marked": "^4.0.14"
66
42
  }
67
43
  }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ plugins: [
3
+ require('autoprefixer')
4
+ ]
5
+ }
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, type?: string, flatMode?: boolean, paramsEditor?: (params: any) => void) {
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: this.ctx.conf.pagination?.pageSize || 15,
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
  // ============================
@@ -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
- if (!resp.ok && resp.status !== 401) throw new Error(`请求响应 ${resp.status}`)
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 === 401) {
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 Sidebar from './components/sidebar'
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 sidebar: Sidebar
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.sidebar = new Sidebar(this.ctx)
72
- this.$root.appendChild(this.sidebar.$el)
72
+ this.sidebarLayer = new SidebarLayer(this.ctx)
73
+ this.$root.appendChild(this.sidebarLayer.$el)
73
74
 
74
75
  // 评论获取
75
- this.list.reqComments()
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
- $layerWrap.querySelectorAll<HTMLElement>(`[atk-only-admin-show]`).forEach(item => items.push(item))
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.reqComments()
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 = true, loadingText?: string) {
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-comment-main')!
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-comment-actions')!
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 data-atk-action="comment-reply">回复</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-emoticons-list-wrap {
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-emoticons-list {
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-emoticons-item {
16
- display: inline-block;
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: 60px;
35
+ max-height: 100%;
25
36
  width: auto;
26
37
  }
27
38
 
28
39
  &:hover {
29
- background: @color-bg-grey;
40
+ background: var(--at-color-bg-grey);
30
41
  }
31
42
  }
32
43
  }
33
44
  }
34
45
 
35
- .atk-emoticons-types {
46
+ & > .atk-grp-switcher {
36
47
  position: absolute;
37
48
  bottom: 0;
38
49
  left: 0;
39
50
  width: 100%;
40
- background: @color-bg-transl;
51
+ background: var(--at-color-bg-transl);
41
52
  height: 30px;
42
- border-top: 1px solid @color-border;
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: @color-bg-grey;
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 $listWrap!: HTMLElement
21
- public $types!: HTMLElement
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.$listWrap = Utils.createElement(`<div class="atk-emoticons-list-wrap"></div>`)
166
- this.$el.append(this.$listWrap)
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 emoticonsEl = Utils.createElement(`<div class="atk-emoticons-list" style="display: none;"></div>`)
170
- this.$listWrap.append(emoticonsEl)
171
- emoticonsEl.setAttribute('data-index', String(index))
172
- emoticonsEl.setAttribute('data-grp-name', grp.name)
173
- emoticonsEl.setAttribute('data-type', grp.type)
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-emoticons-item"></span>`)
176
- emoticonsEl.append($item)
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.$types = Utils.createElement(`<div class="atk-emoticons-types"></div>`)
202
- this.$el.append(this.$types)
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.$types.append($item)
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.$listWrap.children).forEach((item) => {
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.$types.querySelectorAll('span.active').forEach(item => item.classList.remove('active'))
228
- this.$types.querySelector(`span[data-index="${index}"]`)?.classList.add('active')
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.$listWrapem)
241
+ /* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
242
242
  this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
243
243
  }
244
244
 
@@ -3,5 +3,5 @@
3
3
  overflow-x: hidden;
4
4
  height: 100%;
5
5
  width: 100%;
6
- padding: @block-padding 10px + 5px;
6
+ padding: 10px (10px + 5px);
7
7
  }