artalk 2.1.0 → 2.1.4

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 (45) hide show
  1. package/dist/Artalk.css +1 -1
  2. package/dist/Artalk.es.js +3895 -3106
  3. package/dist/Artalk.iife.js +1 -266
  4. package/dist/Artalk.js +1 -266
  5. package/package.json +19 -21
  6. package/src/api/index.ts +6 -7
  7. package/src/artalk.ts +11 -4
  8. package/src/components/action-btn.ts +1 -1
  9. package/src/components/comment.ts +102 -27
  10. package/src/components/editor-plugs/_example-plug.ts.txt +2 -7
  11. package/src/components/editor-plugs/editor-plug.ts +3 -3
  12. package/src/components/editor-plugs/emoticons-plug.less +21 -11
  13. package/src/components/editor-plugs/emoticons-plug.ts +199 -79
  14. package/src/components/editor-plugs/preview-plug.less +1 -1
  15. package/src/components/editor-plugs/preview-plug.ts +2 -7
  16. package/src/components/editor.ts +75 -34
  17. package/src/components/html/comment.html +6 -13
  18. package/src/components/html/editor.html +9 -10
  19. package/src/components/html/list.html +1 -1
  20. package/src/components/layer.ts +33 -14
  21. package/src/components/list-lite.ts +218 -235
  22. package/src/components/list.ts +11 -9
  23. package/src/components/pagination.ts +2 -2
  24. package/src/components/read-more-btn.ts +27 -10
  25. package/src/components/sidebar-views/comments-view.ts +12 -5
  26. package/src/components/sidebar-views/sites-view.ts +0 -6
  27. package/src/components/sidebar.ts +7 -2
  28. package/src/{default-conf.ts → defaults.ts} +7 -7
  29. package/src/lib/utils.ts +41 -12
  30. package/src/style/_color.less +19 -23
  31. package/src/style/_extend.less +3 -3
  32. package/src/style/_variables.less +0 -24
  33. package/src/style/comment.less +191 -197
  34. package/src/style/editor.less +78 -103
  35. package/src/style/item-text-editor.less +2 -2
  36. package/src/style/list.less +14 -13
  37. package/src/style/main.less +35 -31
  38. package/src/style/page-list.less +4 -4
  39. package/src/style/pagination.less +6 -6
  40. package/src/style/sidebar.less +35 -35
  41. package/src/style/site-list.less +13 -13
  42. package/types/artalk-config.d.ts +13 -10
  43. package/types/artalk-data.d.ts +10 -5
  44. package/types/event.d.ts +10 -1
  45. package/src/assets/emoticons.json +0 -100
@@ -3,126 +3,242 @@ import './emoticons-plug.less'
3
3
  import Editor from '../editor'
4
4
  import EditorPlug from './editor-plug'
5
5
  import * as Utils from '~/src/lib/utils'
6
+ import * as Ui from '~/src/lib/ui'
7
+ import { EmoticonListData, EmoticonGrpData } from '~/types/artalk-data'
6
8
 
7
- type EmoticonListType = {
8
- [grpName: string]: {
9
- inputType: 'emoticon'|'image'
10
- container: { [name: string]: string }
9
+ type OwOFormatType = {
10
+ [key: string] : {
11
+ type: 'emoticon'|'emoji'|'image',
12
+ container: {icon: string, text: string}[]
11
13
  }
12
14
  }
13
15
 
14
16
  export default class EmoticonsPlug extends EditorPlug {
15
17
  public $el!: HTMLElement
16
- public emoticons: EmoticonListType
18
+ public emoticons: EmoticonListData = []
17
19
 
18
- public listWrapEl!: HTMLElement
19
- public typesEl!: HTMLElement
20
+ public $grpWrap!: HTMLElement
21
+ public $grpSwitcher!: HTMLElement
20
22
 
21
23
  constructor (public editor: Editor) {
22
24
  super(editor)
23
25
 
24
- this.emoticons = this.ctx.conf.emoticons
25
- this.initEl()
26
+ this.$el = Utils.createElement(`<div class="atk-editor-plug-emoticons"></div>`)
27
+
28
+ this.init()
26
29
  }
27
30
 
28
- initEl () {
29
- this.$el = Utils.createElement(`<div class="atk-editor-plug-emoticons"></div>`)
31
+ async init() {
32
+ // 数据处理
33
+ Ui.showLoading(this.$el)
34
+ this.emoticons = await this.handleData(this.ctx.conf.emoticons)
35
+ Ui.hideLoading(this.$el)
30
36
 
31
- // 表情列表
32
- this.listWrapEl = Utils.createElement(`<div class="atk-emoticons-list-wrap"></div>`)
33
- this.$el.append(this.listWrapEl)
34
-
35
- Object.entries(this.emoticons).forEach(([key, item]) => {
36
- const emoticonsEl = Utils.createElement(`<div class="atk-emoticons-list" style="display: none;"></div>`)
37
- this.listWrapEl.append(emoticonsEl)
38
- emoticonsEl.setAttribute('data-key', key)
39
- emoticonsEl.setAttribute('data-input-type', item.inputType)
40
- Object.entries(item.container).forEach(([name, content]) => {
41
- const itemEl = Utils.createElement(`<span class="atk-emoticons-item"></span>`)
42
- emoticonsEl.append(itemEl)
43
- itemEl.setAttribute('title', name)
44
- itemEl.setAttribute('data-content', content)
45
- if (item.inputType === 'image') {
46
- const imgEl = document.createElement('img')
47
- imgEl.src = content
48
- imgEl.alt = name
49
- itemEl.append(imgEl)
50
- } else {
51
- itemEl.innerText = content
37
+ // 初始化元素
38
+ this.initEmoticonsList()
39
+ }
40
+
41
+ async handleData(data: any): Promise<any[]> {
42
+ if (!Array.isArray(data) && ['object', 'string'].includes(typeof data)) {
43
+ data = [data]
44
+ }
45
+
46
+ if (!Array.isArray(data)) {
47
+ Ui.setError(this.$el, "表情包数据必须为 Array/Object/String 类型")
48
+ Ui.hideLoading(this.$el)
49
+ return []
50
+ }
51
+
52
+ const pushGrp = (grp: EmoticonGrpData) => {
53
+ if (typeof grp !== 'object') return
54
+ if (grp.name && data.find(o => o.name === grp.name)) return
55
+ data.push(grp)
56
+ }
57
+
58
+ // 加载子内容
59
+ const remoteLoad = async (d: any[]) => {
60
+ await Promise.all(d.map(async (grp, index) => {
61
+ if (typeof grp === 'object' && !Array.isArray(grp)) {
62
+ pushGrp(grp)
63
+ } else if (Array.isArray(grp)) {
64
+ await remoteLoad(grp)
65
+ } else if (typeof grp === "string") {
66
+ const grpData = await this.remoteLoad(grp)
67
+
68
+ if (Array.isArray(grpData)) await remoteLoad(grpData)
69
+ else if (typeof grpData === 'object') pushGrp(grpData)
52
70
  }
71
+ }))
72
+ }
73
+ await remoteLoad(data)
74
+
75
+ // 处理数组数据
76
+ data.forEach((item: any) => {
77
+ if (this.isOwOFormat(item)) {
78
+ const c = this.convertOwO(item)
79
+ c.forEach((grp) => { pushGrp(grp) })
80
+ } else if (Array.isArray(item)) {
81
+ item.forEach((grp) => { pushGrp(grp) })
82
+ }
83
+ })
84
+
85
+ // 剔除非法数据
86
+ data = data.filter((item: any) => (typeof item === 'object' && !Array.isArray(item) && !!item && !!item.name))
87
+
88
+ console.log(data)
89
+
90
+ this.solveNullKey(data)
91
+ this.solveSameKey(data)
92
+
93
+ return data
94
+ }
95
+
96
+ /** 远程加载 */
97
+ async remoteLoad(url: string): Promise<any> {
98
+ if (!url) return []
99
+
100
+ try {
101
+ const resp = await fetch(url)
102
+ const json = await resp.json()
103
+ return json
104
+ } catch (err) {
105
+ Ui.hideLoading(this.$el)
106
+ Ui.setError(this.$el, `表情加载失败 ${String(err)}`)
107
+ return []
108
+ }
109
+ }
110
+
111
+ /** 避免表情 item.key 为 null 的情况 */
112
+ solveNullKey(data: EmoticonGrpData[]) {
113
+ data.forEach((grp) => {
114
+ grp.items.forEach((item, index) => {
115
+ if (!item.key) item.key = `${grp.name} ${index+1}`
53
116
  })
54
117
  })
118
+ }
55
119
 
56
- // 表情分类
57
- this.typesEl = Utils.createElement(`<div class="atk-emoticons-types"></div>`)
58
- this.$el.append(this.typesEl)
59
- Object.entries(this.emoticons).forEach(([key, item]) => {
60
- const itemEl = Utils.createElement('<span />')
61
- this.typesEl.append(itemEl)
62
- itemEl.setAttribute('data-key', key)
63
- itemEl.innerText = key
120
+ /** 避免相同 item.key */
121
+ solveSameKey(data: EmoticonGrpData[]) {
122
+ const tmp: {[key: string]: number} = {}
123
+ data.forEach((grp) => {
124
+ grp.items.forEach(item => {
125
+ if (!item.key || String(item.key).trim() === "") return
126
+ if (!tmp[item.key]) tmp[item.key] = 1
127
+ else tmp[item.key]++
128
+
129
+ if (tmp[item.key] > 1) item.key = `${item.key} ${tmp[item.key]}`
130
+ })
64
131
  })
132
+ }
65
133
 
66
- // 绑定切换分类按钮
67
- this.typesEl.querySelectorAll('span').forEach((btn) => {
68
- btn.addEventListener('click', (evt) => {
69
- const btnEl = evt.currentTarget as HTMLElement
70
- const key = btnEl.getAttribute('data-key')
71
- if (key) this.openType(key)
134
+ /** 判断是否为 OwO 格式 */
135
+ isOwOFormat(data: any) {
136
+ try {
137
+ return (typeof data === 'object') && !!Object.values(data).length
138
+ && Array.isArray(Object.keys(Object.values<any>(data)[0].container))
139
+ && Object.keys(Object.values<any>(data)[0].container[0]).includes('icon')
140
+ } catch { return false }
141
+ }
142
+
143
+ /** 转换 OwO 格式数据 */
144
+ convertOwO(owoData: OwOFormatType): EmoticonListData {
145
+ const dest: EmoticonListData = []
146
+ Object.entries(owoData).forEach(([grpName, grp]) => {
147
+ const nGrp: EmoticonGrpData = { name: grpName, type: grp.type, items: [] }
148
+ grp.container.forEach((item, index) => {
149
+ // 图片标签提取 src 属性值
150
+ const iconStr = item.icon
151
+ if (/<(img|IMG)/.test(iconStr)) {
152
+ const find = /src=["'](.*?)["']/.exec(iconStr)
153
+ if (find && find.length > 1) item.icon = find[1]
154
+ }
155
+ nGrp.items.push({ key: item.text || `${grpName} ${index+1}`, val: item.icon })
72
156
  })
157
+ dest.push(nGrp)
73
158
  })
159
+ return dest
160
+ }
74
161
 
75
- // 默认打开第一个分类
76
- if (Object.keys(this.emoticons).length > 0)
77
- this.openType(Object.keys(this.emoticons)[0])
78
-
79
- // 绑定点击表情
80
- this.listWrapEl.querySelectorAll<HTMLElement>('.atk-emoticons-item').forEach((item: HTMLElement) => {
81
- item.onclick = (evt) => {
82
- const elem = evt.currentTarget as HTMLElement
83
- const inputType = elem.closest('.atk-emoticons-list')!.getAttribute('data-input-type')
84
-
85
- const title = elem.getAttribute('title')
86
- const content = elem.getAttribute('data-content')
87
- if (inputType === 'image') {
88
- this.editor.insertContent(`:[${title}]`)
162
+ /** 初始化表情列表界面 */
163
+ initEmoticonsList () {
164
+ // 表情列表
165
+ this.$grpWrap = Utils.createElement(`<div class="atk-grp-wrap"></div>`)
166
+ this.$el.append(this.$grpWrap)
167
+
168
+ this.emoticons.forEach((grp, index) => {
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
+ grp.items.forEach((item) => {
175
+ const $item = Utils.createElement(`<span class="atk-item"></span>`)
176
+ $grp.append($item)
177
+
178
+ if (!!item.key && !(new RegExp(`^(${grp.name})?\\s?[0-9]+$`).test(item.key)))
179
+ $item.setAttribute('title', item.key)
180
+
181
+ if (grp.type === 'image') {
182
+ const imgEl = document.createElement('img')
183
+ imgEl.src = item.val
184
+ imgEl.alt = item.key
185
+ $item.append(imgEl)
89
186
  } else {
90
- this.editor.insertContent(content || '')
187
+ $item.innerText = item.val
91
188
  }
92
- }
189
+
190
+ $item.onclick = () => {
191
+ if (grp.type === 'image') {
192
+ this.editor.insertContent(`:[${item.key}]`)
193
+ } else {
194
+ this.editor.insertContent(item.val || '')
195
+ }
196
+ }
197
+ })
198
+ })
199
+
200
+ // 表情分类
201
+ this.$grpSwitcher = Utils.createElement(`<div class="atk-grp-switcher"></div>`)
202
+ this.$el.append(this.$grpSwitcher)
203
+ this.emoticons.forEach((grp, index) => {
204
+ const $item = Utils.createElement('<span />')
205
+ $item.innerText = grp.name
206
+ $item.setAttribute('data-index', String(index))
207
+ $item.onclick = () => (this.openGrp(index))
208
+ this.$grpSwitcher.append($item)
93
209
  })
210
+
211
+ // 默认打开第一个分类
212
+ if (this.emoticons.length > 0)
213
+ this.openGrp(0)
94
214
  }
95
215
 
96
- openType (key: string) {
97
- Array.from(this.listWrapEl.children).forEach((item) => {
216
+ /** 打开一个表情组 */
217
+ openGrp (index: number) {
218
+ Array.from(this.$grpWrap.children).forEach((item) => {
98
219
  const el = item as HTMLElement
99
- if (el.getAttribute('data-key') !== key) {
220
+ if (el.getAttribute('data-index') !== String(index)) {
100
221
  el.style.display = 'none'
101
222
  } else {
102
223
  el.style.display = ''
103
224
  }
104
225
  })
105
226
 
106
- this.typesEl.querySelectorAll('span.active').forEach(item => item.classList.remove('active'))
107
- this.typesEl.querySelector(`span[data-key="${key}"]`)?.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')
108
229
 
109
230
  this.changeListHeight()
110
231
  }
111
232
 
112
- getName () {
113
- return 'emoticons'
114
- }
115
-
116
- getBtnHtml () {
117
- return '表情'
118
- }
233
+ static Name = 'emoticons'
234
+ static BtnHTML = '表情'
119
235
 
120
236
  getEl () {
121
237
  return this.$el
122
238
  }
123
239
 
124
240
  changeListHeight () {
125
- /* const listWrapHeight = Utils.getHeight(this.listWrapElem)
241
+ /* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
126
242
  this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
127
243
  }
128
244
 
@@ -137,11 +253,15 @@ export default class EmoticonsPlug extends EditorPlug {
137
253
  this.$el.parentElement!.style.height = ''
138
254
  }
139
255
 
256
+ /** 处理评论 content 中的表情内容 */
140
257
  public transEmoticonImageText (text: string) {
141
- Object.entries(this.emoticons).forEach(([grpName, grp]) => {
142
- if (grp.inputType !== 'image') return
143
- Object.entries(grp.container).forEach(([name, imgSrc]) => {
144
- text = text.split(`:[${name}]`).join(`![${name}](${imgSrc}) `) // replaceAll(...)
258
+ if (!this.emoticons || !Array.isArray(this.emoticons))
259
+ return text
260
+
261
+ this.emoticons.forEach((grp) => {
262
+ if (grp.type !== 'image') return
263
+ Object.entries(grp.items).forEach(([index, item]) => {
264
+ text = text.split(`:[${item.key}]`).join(`<img src="${item.val}" atk-emoticon="${item.key}">`) // replaceAll(...)
145
265
  })
146
266
  })
147
267
 
@@ -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
  }
@@ -19,13 +19,8 @@ export default class PreviewPlug extends EditorPlug {
19
19
  this.binded = false
20
20
  }
21
21
 
22
- getName () {
23
- return 'preview'
24
- }
25
-
26
- getBtnHtml () {
27
- return '预览 <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>'
28
- }
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>'
29
24
 
30
25
  getEl () {
31
26
  return this.$el
@@ -18,18 +18,17 @@ export default class Editor extends Component {
18
18
  public $header: HTMLElement
19
19
  public $textareaWrap: HTMLElement
20
20
  public $textarea: HTMLTextAreaElement
21
- public $closeComment: HTMLTextAreaElement
22
21
  public $plugWrap: HTMLElement
23
22
  public $bottom: HTMLElement
24
- public $bottomPartLeft: HTMLElement
25
- public $plugSwitcherWrap: HTMLElement
26
- public $bottomPartRight: HTMLElement
23
+ public $plugBtnWrap: HTMLElement
27
24
  public $submitBtn: HTMLButtonElement
28
25
  public $notifyWrap: HTMLElement
29
26
 
30
27
  private replyComment: CommentData|null = null
31
28
  private $sendReply: HTMLElement|null = null
32
29
 
30
+ private isTraveling = false
31
+
33
32
  private get user () {
34
33
  return this.ctx.user
35
34
  }
@@ -39,17 +38,14 @@ export default class Editor extends Component {
39
38
 
40
39
  this.$el = Utils.createElement(EditorHTML)
41
40
 
42
- this.$header = this.$el.querySelector('.atk-editor-header')!
43
- this.$textareaWrap = this.$el.querySelector('.atk-editor-textarea-wrap')!
44
- this.$textarea = this.$el.querySelector('.atk-editor-textarea')!
45
- this.$closeComment = this.$el.querySelector('.atk-close-comment')!
46
- this.$plugWrap = this.$el.querySelector('.atk-editor-plug-wrap')!
47
- this.$bottom = this.$el.querySelector('.atk-editor-bottom')!
48
- this.$bottomPartLeft = this.$el.querySelector('.atk-editor-bottom-part.atk-left')!
49
- this.$plugSwitcherWrap = this.$el.querySelector('.atk-editor-plug-switcher-wrap')!
50
- this.$bottomPartRight = this.$el.querySelector('.atk-editor-bottom-part.atk-right')!
41
+ this.$header = this.$el.querySelector('.atk-header')!
42
+ this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
43
+ this.$textarea = this.$el.querySelector('.atk-textarea')!
44
+ this.$plugWrap = this.$el.querySelector('.atk-plug-wrap')!
45
+ this.$bottom = this.$el.querySelector('.atk-bottom')!
46
+ this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
51
47
  this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
52
- this.$notifyWrap = this.$el.querySelector('.atk-editor-notify-wrap')!
48
+ this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
53
49
 
54
50
  this.initLocalStorage()
55
51
  this.initHeader()
@@ -60,10 +56,13 @@ export default class Editor extends Component {
60
56
  // 监听事件
61
57
  this.ctx.on('editor-open', () => (this.open()))
62
58
  this.ctx.on('editor-close', () => (this.close()))
63
- this.ctx.on('editor-reply', (commentData) => (this.setReply(commentData)))
59
+ this.ctx.on('editor-reply', (p) => (this.setReply(p.data, p.$el, p.scroll)))
60
+ this.ctx.on('editor-reply-cancel', () => (this.cancelReply()))
64
61
  this.ctx.on('editor-show-loading', () => (Ui.showLoading(this.$el)))
65
62
  this.ctx.on('editor-hide-loading', () => (Ui.hideLoading(this.$el)))
66
63
  this.ctx.on('editor-notify', (f) => (this.showNotify(f.msg, f.type)))
64
+ this.ctx.on('editor-travel', ($el) => (this.travel($el)))
65
+ this.ctx.on('editor-travel-back', () => (this.travelBack()))
67
66
  }
68
67
 
69
68
  initLocalStorage () {
@@ -196,38 +195,41 @@ export default class Editor extends Component {
196
195
  this.$plugWrap.innerHTML = ''
197
196
  this.$plugWrap.style.display = 'none'
198
197
  this.openedPlugName = null
199
- this.$plugSwitcherWrap.innerHTML = ''
198
+ this.$plugBtnWrap.innerHTML = ''
200
199
 
201
200
  // 依次实例化 plug
202
201
  this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
203
- const plug = new PlugObj(this)
204
- this.plugList[plug.getName()] = plug
205
-
206
202
  // 切换按钮
207
- const btnElem = Utils.createElement(`<span class="atk-editor-action atk-editor-plug-switcher">${plug.getBtnHtml()}</span>`)
208
- this.$plugSwitcherWrap.appendChild(btnElem)
203
+ const btnElem = Utils.createElement(`<span class="atk-plug-btn">${PlugObj.BtnHTML}</span>`)
204
+ this.$plugBtnWrap.appendChild(btnElem)
209
205
  btnElem.addEventListener('click', () => {
210
- this.$plugSwitcherWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
206
+ let plug = this.plugList[PlugObj.Name]
207
+ if (!plug) {
208
+ plug = new PlugObj(this)
209
+ this.plugList[PlugObj.Name] = plug
210
+ }
211
+
212
+ this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
211
213
 
212
214
  // 若点击已打开的,则收起
213
- if (plug.getName() === this.openedPlugName) {
215
+ if (PlugObj.Name === this.openedPlugName) {
214
216
  plug.onHide()
215
217
  this.$plugWrap.style.display = 'none'
216
218
  this.openedPlugName = null
217
219
  return
218
220
  }
219
221
 
220
- if (this.$plugWrap.querySelector(`[data-plug-name="${plug.getName()}"]`) === null) {
222
+ if (this.$plugWrap.querySelector(`[data-plug-name="${PlugObj.Name}"]`) === null) {
221
223
  // 需要初始化
222
224
  const plugEl = plug.getEl()
223
- plugEl.setAttribute('data-plug-name', plug.getName())
225
+ plugEl.setAttribute('data-plug-name', PlugObj.Name)
224
226
  plugEl.style.display = 'none'
225
227
  this.$plugWrap.appendChild(plugEl)
226
228
  }
227
229
 
228
230
  (Array.from(this.$plugWrap.children) as HTMLElement[]).forEach((plugItemEl: HTMLElement) => {
229
231
  const plugItemName = plugItemEl.getAttribute('data-plug-name')!
230
- if (plugItemName === plug.getName()) {
232
+ if (plugItemName === PlugObj.Name) {
231
233
  plugItemEl.style.display = ''
232
234
  this.plugList[plugItemName].onShow()
233
235
  } else {
@@ -237,7 +239,7 @@ export default class Editor extends Component {
237
239
  })
238
240
 
239
241
  this.$plugWrap.style.display = ''
240
- this.openedPlugName = plug.getName()
242
+ this.openedPlugName = PlugObj.Name
241
243
 
242
244
  btnElem.classList.add('active')
243
245
  })
@@ -315,21 +317,27 @@ export default class Editor extends Component {
315
317
  this.$sendReply = null
316
318
  }
317
319
 
318
- setReply (commentData: CommentData) {
320
+ setReply (commentData: CommentData, $comment: HTMLElement, scroll = true) {
319
321
  if (this.replyComment !== null) {
320
322
  this.cancelReply()
321
323
  }
322
324
 
323
325
  if (this.$sendReply === null) {
324
- this.$sendReply = Utils.createElement('<div class="atk-send-reply-wrap"><div class="atk-send-reply">回复 <span class="atk-text"></span><span class="atk-cancel" title="取消 AT">×</span></div></div>');
326
+ this.$sendReply = Utils.createElement('<div class="atk-send-reply">回复 <span class="atk-text"></span><span class="atk-cancel" title="取消 AT">×</span></div>');
325
327
  this.$sendReply.querySelector<HTMLElement>('.atk-text')!.innerText = `@${commentData.nick}`
326
328
  this.$sendReply.addEventListener('click', () => {
327
329
  this.cancelReply()
328
330
  })
329
- this.$textareaWrap.prepend(this.$sendReply)
331
+ this.$textareaWrap.append(this.$sendReply)
330
332
  }
331
333
  this.replyComment = commentData
332
- Ui.scrollIntoView(this.$el)
334
+
335
+ if (this.ctx.conf.editorTravel === true) {
336
+ this.travel($comment)
337
+ }
338
+
339
+ if (scroll) Ui.scrollIntoView(this.$el)
340
+
333
341
  this.$textarea.focus()
334
342
  }
335
343
 
@@ -339,6 +347,10 @@ export default class Editor extends Component {
339
347
  this.$sendReply = null
340
348
  }
341
349
  this.replyComment = null
350
+
351
+ if (this.ctx.conf.editorTravel === true) {
352
+ this.travelBack()
353
+ }
342
354
  }
343
355
 
344
356
  initSubmit () {
@@ -366,15 +378,24 @@ export default class Editor extends Component {
366
378
  nick: this.user.data.nick,
367
379
  email: this.user.data.email,
368
380
  link: this.user.data.link,
369
- rid: this.replyComment === null ? 0 : this.replyComment.id
381
+ rid: (this.replyComment === null) ? 0 : this.replyComment.id,
382
+ page_key: (this.replyComment === null) ? this.ctx.conf.pageKey : this.replyComment.page_key,
383
+ page_title: (this.replyComment === null) ? this.ctx.conf.pageTitle : undefined,
384
+ site_name: (this.replyComment === null) ? this.ctx.conf.site : this.replyComment.site_name
370
385
  })
371
386
 
387
+ // 回复不同页面的评论
388
+ if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
389
+ window.open(`${this.replyComment.page_key}#atk-comment-${nComment.id}`)
390
+ }
391
+
372
392
  this.ctx.trigger('list-insert', nComment)
373
393
  this.clearEditor() // 清空编辑器
374
394
  this.ctx.trigger('editor-submitted')
375
395
  } catch (err: any) {
376
396
  console.error(err)
377
397
  this.showNotify(`评论失败,${err.msg || String(err)}`, 'e')
398
+ return
378
399
  } finally {
379
400
  Ui.hideLoading(this.$el)
380
401
  }
@@ -386,7 +407,8 @@ export default class Editor extends Component {
386
407
 
387
408
  /** 关闭评论 */
388
409
  close () {
389
- this.$closeComment.style.display = ''
410
+ if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
411
+ this.$textareaWrap.prepend(Utils.createElement('<div class="atk-comment-closed">仅管理员可评论</div>'))
390
412
 
391
413
  if (!this.user.data.isAdmin) {
392
414
  this.$textarea.style.display = 'none'
@@ -401,9 +423,28 @@ export default class Editor extends Component {
401
423
 
402
424
  /** 打开评论 */
403
425
  open () {
404
- this.$closeComment.style.display = 'none'
426
+ this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
405
427
  this.$textarea.style.display = ''
406
428
  this.$bottom.style.display = ''
407
429
  }
430
+
431
+ travel ($afterEl: HTMLElement) {
432
+ if (this.isTraveling) return
433
+ this.isTraveling = true
434
+ this.$el.after(Utils.createElement('<div class="atk-editor-travel-placeholder"></div>'))
435
+
436
+ const $travelPlace = Utils.createElement('<div></div>')
437
+ $afterEl.after($travelPlace)
438
+ $travelPlace.replaceWith(this.$el)
439
+ }
440
+
441
+ travelBack () {
442
+ if (!this.isTraveling) return
443
+ this.isTraveling = false
444
+ this.ctx.$root.querySelector('.atk-editor-travel-placeholder')?.replaceWith(this.$el)
445
+
446
+ // 取消回复
447
+ if (this.replyComment !== null) this.cancelReply()
448
+ }
408
449
  }
409
450
 
@@ -1,25 +1,18 @@
1
- <div class="atk-comment-wrap" data-comment-id="">
1
+ <div class="atk-comment-wrap">
2
2
  <div class="atk-comment">
3
-
4
3
  <div class="atk-avatar"></div>
5
-
6
- <div class="atk-comment-main">
7
-
4
+ <div class="atk-main">
8
5
  <div class="atk-header">
9
- <span class="atk-nick"></span>
10
- <span class="atk-badge"></span>
11
- <span class="atk-date"></span>
6
+ <span class="atk-item atk-nick"></span>
7
+ <span class="atk-item atk-badge"></span>
8
+ <span class="atk-item atk-date"></span>
12
9
  </div>
13
-
14
10
  <div class="atk-body">
15
11
  <div class="atk-content"></div>
16
12
  </div>
17
-
18
13
  <div class="atk-footer">
19
- <div class="atk-comment-actions"></div>
14
+ <div class="atk-actions"></div>
20
15
  </div>
21
-
22
16
  </div>
23
-
24
17
  </div>
25
18
  </div>
@@ -1,19 +1,18 @@
1
- <div class="atk-editor">
2
- <div class="atk-editor-header">
1
+ <div class="atk-main-editor">
2
+ <div class="atk-header">
3
3
  <input name="nick" placeholder="昵称" class="atk-nick" type="text" required="required">
4
4
  <input name="email" placeholder="邮箱" class="atk-email" type="email" required="required">
5
5
  <input name="link" placeholder="网址 (https://)" class="atk-link" type="url">
6
6
  </div>
7
- <div class="atk-editor-textarea-wrap">
8
- <div class="atk-close-comment" style="display: none;"><span>仅管理员可评论</span></div>
9
- <textarea id="atk-editor-textarea" class="atk-editor-textarea" placeholder=""></textarea>
7
+ <div class="atk-textarea-wrap">
8
+ <textarea class="atk-textarea" placeholder=""></textarea>
10
9
  </div>
11
- <div class="atk-editor-plug-wrap" style="display: none;"></div>
12
- <div class="atk-editor-bottom">
13
- <div class="atk-editor-bottom-part atk-left atk-editor-plug-switcher-wrap"></div>
14
- <div class="atk-editor-bottom-part atk-right">
10
+ <div class="atk-plug-wrap" style="display: none;"></div>
11
+ <div class="atk-bottom">
12
+ <div class="atk-item atk-plug-btn-wrap"></div>
13
+ <div class="atk-item">
15
14
  <button type="button" class="atk-send-btn"></button>
16
15
  </div>
17
16
  </div>
18
- <div class="atk-editor-notify-wrap"></div>
17
+ <div class="atk-notify-wrap"></div>
19
18
  </div>