From 1608911328824b3dcb365453b9302bb964f425f9 Mon Sep 17 00:00:00 2001
From: qwqcode <22412567+qwqcode@users.noreply.github.com>
Date: Fri, 27 Oct 2023 14:28:58 +0800
Subject: [PATCH] refactor(ui/list): separate list into standalone components
(#618)
* refactor(ui/list): separate the functions of list
* fix
* move files
---
ui/packages/artalk-sidebar/src/global.ts | 4 +-
.../artalk-sidebar/src/pages/comments.vue | 51 ++-
ui/packages/artalk/src/artalk.ts | 2 +-
ui/packages/artalk/src/comment/render-ctx.ts | 30 --
ui/packages/artalk/src/comment/render.ts | 32 +-
.../artalk/src/comment/renders/actions.ts | 90 ++---
.../artalk/src/comment/renders/avatar.ts | 14 +-
.../artalk/src/comment/renders/content.ts | 34 +-
.../artalk/src/comment/renders/header.ts | 26 +-
.../artalk/src/comment/renders/index.ts | 6 +-
.../artalk/src/comment/renders/pending.ts | 10 +-
.../artalk/src/comment/renders/reply-at.ts | 16 +-
.../artalk/src/comment/renders/reply-to.ts | 26 +-
.../artalk/src/components/pagination.ts | 4 +
ui/packages/artalk/src/config.ts | 8 +
ui/packages/artalk/src/context.ts | 80 +---
ui/packages/artalk/src/data.ts | 111 ++++++
ui/packages/artalk/src/layer/sidebar-layer.ts | 2 +-
ui/packages/artalk/src/lib/checker/admin.ts | 2 +-
ui/packages/artalk/src/lib/event-manager.ts | 16 +-
ui/packages/artalk/src/list/comment.ts | 21 +
ui/packages/artalk/src/list/layout.ts | 22 +-
ui/packages/artalk/src/list/list.ts | 358 +++---------------
ui/packages/artalk/src/list/options.ts | 37 --
ui/packages/artalk/src/list/page.ts | 94 +++++
.../src/list/paginator/adaptors/index.ts | 21 -
.../src/list/paginator/adaptors/pagination.ts | 42 --
.../src/list/paginator/adaptors/read-more.ts | 56 ---
.../artalk/src/list/paginator/index.ts | 79 +---
.../artalk/src/list/paginator/read-more.ts | 68 ++++
.../artalk/src/list/paginator/up-down.ts | 51 +++
.../artalk/src/plugins/editor/header-user.ts | 2 +-
.../artalk/src/plugins/editor/state-edit.ts | 2 +-
.../artalk/src/plugins/editor/submit-add.ts | 2 +-
ui/packages/artalk/src/plugins/index.ts | 27 +-
ui/packages/artalk/src/plugins/list-goto.ts | 65 ----
.../{list-copyright.ts => list/copyright.ts} | 2 +-
.../plugins/{list-count.ts => list/count.ts} | 16 +-
.../{list-dropdown.ts => list/dropdown.ts} | 4 +-
.../error-dialog.ts} | 32 +-
ui/packages/artalk/src/plugins/list/fetch.ts | 93 +++++
.../artalk/src/plugins/list/goto-first.ts | 24 ++
ui/packages/artalk/src/plugins/list/goto.ts | 68 ++++
ui/packages/artalk/src/plugins/list/index.ts | 25 ++
.../artalk/src/plugins/list/loading.ts | 19 +
.../no-comment.ts} | 8 +-
.../artalk/src/plugins/list/reach-bottom.ts | 37 ++
.../sidebar-btn.ts} | 2 +-
.../time-ticking.ts} | 2 +-
.../unread-badge.ts} | 6 +-
ui/packages/artalk/src/plugins/list/unread.ts | 41 ++
.../with-editor.ts} | 11 +-
ui/packages/artalk/src/plugins/unread.ts | 42 --
.../artalk/src/plugins/version-check.ts | 2 +-
ui/packages/artalk/src/service.ts | 6 +-
ui/packages/artalk/types/artalk-config.d.ts | 8 +
ui/packages/artalk/types/artalk-data.d.ts | 38 ++
ui/packages/artalk/types/context.d.ts | 52 +--
ui/packages/artalk/types/event.d.ts | 35 +-
59 files changed, 1092 insertions(+), 992 deletions(-)
delete mode 100644 ui/packages/artalk/src/comment/render-ctx.ts
create mode 100644 ui/packages/artalk/src/data.ts
create mode 100644 ui/packages/artalk/src/list/comment.ts
delete mode 100644 ui/packages/artalk/src/list/options.ts
create mode 100644 ui/packages/artalk/src/list/page.ts
delete mode 100644 ui/packages/artalk/src/list/paginator/adaptors/index.ts
delete mode 100644 ui/packages/artalk/src/list/paginator/adaptors/pagination.ts
delete mode 100644 ui/packages/artalk/src/list/paginator/adaptors/read-more.ts
create mode 100644 ui/packages/artalk/src/list/paginator/read-more.ts
create mode 100644 ui/packages/artalk/src/list/paginator/up-down.ts
delete mode 100644 ui/packages/artalk/src/plugins/list-goto.ts
rename ui/packages/artalk/src/plugins/{list-copyright.ts => list/copyright.ts} (89%)
rename ui/packages/artalk/src/plugins/{list-count.ts => list/count.ts} (55%)
rename ui/packages/artalk/src/plugins/{list-dropdown.ts => list/dropdown.ts} (94%)
rename ui/packages/artalk/src/plugins/{list-error-dialog.ts => list/error-dialog.ts} (54%)
create mode 100644 ui/packages/artalk/src/plugins/list/fetch.ts
create mode 100644 ui/packages/artalk/src/plugins/list/goto-first.ts
create mode 100644 ui/packages/artalk/src/plugins/list/goto.ts
create mode 100644 ui/packages/artalk/src/plugins/list/index.ts
create mode 100644 ui/packages/artalk/src/plugins/list/loading.ts
rename ui/packages/artalk/src/plugins/{list-no-comment.ts => list/no-comment.ts} (68%)
create mode 100644 ui/packages/artalk/src/plugins/list/reach-bottom.ts
rename ui/packages/artalk/src/plugins/{list-sidebar-btn.ts => list/sidebar-btn.ts} (94%)
rename ui/packages/artalk/src/plugins/{list-time-ticking.ts => list/time-ticking.ts} (91%)
rename ui/packages/artalk/src/plugins/{list-unread-badge.ts => list/unread-badge.ts} (78%)
create mode 100644 ui/packages/artalk/src/plugins/list/unread.ts
rename ui/packages/artalk/src/plugins/{list-close-editor.ts => list/with-editor.ts} (88%)
delete mode 100644 ui/packages/artalk/src/plugins/unread.ts
diff --git a/ui/packages/artalk-sidebar/src/global.ts b/ui/packages/artalk-sidebar/src/global.ts
index b89a889de..b8ada45be 100644
--- a/ui/packages/artalk-sidebar/src/global.ts
+++ b/ui/packages/artalk-sidebar/src/global.ts
@@ -1,6 +1,7 @@
import Artalk from 'artalk'
import type { LocalUser } from 'artalk/types/artalk-config'
import { useUserStore } from './stores/user'
+const { t } = useI18n()
export let artalk: Artalk|null = null
@@ -25,14 +26,13 @@ export function createArtalkInstance() {
artalkEl.style.display = 'none'
document.body.append(artalkEl)
- Artalk.DisabledComponents = ['list']
return Artalk.init({
el: artalkEl,
server: (import.meta.env.DEV) ? 'http://localhost:23366' : '../',
pageKey: bootParams.pageKey,
site: bootParams.site,
darkMode: bootParams.darkMode,
- useBackendConf: true
+ useBackendConf: true,
})
}
diff --git a/ui/packages/artalk-sidebar/src/pages/comments.vue b/ui/packages/artalk-sidebar/src/pages/comments.vue
index 8ffb6a73e..547041027 100644
--- a/ui/packages/artalk-sidebar/src/pages/comments.vue
+++ b/ui/packages/artalk-sidebar/src/pages/comments.vue
@@ -33,54 +33,51 @@ onMounted(() => {
}
watch(curtTab, (curtTab) => {
- list.fetchComments(0)
+ artalk!.ctx.fetch({
+ offset: 0
+ })
})
watch(curtSite, (value) => {
- list.reload()
+ artalk!.ctx.reload()
})
- // 初始化评论列表
- const list = new Artalk.List(artalk!.ctx, {
- liteMode: true,
- flatMode: true,
- unreadHighlight: true,
- scrollListenerAt: wrapEl.value,
- pageMode: 'pagination',
- // pageSize: 20 // TODO consider fixed pageSize value in sidebar
- noCommentText: `
`,
- renderComment: (comment) => {
- const pageURL = comment.getData().page_url
- comment.getRender().setOpenURL(`${pageURL}#atk-comment-${comment.getID()}`)
- comment.getConf().onReplyBtnClick = () => {
- artalk!.ctx.replyComment(comment.getData(), comment.getEl())
- }
+ artalk!.ctx.updateConf({
+ noComment: ``,
+ pagination: {
+ pageSize: 20,
+ readMore: false,
+ autoLoad: false,
},
- paramsEditor: (params) => {
+ listUnreadHighlight: true,
+ listFetchParamsModifier: (params) => {
params.type = curtTab.value // 列表数据类型
params.site_name = curtSite.value // 站点名
if (search.value) params.search = search.value
- }
+ },
+ listScrollListenerAt: wrapEl.value,
})
- artalk!.ctx.inject('list', list)
-
- artalk!.on('list-inserted', (data) => {
- wrapEl.value!.scrollTo(0, 0)
+ artalk!.ctx.on('comment-rendered', (comment) => {
+ const pageURL = comment.getData().page_url
+ comment.getRender().setOpenURL(`${pageURL}#atk-comment-${comment.getID()}`)
+ comment.getConf().onReplyBtnClick = () => {
+ artalk!.ctx.replyComment(comment.getData(), comment.getEl())
+ }
})
- list.reload()
+ artalk!.reload()
- listEl.value?.append(list.$el)
+ listEl.value?.append(artalk!.ctx.get('list')!.$el)
// 搜索功能
nav.enableSearch((value: string) => {
search.value = value
- list.reload()
+ artalk!.reload()
}, () => {
if (search.value === '') return
search.value = ''
- list.reload()
+ artalk!.reload()
})
})
diff --git a/ui/packages/artalk/src/artalk.ts b/ui/packages/artalk/src/artalk.ts
index 578aeae04..6bf512d9a 100644
--- a/ui/packages/artalk/src/artalk.ts
+++ b/ui/packages/artalk/src/artalk.ts
@@ -83,7 +83,7 @@ export default class Artalk {
/** Reload comment list of Artalk */
public reload() {
- this.ctx.listReload()
+ this.ctx.reload()
}
/** Destroy instance of Artalk */
diff --git a/ui/packages/artalk/src/comment/render-ctx.ts b/ui/packages/artalk/src/comment/render-ctx.ts
deleted file mode 100644
index 1df911c5f..000000000
--- a/ui/packages/artalk/src/comment/render-ctx.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Comment from './comment'
-import ActionBtn from '../components/action-btn'
-
-export default class RenderCtx {
- public comment: Comment
-
- public get ctx() { return this.comment.ctx }
- public get data() { return this.comment.getData() }
- public get conf() { return this.comment.conf }
- public get cConf() { return this.comment.getConf() }
-
- public $el!: HTMLElement
- public $main!: HTMLElement
- public $header!: HTMLElement
- public $headerNick!: HTMLElement
- public $headerBadgeWrap!: HTMLElement
- public $body!: HTMLElement
- public $content!: HTMLElement
- public $childrenWrap!: HTMLElement|null
- public $actions!: HTMLElement
- public voteBtnUp?: ActionBtn
- public voteBtnDown?: ActionBtn
-
- public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
- public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
-
- public constructor(comment: Comment) {
- this.comment = comment
- }
-}
diff --git a/ui/packages/artalk/src/comment/render.ts b/ui/packages/artalk/src/comment/render.ts
index c016a94d4..6aa79c34b 100644
--- a/ui/packages/artalk/src/comment/render.ts
+++ b/ui/packages/artalk/src/comment/render.ts
@@ -1,15 +1,37 @@
+import Comment from './comment'
+import ActionBtn from '../components/action-btn'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
+import * as HeightLimit from './height-limit'
import CommentHTML from './comment.html?raw'
-import Comment from './comment'
-import RenderCtx from './render-ctx'
import loadRenders from './renders'
-import * as HeightLimit from './height-limit'
-export default class CommentRender extends RenderCtx {
+export default class Render {
+ public comment: Comment
+
+ public get ctx() { return this.comment.ctx }
+ public get data() { return this.comment.getData() }
+ public get conf() { return this.comment.conf }
+ public get cConf() { return this.comment.getConf() }
+
+ public $el!: HTMLElement
+ public $main!: HTMLElement
+ public $header!: HTMLElement
+ public $headerNick!: HTMLElement
+ public $headerBadgeWrap!: HTMLElement
+ public $body!: HTMLElement
+ public $content!: HTMLElement
+ public $childrenWrap!: HTMLElement|null
+ public $actions!: HTMLElement
+ public voteBtnUp?: ActionBtn
+ public voteBtnDown?: ActionBtn
+
+ public $replyTo?: HTMLElement // 回复评论内容 (平铺下显示)
+ public $replyAt?: HTMLElement // 回复 AT(层级嵌套下显示)
+
public constructor(comment: Comment) {
- super(comment)
+ this.comment = comment
}
public render() {
diff --git a/ui/packages/artalk/src/comment/renders/actions.ts b/ui/packages/artalk/src/comment/renders/actions.ts
index 209867830..1a008ee7c 100644
--- a/ui/packages/artalk/src/comment/renders/actions.ts
+++ b/ui/packages/artalk/src/comment/renders/actions.ts
@@ -1,114 +1,114 @@
import * as Utils from '../../lib/utils'
import ActionBtn from '../../components/action-btn'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 评论操作按钮界面
*/
-export default function renderActions(ctx: RenderCtx) {
+export default function renderActions(r: Render) {
Object.entries({
renderVote, renderReply,
// 管理员操作
renderCollapse, renderModerator, renderPin, renderEdit, renderDel
}).forEach(([name, render]) => {
- render(ctx)
+ render(r)
})
}
// 操作按钮 - 投票
-function renderVote(ctx: RenderCtx) {
- if (!ctx.ctx.conf.vote) return // 关闭投票功能
+function renderVote(r: Render) {
+ if (!r.ctx.conf.vote) return // 关闭投票功能
// 赞同按钮
- ctx.voteBtnUp = new ActionBtn(ctx.ctx, () => `${ctx.ctx.$t('voteUp')} (${ctx.data.vote_up || 0})`).appendTo(ctx.$actions)
- ctx.voteBtnUp.setClick(() => {
- ctx.comment.getActions().vote('up')
+ r.voteBtnUp = new ActionBtn(r.ctx, () => `${r.ctx.$t('voteUp')} (${r.data.vote_up || 0})`).appendTo(r.$actions)
+ r.voteBtnUp.setClick(() => {
+ r.comment.getActions().vote('up')
})
// 反对按钮
- if (ctx.ctx.conf.voteDown) {
- ctx.voteBtnDown = new ActionBtn(ctx.ctx, () => `${ctx.ctx.$t('voteDown')} (${ctx.data.vote_down || 0})`).appendTo(ctx.$actions)
- ctx.voteBtnDown.setClick(() => {
- ctx.comment.getActions().vote('down')
+ if (r.ctx.conf.voteDown) {
+ r.voteBtnDown = new ActionBtn(r.ctx, () => `${r.ctx.$t('voteDown')} (${r.data.vote_down || 0})`).appendTo(r.$actions)
+ r.voteBtnDown.setClick(() => {
+ r.comment.getActions().vote('down')
})
}
}
// 操作按钮 - 回复
-function renderReply(ctx: RenderCtx) {
- if (!ctx.data.is_allow_reply) return // 不允许回复
+function renderReply(r: Render) {
+ if (!r.data.is_allow_reply) return // 不允许回复
- const replyBtn = Utils.createElement(`${ctx.ctx.$t('reply')}`)
- ctx.$actions.append(replyBtn)
+ const replyBtn = Utils.createElement(`${r.ctx.$t('reply')}`)
+ r.$actions.append(replyBtn)
replyBtn.addEventListener('click', (e) => {
e.stopPropagation() // 防止穿透
- if (!ctx.cConf.onReplyBtnClick) {
- ctx.ctx.replyComment(ctx.data, ctx.$el)
+ if (!r.cConf.onReplyBtnClick) {
+ r.ctx.replyComment(r.data, r.$el)
} else {
- ctx.cConf.onReplyBtnClick()
+ r.cConf.onReplyBtnClick()
}
})
}
// 操作按钮 - 折叠
-function renderCollapse(ctx: RenderCtx) {
- const collapseBtn = new ActionBtn(ctx.ctx, {
- text: () => (ctx.data.is_collapsed ? ctx.ctx.$t('expand') : ctx.ctx.$t('collapse')),
+function renderCollapse(r: Render) {
+ const collapseBtn = new ActionBtn(r.ctx, {
+ text: () => (r.data.is_collapsed ? r.ctx.$t('expand') : r.ctx.$t('collapse')),
adminOnly: true
})
- collapseBtn.appendTo(ctx.$actions)
+ collapseBtn.appendTo(r.$actions)
collapseBtn.setClick(() => {
- ctx.comment.getActions().adminEdit('collapsed', collapseBtn)
+ r.comment.getActions().adminEdit('collapsed', collapseBtn)
})
}
// 操作按钮 - 审核
-function renderModerator(ctx: RenderCtx) {
- const pendingBtn = new ActionBtn(ctx.ctx, {
- text: () => (ctx.data.is_pending ? ctx.ctx.$t('pending') : ctx.ctx.$t('approved')),
+function renderModerator(r: Render) {
+ const pendingBtn = new ActionBtn(r.ctx, {
+ text: () => (r.data.is_pending ? r.ctx.$t('pending') : r.ctx.$t('approved')),
adminOnly: true
})
- pendingBtn.appendTo(ctx.$actions)
+ pendingBtn.appendTo(r.$actions)
pendingBtn.setClick(() => {
- ctx.comment.getActions().adminEdit('pending', pendingBtn)
+ r.comment.getActions().adminEdit('pending', pendingBtn)
})
}
// 操作按钮 - 置顶
-function renderPin(ctx: RenderCtx) {
- const pinnedBtn = new ActionBtn(ctx.ctx, {
- text: () => (ctx.data.is_pinned ? ctx.ctx.$t('unpin') : ctx.ctx.$t('pin')),
+function renderPin(r: Render) {
+ const pinnedBtn = new ActionBtn(r.ctx, {
+ text: () => (r.data.is_pinned ? r.ctx.$t('unpin') : r.ctx.$t('pin')),
adminOnly: true
})
- pinnedBtn.appendTo(ctx.$actions)
+ pinnedBtn.appendTo(r.$actions)
pinnedBtn.setClick(() => {
- ctx.comment.getActions().adminEdit('pinned', pinnedBtn)
+ r.comment.getActions().adminEdit('pinned', pinnedBtn)
})
}
// 操作按钮 - 编辑
-function renderEdit(ctx: RenderCtx) {
- const editBtn = new ActionBtn(ctx.ctx, {
- text: ctx.ctx.$t('edit'),
+function renderEdit(r: Render) {
+ const editBtn = new ActionBtn(r.ctx, {
+ text: r.ctx.$t('edit'),
adminOnly: true
})
- editBtn.appendTo(ctx.$actions)
+ editBtn.appendTo(r.$actions)
editBtn.setClick(() => {
- ctx.ctx.editComment(ctx.data, ctx.$el)
+ r.ctx.editComment(r.data, r.$el)
})
}
// 操作按钮 - 删除
-function renderDel(ctx: RenderCtx) {
- const delBtn = new ActionBtn(ctx.ctx, {
- text: ctx.ctx.$t('delete'),
+function renderDel(r: Render) {
+ const delBtn = new ActionBtn(r.ctx, {
+ text: r.ctx.$t('delete'),
confirm: true,
- confirmText: ctx.ctx.$t('deleteConfirm'),
+ confirmText: r.ctx.$t('deleteConfirm'),
adminOnly: true,
})
- delBtn.appendTo(ctx.$actions)
+ delBtn.appendTo(r.$actions)
delBtn.setClick(() => {
- ctx.comment.getActions().adminDelete(delBtn)
+ r.comment.getActions().adminDelete(delBtn)
})
}
diff --git a/ui/packages/artalk/src/comment/renders/avatar.ts b/ui/packages/artalk/src/comment/renders/avatar.ts
index 065dff3b5..a7305fc94 100644
--- a/ui/packages/artalk/src/comment/renders/avatar.ts
+++ b/ui/packages/artalk/src/comment/renders/avatar.ts
@@ -1,19 +1,19 @@
import * as Utils from '../../lib/utils'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 评论头像界面
*/
-export default function renderAvatar(ctx: RenderCtx) {
- const $avatar = ctx.$el.querySelector('.atk-avatar')!
+export default function renderAvatar(r: Render) {
+ const $avatar = r.$el.querySelector('.atk-avatar')!
const $avatarImg = Utils.createElement('')
- const avatarURLBuilder = ctx.conf.avatarURLBuilder
- $avatarImg.src = avatarURLBuilder ? avatarURLBuilder(ctx.data) : ctx.comment.getGravatarURL()
+ const avatarURLBuilder = r.conf.avatarURLBuilder
+ $avatarImg.src = avatarURLBuilder ? avatarURLBuilder(r.data) : r.comment.getGravatarURL()
- if (ctx.data.link) {
+ if (r.data.link) {
const $avatarA = Utils.createElement('')
- $avatarA.href = Utils.isValidURL(ctx.data.link) ? ctx.data.link : `https://${ctx.data.link}`
+ $avatarA.href = Utils.isValidURL(r.data.link) ? r.data.link : `https://${r.data.link}`
$avatarA.append($avatarImg)
$avatar.append($avatarA)
} else {
diff --git a/ui/packages/artalk/src/comment/renders/content.ts b/ui/packages/artalk/src/comment/renders/content.ts
index f41a3f8a8..f00da67ff 100644
--- a/ui/packages/artalk/src/comment/renders/content.ts
+++ b/ui/packages/artalk/src/comment/renders/content.ts
@@ -1,39 +1,39 @@
import * as Utils from '../../lib/utils'
import * as Ui from '../../lib/ui'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 评论内容界面
*/
-export default function renderContent(ctx: RenderCtx) {
- if (!ctx.data.is_collapsed) {
- ctx.$content.innerHTML = ctx.comment.getContentMarked()
- ctx.$content.classList.remove('atk-hide', 'atk-collapsed')
+export default function renderContent(r: Render) {
+ if (!r.data.is_collapsed) {
+ r.$content.innerHTML = r.comment.getContentMarked()
+ r.$content.classList.remove('atk-hide', 'atk-collapsed')
return
}
// 内容 & 折叠
- ctx.$content.classList.add('atk-hide', 'atk-type-collapsed')
+ r.$content.classList.add('atk-hide', 'atk-type-collapsed')
const collapsedInfoEl = Utils.createElement(`
- ${ctx.ctx.$t('collapsedMsg')}
- ${ctx.ctx.$t('expand')}
+ ${r.ctx.$t('collapsedMsg')}
+ ${r.ctx.$t('expand')}
`)
- ctx.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
+ r.$body.insertAdjacentElement('beforeend', collapsedInfoEl)
const contentShowBtn = collapsedInfoEl.querySelector('.atk-show-btn')!
contentShowBtn.addEventListener('click', (e) => {
e.stopPropagation() // 防止穿透
- if (ctx.$content.classList.contains('atk-hide')) {
- ctx.$content.innerHTML = ctx.comment.getContentMarked()
- ctx.$content.classList.remove('atk-hide')
- Ui.playFadeInAnim(ctx.$content)
- contentShowBtn.innerText = ctx.ctx.$t('collapse')
+ if (r.$content.classList.contains('atk-hide')) {
+ r.$content.innerHTML = r.comment.getContentMarked()
+ r.$content.classList.remove('atk-hide')
+ Ui.playFadeInAnim(r.$content)
+ contentShowBtn.innerText = r.ctx.$t('collapse')
} else {
- ctx.$content.innerHTML = ''
- ctx.$content.classList.add('atk-hide')
- contentShowBtn.innerText = ctx.ctx.$t('expand')
+ r.$content.innerHTML = ''
+ r.$content.classList.add('atk-hide')
+ contentShowBtn.innerText = r.ctx.$t('expand')
}
})
}
diff --git a/ui/packages/artalk/src/comment/renders/header.ts b/ui/packages/artalk/src/comment/renders/header.ts
index f30883ff1..e84094917 100644
--- a/ui/packages/artalk/src/comment/renders/header.ts
+++ b/ui/packages/artalk/src/comment/renders/header.ts
@@ -1,31 +1,31 @@
import * as Utils from '../../lib/utils'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 评论头部界面
*/
-export default function renderHeader(ctx: RenderCtx) {
+export default function renderHeader(r: Render) {
Object.entries({
renderNick, renderVerifyBadge, renderDate, renderUABadge
}).forEach(([name, render]) => {
- render(ctx)
+ render(r)
})
}
-function renderNick(ctx: RenderCtx) {
- ctx.$headerNick = ctx.$el.querySelector('.atk-nick')!
+function renderNick(r: Render) {
+ r.$headerNick = r.$el.querySelector('.atk-nick')!
- if (ctx.data.link) {
+ if (r.data.link) {
const $nickA = Utils.createElement('')
- $nickA.innerText = ctx.data.nick
- $nickA.href = Utils.isValidURL(ctx.data.link) ? ctx.data.link : `https://${ctx.data.link}`
- ctx.$headerNick.append($nickA)
+ $nickA.innerText = r.data.nick
+ $nickA.href = Utils.isValidURL(r.data.link) ? r.data.link : `https://${r.data.link}`
+ r.$headerNick.append($nickA)
} else {
- ctx.$headerNick.innerText = ctx.data.nick
+ r.$headerNick.innerText = r.data.nick
}
}
-function renderVerifyBadge(ctx: RenderCtx) {
+function renderVerifyBadge(ctx: Render) {
ctx.$headerBadgeWrap = ctx.$el.querySelector('.atk-badge-wrap')!
ctx.$headerBadgeWrap.innerHTML = ''
@@ -44,13 +44,13 @@ function renderVerifyBadge(ctx: RenderCtx) {
}
}
-function renderDate(ctx: RenderCtx) {
+function renderDate(ctx: Render) {
const $date = ctx.$el.querySelector('.atk-date')!
$date.innerText = ctx.comment.getDateFormatted()
$date.setAttribute('data-atk-comment-date', String(+new Date(ctx.data.date)))
}
-function renderUABadge(ctx: RenderCtx) {
+function renderUABadge(ctx: Render) {
if (!ctx.ctx.conf.uaBadge && !ctx.data.ip_region) return
let $uaWrap = ctx.$header.querySelector('atk-ua-wrap')
diff --git a/ui/packages/artalk/src/comment/renders/index.ts b/ui/packages/artalk/src/comment/renders/index.ts
index bcd821b5d..d0e296ddd 100644
--- a/ui/packages/artalk/src/comment/renders/index.ts
+++ b/ui/packages/artalk/src/comment/renders/index.ts
@@ -1,4 +1,4 @@
-import RenderCtx from '../render-ctx'
+import Render from '../render'
import Avatar from './avatar'
import Header from './header'
import Content from './content'
@@ -12,8 +12,8 @@ const Renders = {
ReplyTo, Pending, Actions
}
-export default function loadRenders(ctx: RenderCtx) {
+export default function loadRenders(r: Render) {
Object.entries(Renders).forEach(([ name, render ]) => {
- render(ctx)
+ render(r)
})
}
diff --git a/ui/packages/artalk/src/comment/renders/pending.ts b/ui/packages/artalk/src/comment/renders/pending.ts
index f357e8f2c..118bba923 100644
--- a/ui/packages/artalk/src/comment/renders/pending.ts
+++ b/ui/packages/artalk/src/comment/renders/pending.ts
@@ -1,12 +1,12 @@
import * as Utils from '../../lib/utils'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 待审核状态界面
*/
-export default function renderPending(ctx: RenderCtx) {
- if (!ctx.data.is_pending) return
+export default function renderPending(r: Render) {
+ if (!r.data.is_pending) return
- const pendingEl = Utils.createElement(`${ctx.ctx.$t('pendingMsg')}
`)
- ctx.$body.prepend(pendingEl)
+ const pendingEl = Utils.createElement(`${r.ctx.$t('pendingMsg')}
`)
+ r.$body.prepend(pendingEl)
}
diff --git a/ui/packages/artalk/src/comment/renders/reply-at.ts b/ui/packages/artalk/src/comment/renders/reply-at.ts
index 42774773b..38fd24312 100644
--- a/ui/packages/artalk/src/comment/renders/reply-at.ts
+++ b/ui/packages/artalk/src/comment/renders/reply-at.ts
@@ -1,16 +1,16 @@
import * as Utils from '../../lib/utils'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 层级嵌套模式显示 AT 界面
*/
-export default function renderReplyAt(ctx: RenderCtx) {
- if (ctx.cConf.isFlatMode || ctx.data.rid === 0) return // not 平铺模式 或 根评论
- if (!ctx.cConf.replyTo) return
+export default function renderReplyAt(r: Render) {
+ if (r.cConf.isFlatMode || r.data.rid === 0) return // not 平铺模式 或 根评论
+ if (!r.cConf.replyTo) return
- ctx.$replyAt = Utils.createElement(``)
- ctx.$replyAt.querySelector('.atk-nick')!.innerText = `${ctx.cConf.replyTo.nick}`
- ctx.$replyAt.onclick = () => { ctx.comment.getActions().goToReplyComment() }
+ r.$replyAt = Utils.createElement(``)
+ r.$replyAt.querySelector('.atk-nick')!.innerText = `${r.cConf.replyTo.nick}`
+ r.$replyAt.onclick = () => { r.comment.getActions().goToReplyComment() }
- ctx.$headerBadgeWrap.insertAdjacentElement('afterend', ctx.$replyAt)
+ r.$headerBadgeWrap.insertAdjacentElement('afterend', r.$replyAt)
}
diff --git a/ui/packages/artalk/src/comment/renders/reply-to.ts b/ui/packages/artalk/src/comment/renders/reply-to.ts
index 644c46a61..2def90929 100644
--- a/ui/packages/artalk/src/comment/renders/reply-to.ts
+++ b/ui/packages/artalk/src/comment/renders/reply-to.ts
@@ -1,24 +1,24 @@
import * as Utils from '../../lib/utils'
import marked from '../../lib/marked'
-import RenderCtx from '../render-ctx'
+import Render from '../render'
/**
* 回复的对象界面
*/
-export default function renderReplyTo(ctx: RenderCtx) {
- if (!ctx.cConf.isFlatMode) return // 仅平铺模式显示
- if (!ctx.cConf.replyTo) return
+export default function renderReplyTo(r: Render) {
+ if (!r.cConf.isFlatMode) return // 仅平铺模式显示
+ if (!r.cConf.replyTo) return
- ctx.$replyTo = Utils.createElement(`
+ r.$replyTo = Utils.createElement(`
-
${ctx.ctx.$t('reply')} :
+
${r.ctx.$t('reply')} :
`)
- const $nick = ctx.$replyTo.querySelector('.atk-nick')!
- $nick.innerText = `@${ctx.cConf.replyTo.nick}`
- $nick.onclick = () => { ctx.comment.getActions().goToReplyComment() }
- let replyContent = marked(ctx.ctx, ctx.cConf.replyTo.content)
- if (ctx.cConf.replyTo.is_collapsed) replyContent = `[${Utils.htmlEncode(ctx.ctx.$t('collapsed'))}]`
- ctx.$replyTo.querySelector('.atk-content')!.innerHTML = replyContent
- ctx.$body.prepend(ctx.$replyTo)
+ const $nick = r.$replyTo.querySelector('.atk-nick')!
+ $nick.innerText = `@${r.cConf.replyTo.nick}`
+ $nick.onclick = () => { r.comment.getActions().goToReplyComment() }
+ let replyContent = marked(r.ctx, r.cConf.replyTo.content)
+ if (r.cConf.replyTo.is_collapsed) replyContent = `[${Utils.htmlEncode(r.ctx.$t('collapsed'))}]`
+ r.$replyTo.querySelector('.atk-content')!.innerHTML = replyContent
+ r.$body.prepend(r.$replyTo)
}
diff --git a/ui/packages/artalk/src/components/pagination.ts b/ui/packages/artalk/src/components/pagination.ts
index cc2b7e742..54c0902a0 100644
--- a/ui/packages/artalk/src/components/pagination.ts
+++ b/ui/packages/artalk/src/components/pagination.ts
@@ -99,6 +99,10 @@ export default class Pagination {
this.change(page)
}
+ public getHasMore() {
+ return this.page + 1 <= this.maxPage
+ }
+
public change(page: number) {
this.page = page
this.conf.onChange(this.offset)
diff --git a/ui/packages/artalk/src/config.ts b/ui/packages/artalk/src/config.ts
index d32d211b5..b77a955fa 100644
--- a/ui/packages/artalk/src/config.ts
+++ b/ui/packages/artalk/src/config.ts
@@ -40,6 +40,14 @@ export function handelBaseConf(customConf: Partial): ArtalkConfig
conf.locale = navigator.language
}
+ // flatMode
+ if (conf.flatMode === true || Number(conf.nestMax) <= 1)
+ conf.flatMode = true
+
+ // 自动判断启用平铺模式
+ if (conf.flatMode === 'auto')
+ conf.flatMode = window.matchMedia("(max-width: 768px)").matches
+
return conf
}
diff --git a/ui/packages/artalk/src/context.ts b/ui/packages/artalk/src/context.ts
index 2a26f2d3a..d1a5fe972 100644
--- a/ui/packages/artalk/src/context.ts
+++ b/ui/packages/artalk/src/context.ts
@@ -1,5 +1,5 @@
import type ArtalkConfig from '~/types/artalk-config'
-import type { CommentData, NotifyData, PageData } from '~/types/artalk-data'
+import type { CommentData, ListFetchParams } from '~/types/artalk-data'
import type { EventPayloadMap } from '~/types/event'
import type ContextApi from '~/types/context'
import type { TInjectedServices } from './service'
@@ -9,11 +9,12 @@ import * as DarkMode from './lib/dark-mode'
import * as marked from './lib/marked'
import { CheckerCaptchaPayload, CheckerPayload } from './lib/checker'
+import { DataManager } from './data'
import * as I18n from './i18n'
import { getLayerWrap } from './layer'
import { SidebarShowPayload } from './layer/sidebar-layer'
-import Comment from './comment'
import EventManager from './lib/event-manager'
+import { handelBaseConf } from './config'
// Auto dependency injection
interface Context extends TInjectedServices { }
@@ -24,13 +25,10 @@ interface Context extends TInjectedServices { }
class Context implements ContextApi {
/* 运行参数 */
public conf: ArtalkConfig
+ public data: DataManager
public $root: HTMLElement
public markedReplacers: ((raw: string) => string)[] = []
- private commentList: Comment[] = [] // Note: 无层级结构 + 无须排列
- private page?: PageData
- private unreadList: NotifyData[] = []
-
/* Event Manager */
private events = new EventManager()
@@ -40,6 +38,8 @@ class Context implements ContextApi {
this.$root = $root || document.createElement('div')
this.$root.classList.add('artalk')
this.$root.innerHTML = ''
+
+ this.data = new DataManager(this.events)
}
public inject(depName: string, obj: any) {
@@ -54,37 +54,8 @@ class Context implements ContextApi {
return this.api
}
- /* 评论操作 */
- public getCommentList() {
- return this.commentList
- }
-
- public clearCommentList() {
- this.commentList = []
- }
-
- public getCommentDataList() {
- return this.commentList.map(c => c.getData())
- }
-
- public findComment(id: number): Comment|undefined {
- return this.commentList.find(c => c.getData().id === id)
- }
-
- public deleteComment(id: number) {
- this.list?.deleteComment(id)
- }
-
- public clearAllComments() {
- this.list?.clearAllComments()
- }
-
- public insertComment(commentData: CommentData) {
- this.list?.insertComment(commentData)
- }
-
- public updateComment(commentData: CommentData): void {
- this.list?.updateComment(commentData)
+ public getData() {
+ return this.data
}
public replyComment(commentData: CommentData, $comment: HTMLElement): void {
@@ -95,33 +66,18 @@ class Context implements ContextApi {
this.editor.setEditComment(commentData, $comment)
}
- /** 未读通知 */
- public getUnreadList() {
- return this.unreadList
- }
-
- public updateUnreadList(notifies: NotifyData[]): void {
- this.unreadList = notifies
- this.trigger('unread-updated', notifies)
- }
-
- /** 页面数据 */
- getPage(): PageData|undefined {
- return this.page
- }
-
- updatePage(pageData: PageData): void {
- this.page = pageData
- this.trigger('page-loaded', pageData)
+ public fetch(params: Partial): void {
+ this.data.fetchComments(params)
}
- /* 评论列表 */
- public listReload(): void {
- this.list?.reload()
+ public reload(): void {
+ this.data.fetchComments({
+ offset: 0,
+ })
}
- public reload(): void {
- this.listReload()
+ public listGotoFirst(): void {
+ this.trigger('list-goto-first')
}
/* 编辑器 */
@@ -202,8 +158,8 @@ class Context implements ContextApi {
}
public updateConf(nConf: Partial): void {
- this.conf = Utils.mergeDeep(this.conf, nConf)
- this.trigger('conf-loaded')
+ this.conf = Utils.mergeDeep(this.conf, handelBaseConf(nConf))
+ this.trigger('conf-loaded', this.conf)
}
public getMarkedInstance() {
diff --git a/ui/packages/artalk/src/data.ts b/ui/packages/artalk/src/data.ts
new file mode 100644
index 000000000..2518e7e11
--- /dev/null
+++ b/ui/packages/artalk/src/data.ts
@@ -0,0 +1,111 @@
+import type { NotifyData, PageData, CommentData, DataManagerApi, ListFetchParams, ListLastFetchData } from '~/types/artalk-data'
+import type { EventPayloadMap } from '~/types/event'
+import EventManager from './lib/event-manager'
+
+export class DataManager implements DataManagerApi {
+ private loading: boolean = false
+ private listLastFetch?: ListLastFetchData
+ private comments: CommentData[] = [] // Note: 无层级结构 + 无须排列
+ private unreads: NotifyData[] = []
+ private page?: PageData
+
+ constructor(
+ protected events: EventManager
+ ) {}
+
+ getLoading() {
+ return this.loading
+ }
+
+ setLoading(val: boolean) {
+ this.loading = val
+ }
+
+ getListLastFetch() {
+ return this.listLastFetch
+ }
+
+ setListLastFetch(val: ListLastFetchData) {
+ this.listLastFetch = val
+ }
+
+ // -------------------------------------------------------------------
+ // Comments
+ // -------------------------------------------------------------------
+ getComments() {
+ return this.comments
+ }
+
+ fetchComments(params: Partial) {
+ this.events.trigger('list-fetch', params)
+ }
+
+ findComment(id: number) {
+ return this.comments.find(c => c.id === id)
+ }
+
+ clearComments() {
+ this.comments = []
+ this.events.trigger('list-loaded', this.comments)
+ }
+
+ loadComments(comments: CommentData[]) {
+ this.events.trigger('list-load', comments)
+
+ this.comments = comments
+
+ this.events.trigger('list-loaded', comments)
+ }
+
+ insertComment(comment: CommentData) {
+ this.comments.push(comment)
+
+ this.events.trigger('comment-inserted', comment)
+ this.events.trigger('list-loaded', this.comments) // list-loaded should always keep the last
+ }
+
+ updateComment(comment: CommentData) {
+ this.comments = this.comments.map(c => {
+ if (c.id === comment.id) return comment
+ return c
+ })
+
+ this.events.trigger('comment-updated', comment)
+ this.events.trigger('list-loaded', this.comments)
+ }
+
+ deleteComment(id: number) {
+ const comment = this.comments.find(c => c.id === id)
+ if (!comment) throw new Error(`Comment ${id} not found`)
+ this.comments = this.comments.filter(c => c.id !== id)
+
+ this.events.trigger('comment-deleted', comment)
+ this.events.trigger('list-loaded', this.comments)
+ }
+
+ // -------------------------------------------------------------------
+ // Unreads
+ // -------------------------------------------------------------------
+ getUnreads() {
+ return this.unreads
+ }
+
+ updateUnreads(unread: NotifyData[]) {
+ this.unreads = unread
+
+ this.events.trigger('unreads-updated', this.unreads)
+ }
+
+ // -------------------------------------------------------------------
+ // Page
+ // -------------------------------------------------------------------
+ getPage() {
+ return this.page
+ }
+
+ updatePage(pageData: PageData) {
+ this.page = pageData
+
+ this.events.trigger('page-loaded', pageData)
+ }
+}
diff --git a/ui/packages/artalk/src/layer/sidebar-layer.ts b/ui/packages/artalk/src/layer/sidebar-layer.ts
index cbd83074e..8aba87ec4 100644
--- a/ui/packages/artalk/src/layer/sidebar-layer.ts
+++ b/ui/packages/artalk/src/layer/sidebar-layer.ts
@@ -113,7 +113,7 @@ export default class SidebarLayer extends Component {
// 清空 unread
setTimeout(() => {
- this.ctx.updateUnreadList([])
+ this.ctx.getData().updateUnreads([])
}, 0)
this.ctx.trigger('sidebar-show')
diff --git a/ui/packages/artalk/src/lib/checker/admin.ts b/ui/packages/artalk/src/lib/checker/admin.ts
index a931b2206..e45aaa74b 100644
--- a/ui/packages/artalk/src/lib/checker/admin.ts
+++ b/ui/packages/artalk/src/lib/checker/admin.ts
@@ -28,7 +28,7 @@ const AdminChecker: Checker = {
token: userToken
})
checker.getCtx().trigger('user-changed', User.data)
- checker.getCtx().listReload()
+ checker.getCtx().reload()
},
onError(checker, err, inputVal, formEl) {}
diff --git a/ui/packages/artalk/src/lib/event-manager.ts b/ui/packages/artalk/src/lib/event-manager.ts
index be6f13d8f..fd42daaa4 100644
--- a/ui/packages/artalk/src/lib/event-manager.ts
+++ b/ui/packages/artalk/src/lib/event-manager.ts
@@ -1,11 +1,14 @@
export type EventHandler = (payload: T) => void
-export interface Event {
+export interface Event extends EventOptions {
name: K
handler: EventHandler
}
+export interface EventOptions {
+ once?: boolean
+}
export interface EventManagerFuncs {
- on(name: K, handler: EventHandler): void
+ on(name: K, handler: EventHandler, opts?: EventOptions): void
off(name: K, handler: EventHandler): void
trigger(name: K, payload?: PayloadMap[K]): void
}
@@ -16,8 +19,8 @@ export default class EventManager implements EventManagerFuncs(name: K, handler: EventHandler) {
- this.events.push({ name, handler: handler as EventHandler })
+ public on(name: K, handler: EventHandler, opts: EventOptions = {}) {
+ this.events.push({ name, handler: handler as EventHandler, ...opts })
}
/**
@@ -35,6 +38,9 @@ export default class EventManager implements EventManagerFuncs(name: K, payload?: PayloadMap[K]) {
this.events
.filter((evt) => evt.name === name && typeof evt.handler === 'function')
- .forEach((evt) => evt.handler(payload!))
+ .forEach((evt) => {
+ evt.handler(payload!)
+ if (evt.once) this.off(name, evt.handler)
+ })
}
}
diff --git a/ui/packages/artalk/src/list/comment.ts b/ui/packages/artalk/src/list/comment.ts
new file mode 100644
index 000000000..27e86bd28
--- /dev/null
+++ b/ui/packages/artalk/src/list/comment.ts
@@ -0,0 +1,21 @@
+import { CommentData } from '~/types/artalk-data'
+import Context from '~/types/context'
+import Comment from '../comment/comment'
+
+export function createComment(ctx: Context, comment: CommentData, ctxComments: CommentData[]): Comment {
+ const instance = new Comment(ctx, comment, {
+ isFlatMode: ctx.getData().getListLastFetch()?.params.flatMode!,
+ afterRender: () => {
+ ctx.trigger('comment-rendered', instance)
+ },
+ onDelete: (c: Comment) => {
+ ctx.getData().deleteComment(c.getID())
+ },
+ replyTo: (comment.rid ? ctxComments.find(c => c.id === comment.rid) : undefined)
+ })
+
+ // 渲染元素
+ instance.render()
+
+ return instance
+}
diff --git a/ui/packages/artalk/src/list/layout.ts b/ui/packages/artalk/src/list/layout.ts
index bf15a5bdd..be566b96d 100644
--- a/ui/packages/artalk/src/list/layout.ts
+++ b/ui/packages/artalk/src/list/layout.ts
@@ -9,15 +9,15 @@ export interface LayoutOptions {
nestMax: number
flatMode: boolean
- createComment(comment: CommentData, ctxComments: CommentData[]): Comment
- findComment(id: number): Comment|undefined
+ createCommentNode(comment: CommentData, ctxComments: CommentData[]): Comment
+ findCommentNode(id: number): Comment|undefined
getCommentDataList(): CommentData[]
}
export default class ListLayout {
constructor(private options: LayoutOptions) {}
- // TODO refactor if syntax to strategy pattern
+ // TODO refactor `if syntax` to strategy pattern
import(comments: CommentData[]) {
if (this.options.flatMode) {
comments.forEach((commentData: CommentData) => {
@@ -41,18 +41,17 @@ export default class ListLayout {
// 遍历 root 评论
const rootNodes = ListNest.makeNestCommentNodeList(srcData, this.options.nestSortBy, this.options.nestMax)
rootNodes.forEach((rootNode: ListNest.CommentNode) => {
- const rootC = this.options.createComment(rootNode.comment, srcData)
+ const rootC = this.options.createCommentNode(rootNode.comment, srcData)
// 显示并播放渐入动画
this.options.$commentsWrap?.appendChild(rootC.getEl())
rootC.getRender().playFadeAnim()
// 加载子评论
- const that = this
- ;(function loadChildren(parentC: Comment, parentNode: ListNest.CommentNode) {
+ const loadChildren = (parentC: Comment, parentNode: ListNest.CommentNode) => {
parentNode.children.forEach((node: ListNest.CommentNode) => {
const childD = node.comment
- const childC = that.options.createComment(childD, srcData)
+ const childC = this.options.createCommentNode(childD, srcData)
// 插入到父评论中
parentC.putChild(childC)
@@ -60,7 +59,8 @@ export default class ListLayout {
// 递归加载子评论
loadChildren(childC, node)
})
- })(rootC, rootNode)
+ }
+ loadChildren(rootC, rootNode)
// 限高检测
rootC.getRender().checkHeightLimit()
@@ -70,7 +70,7 @@ export default class ListLayout {
/** 导入评论 · 平铺模式 */
private putCommentFlatMode(cData: CommentData, ctxData: CommentData[], insertMode: 'append'|'prepend') {
if (cData.is_collapsed) cData.is_allow_reply = false
- const comment = this.options.createComment(cData, ctxData)
+ const comment = this.options.createCommentNode(cData, ctxData)
// 可见评论添加到界面
// 注:不可见评论用于显示 “引用内容”
@@ -88,14 +88,14 @@ export default class ListLayout {
private insertCommentNest(commentData: CommentData) {
// 嵌套模式
- const comment = this.options.createComment(commentData, this.options.getCommentDataList())
+ const comment = this.options.createCommentNode(commentData, this.options.getCommentDataList())
if (commentData.rid === 0) {
// root评论 新增
this.options.$commentsWrap?.prepend(comment.getEl())
} else {
// 子评论 新增
- const parent = this.options.findComment(commentData.rid)
+ const parent = this.options.findCommentNode(commentData.rid)
if (parent) {
parent.putChild(comment, (this.options.nestSortBy === 'DATE_ASC' ? 'append' : 'prepend'))
diff --git a/ui/packages/artalk/src/list/list.ts b/ui/packages/artalk/src/list/list.ts
index c63f45a5b..7452f20e0 100644
--- a/ui/packages/artalk/src/list/list.ts
+++ b/ui/packages/artalk/src/list/list.ts
@@ -1,342 +1,86 @@
-import { ListData, CommentData } from '~/types/artalk-data'
import Context from '~/types/context'
-import type ArtalkConfig from '~/types/artalk-config'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
-import * as Ui from '../lib/ui'
-import Comment from '../comment/comment'
-import type { ListOptions } from './options'
-import PgHolder, { TPgMode } from './paginator'
-import * as ListNest from './nest'
+import CommentNode from '../comment/comment'
import ListHTML from './list.html?raw'
import ListLayout from './layout'
-import { handleBackendRefConf } from '../config'
+import { createComment as createCommentNode } from './comment'
+import { initListPaginatorFunc } from './page'
export default class List extends Component {
- /** The options of List */
- protected options: ListOptions = {}
-
- protected layout: ListLayout
-
/** 列表评论集区域元素 */
- $commentsWrap?: HTMLElement
-
- /** 最后一次请求得到的列表数据 */
- protected data?: ListData
-
- /** 是否处于加载中状态 */
- protected isLoading: boolean = false
+ $commentsWrap!: HTMLElement
+ public getCommentsWrapEl() { return this.$commentsWrap }
- /** 配置是否已加载 */
- private confLoaded = false
+ protected commentNodes: CommentNode[] = []
+ getCommentNodes() { return this.commentNodes }
- /** 分页方式持有者 */
- public pgHolder?: PgHolder
-
- constructor (ctx: Context, options: ListOptions = {}) {
+ // TODO remove options and use ctx.conf instead
+ constructor (ctx: Context) {
super(ctx)
- this.options = options
-
- this.initBaseEl()
-
- // init layout manager
- this.layout = new ListLayout({
- $commentsWrap: this.$commentsWrap!,
- nestSortBy: this.getNestSortBy(),
- nestMax: this.ctx.conf.nestMax,
- flatMode: this.getFlatMode(),
- createComment: (d, c) => this.createComment(d, c),
- findComment: (id) => this.ctx.findComment(id),
- getCommentDataList: () => this.ctx.getCommentDataList()
- })
-
- // 事件监听
- this.ctx.on('conf-loaded', () => {
- })
-
- this.ctx.on('list-loaded', () => {
- // 防止评论框被吞
- this.ctx.editorResetState()
- })
- }
-
- getOptions() {
- return this.options
- }
-
- /** 嵌套模式下的排序方式 */
- getNestSortBy(): ListNest.SortByType {
- return this.options.nestSortBy || this.ctx.conf.nestSort || 'DATE_ASC'
- }
-
- /** 平铺模式 */
- getFlatMode(): boolean {
- if (this.options.flatMode !== undefined)
- return this.options.flatMode
-
- // 配置开启平铺模式
- if (this.ctx.conf.flatMode === true || Number(this.ctx.conf.nestMax) <= 1)
- return true
-
- // 自动判断启用平铺模式
- if (this.ctx.conf.flatMode === 'auto' && window.matchMedia("(max-width: 768px)").matches)
- return true
-
- return false
- }
-
- /** 分页方式 */
- getPageMode(): TPgMode {
- return this.options.pageMode || (this.conf.pagination.readMore ? 'read-more' : 'pagination')
- }
-
- /** 每页数量 (每次请求获取量) */
- getPageSize(): number {
- return this.options.pageSize || this.conf.pagination.pageSize
- }
-
- public getData() {
- return this.data
- }
-
- public clearData() {
- this.data = undefined
- }
-
- public getLoading() {
- return this.isLoading
- }
-
- public getCommentsWrapEl() {
- return this.$commentsWrap!
- }
-
- private initBaseEl() {
+ // Init base element
this.$el = Utils.createElement(
`
`)
- this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
- if (!this.options.liteMode) {
+ if (!this.ctx.conf.listLiteMode) {
const el = Utils.createElement(ListHTML)
el.querySelector('.atk-list-body')!.append(this.$el) // 把 list 的 $el 变为子元素
this.$el = el
-
- this.options.repositionAt = this.$el // 更新翻页归位元素
- }
- }
-
- /** 设置加载状态 */
- public setLoading(val: boolean, isFirstLoad: boolean = false) {
- this.isLoading = val
- if (isFirstLoad) {
- Ui.setLoading(val, this.$el)
- return
- }
-
- this.pgHolder?.setLoading(val)
- }
-
- /** 评论获取 */
- public async fetchComments(offset: number) {
- if (this.isLoading) return
-
- const isFirstLoad = (offset === 0)
- const setLoading = (val: boolean) => this.setLoading(val, isFirstLoad)
-
- // 加载动画
- setLoading(true)
-
- // 事件通知(开始加载评论)
- this.ctx.trigger('list-load')
-
- // 清空评论(按钮加载更多的第一页、每次加载分页页面)
- const pageMode = this.getPageMode()
- if ((pageMode === 'read-more' && isFirstLoad) || pageMode === 'pagination') {
- this.clearAllComments()
- }
-
- // 请求评论数据
- let listData: ListData
- try {
- // 执行请求
- listData = await this.ctx.getApi().comment
- .get(offset, this.getPageSize(), this.getFlatMode(), this.options.paramsEditor)
- } catch (e: any) {
- this.onError(e.msg || String(e), offset, e.data)
- throw e
- } finally {
- setLoading(false)
}
- // 清除原有错误
- Ui.setError(this.$el, null)
-
- // 装载数据
- try {
- this.onLoad(listData, offset)
- } catch (e: any) {
- this.onError(String(e), offset)
- throw e
- } finally {
- setLoading(false)
- }
- }
-
- protected onLoad(data: ListData, offset: number) {
- this.data = data
-
- // 装载后端提供的配置
- this.loadConf(data)
-
- // 导入数据
- this.importComments(data.comments)
-
- // 分页功能
- this.loadPagination(offset, (this.getFlatMode() ? data.total : data.total_roots))
-
- // 更新页面数据
- this.ctx.updatePage(data.page)
-
- // 未读消息提示功能
- this.ctx.updateUnreadList(data.unread || [])
-
- // 事件触发,列表已加载
- this.ctx.trigger('list-loaded')
- }
-
- private loadConf(data: ListData) {
- if (!this.confLoaded) { // 仅应用一次配置
- let conf: Partial = {
- apiVersion: data.api_version.version
- }
-
- // reference conf from backend
- if (this.conf.useBackendConf) {
- conf = { ...conf, ...handleBackendRefConf(data.conf.frontend_conf) }
- }
-
- this.ctx.updateConf(conf)
- this.confLoaded = true
- }
- }
-
- /** 分页模式 */
- private loadPagination(offset: number, total: number) {
- // 初始化
- if (!this.pgHolder) {
- this.pgHolder = new PgHolder({
- list: this,
- mode: this.getPageMode(),
- pageSize: this.getPageSize(),
- total
- })
- }
-
- // 更新
- this.pgHolder?.update(offset, total)
- }
-
- /** 错误处理 */
- protected onError(msg: any, offset: number, errData?: any) {
- if (!this.confLoaded) {
- this.ctx.updateConf({})
- }
-
- msg = String(msg)
- console.error(msg)
-
- // 加载更多按钮显示错误
- if (offset !== 0 && this.getPageMode() === 'read-more') {
- this.pgHolder?.showErr(this.$t('loadFail'))
- return
- }
+ this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
- // 显示错误对话框
- this.ctx.trigger('list-error', {
- msg, data: errData
- })
- }
+ // Init paginator
+ initListPaginatorFunc(ctx)
- /** 重新加载列表 */
- public reload() {
- this.fetchComments(0)
+ // Bind events
+ this.initCrudEvents()
}
- /** 创建新评论 */
- private createComment(comment: CommentData, ctxComments: CommentData[]): Comment {
- const instance = new Comment(this.ctx, comment, {
- isFlatMode: this.getFlatMode(),
- afterRender: () => {
- const renderCommentFn = this.options.renderComment
- renderCommentFn && renderCommentFn(instance)
- },
- onDelete: (c: Comment) => {
- this.deleteComment(c.getID())
+ getListLayout() {
+ return new ListLayout({
+ $commentsWrap: this.$commentsWrap,
+ nestSortBy: this.ctx.conf.nestSort,
+ nestMax: this.ctx.conf.nestMax,
+ flatMode: this.ctx.conf.flatMode as boolean,
+ // flatMode must be boolean because it had been handled when Artalk.init
+ createCommentNode: (d, c) => {
+ const node = createCommentNode(this.ctx, d, c)
+ this.commentNodes.push(node)
+ return node
},
- replyTo: (comment.rid ? ctxComments.find(c => c.id === comment.rid) : undefined)
+ findCommentNode: (id) => this.commentNodes.find(c => c.getID() === id),
+ getCommentDataList: () => this.ctx.getData().getComments(),
})
-
- // 渲染元素
- instance.render()
-
- // 放入 comment 总表中
- this.ctx.getCommentList().push(instance)
-
- return instance
- }
-
- /** 导入评论 */
- public importComments(srcData: CommentData[]) {
- this.layout.import(srcData)
}
- /** 新增评论 · 首部添加 */
- public insertComment(commentData: CommentData) {
- this.layout.insert(commentData)
-
- // 评论数增加 1
- if (this.data) this.data.total += 1
-
- // 评论新增后
- this.ctx.trigger('list-loaded')
- this.ctx.trigger('list-inserted', commentData)
- }
-
- /** 更新评论 */
- public updateComment(commentData: CommentData) {
- const comment = this.ctx.findComment(commentData.id)
- comment && comment.setData(commentData)
-
- this.ctx.trigger('list-loaded')
- }
-
- /** 删除评论 */
- public deleteComment(id: number) {
- const comment = this.ctx.findComment(id)
- if (!comment) throw Error(`Comment ${id} cannot be found`)
-
- comment.getEl().remove()
-
- const list = this.ctx.getCommentList()
- list.splice(list.indexOf(comment), 1)
-
- // 评论数减 1
- if (this.data) this.data.total -= 1
+ private initCrudEvents() {
+ this.ctx.on('list-load', (comments) => {
+ // 导入数据
+ this.getListLayout().import(comments)
+ })
- // 评论删除后
- this.ctx.trigger('list-loaded')
- this.ctx.trigger('list-deleted', comment.getData())
- }
+ // When comment insert
+ this.ctx.on('comment-inserted', (comment) => {
+ this.getListLayout().insert(comment)
+ })
- /** 删除全部评论 */
- public clearAllComments() {
- this.getCommentsWrapEl().innerHTML = ''
- this.clearData()
+ // When comment delete
+ this.ctx.on('comment-deleted', (comment) => {
+ const node = this.commentNodes.find(c => c.getID() === comment.id)
+ if (!node) { console.error(`comment node id=${comment.id} not found`);return }
+ node.getEl().remove()
+ this.commentNodes = this.commentNodes.filter(c => c.getID() !== comment.id)
+ // TODO remove child nodes
+ })
- this.ctx.clearCommentList()
- this.ctx.trigger('list-loaded')
+ // When comment update
+ this.ctx.on('comment-updated', (comment) => {
+ const node = this.commentNodes.find(c => c.getID() === comment.id)
+ node && node.setData(comment)
+ })
}
}
diff --git a/ui/packages/artalk/src/list/options.ts b/ui/packages/artalk/src/list/options.ts
deleted file mode 100644
index 79ebbf606..000000000
--- a/ui/packages/artalk/src/list/options.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { TPgMode } from './paginator'
-import type { SortByType } from './nest'
-import type Comment from '../comment/comment'
-
-export interface ListOptions {
- /** Lite mode */
- liteMode?: boolean
-
- /** Flat mode */
- flatMode?: boolean
-
- /** Pagination mode */
- pageMode?: TPgMode
-
- /** Page size */
- pageSize?: number
-
- /** 监听指定元素上的滚动 */
- scrollListenerAt?: HTMLElement
-
- /** 翻页归位到指定元素 */
- repositionAt?: HTMLElement
-
- /** 启用列表未读高亮 */
- unreadHighlight?: boolean
-
- /** Sort condition in nest mode */
- nestSortBy?: SortByType
-
- /** Text to show when no comment */
- noCommentText?: string
-
- // 一些 Hook 函数
- // ----------------
- renderComment?: (comment: Comment) => void
- paramsEditor?: (params: any) => void
-}
diff --git a/ui/packages/artalk/src/list/page.ts b/ui/packages/artalk/src/list/page.ts
new file mode 100644
index 000000000..6d11951cb
--- /dev/null
+++ b/ui/packages/artalk/src/list/page.ts
@@ -0,0 +1,94 @@
+import ArtalkConfig from '~/types/artalk-config'
+import type ContextApi from '~/types/context'
+import $t from '@/i18n'
+import { Paginator } from './paginator'
+import ReadMorePaginator from './paginator/read-more'
+import UpDownPaginator from './paginator/up-down'
+
+function createPaginatorByConf(conf: ArtalkConfig): Paginator {
+ if (conf.pagination.readMore) return new ReadMorePaginator()
+ return new UpDownPaginator()
+}
+
+function getPageDataByLastData(ctx: ContextApi): { offset: number, total: number } {
+ const last = ctx.getData().getListLastFetch()
+ const r = { offset: 0, total: 0 }
+ if (!last) return r
+
+ r.offset = last.params.offset
+ if (last.data) r.total = last.params.flatMode ? last.data.total : last.data.total_roots
+
+ return r
+}
+
+export const initListPaginatorFunc = (ctx: ContextApi) => {
+ let paginator: Paginator|null = null
+
+ // Init paginator when conf loaded
+ ctx.on('conf-loaded', (conf) => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ if (paginator) paginator.dispose() // if had been init, dispose it
+
+ // create paginator instance
+ paginator = createPaginatorByConf(conf)
+
+ // create paginator dom
+ const { offset, total } = getPageDataByLastData(ctx)
+ const $paginator = paginator.create({
+ ctx, pageSize: conf.pagination.pageSize, total,
+
+ readMoreAutoLoad: conf.pagination.autoLoad,
+ })
+
+ // mount paginator dom
+ list.$el.append($paginator)
+ })
+
+ // When list loaded
+ ctx.on('list-loaded', (comments) => {
+ // update paginator info
+ const { offset, total } = getPageDataByLastData(ctx)
+ paginator?.update(offset, total)
+ })
+
+ // When list fetch
+ ctx.on('list-fetch', (params) => {
+ // if clear comments when fetch new page data
+ if (paginator?.getIsClearComments(params)) {
+ ctx.getData().clearComments()
+ }
+ })
+
+ // When list error
+ ctx.on('list-error', () => {
+ paginator?.showErr?.($t('loadFail'))
+ })
+
+ // List goto auto next page when comment not found
+ const autoSwitchPageForFindComment = (commentID: number) => {
+ const comment = ctx.getData().findComment(commentID)
+ if (!!comment || !paginator?.getHasMore()) return
+
+ // TODO 自动范围改为直接跳转到计算后的页面
+ paginator?.next()
+
+ // wait for list loaded
+ ctx.on('list-loaded', () => {
+ autoSwitchPageForFindComment(commentID) // recursive, until comment found or no more page
+ }, { once: true })
+ }
+
+ ctx.on('list-goto', (commentID) => {
+ autoSwitchPageForFindComment(commentID)
+ })
+
+ // loading
+ ctx.on('list-fetch', (params) => {
+ paginator?.setLoading(true)
+ })
+ ctx.on('list-fetched', ({ params }) => {
+ paginator?.setLoading(false)
+ })
+}
diff --git a/ui/packages/artalk/src/list/paginator/adaptors/index.ts b/ui/packages/artalk/src/list/paginator/adaptors/index.ts
deleted file mode 100644
index 9de1ada21..000000000
--- a/ui/packages/artalk/src/list/paginator/adaptors/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { IPgHolderConf, TPgMode } from '../index'
-import PaginationAdaptor from './pagination'
-import ReadMoreAdaptor from './read-more'
-
-// 分页方式适配器
-const Adaptors: Record> = {
- 'pagination': PaginationAdaptor,
- 'read-more': ReadMoreAdaptor
-}
-
-export default Adaptors
-
-export interface IPgAdaptor {
- instance: T
- el: HTMLElement
- createInstance(conf: IPgHolderConf): [T, HTMLElement]
- setLoading(val: boolean): void
- update(offset: number, total: number): void
- next(): void
- showErr?(msg: string): void
-}
diff --git a/ui/packages/artalk/src/list/paginator/adaptors/pagination.ts b/ui/packages/artalk/src/list/paginator/adaptors/pagination.ts
deleted file mode 100644
index c143329d3..000000000
--- a/ui/packages/artalk/src/list/paginator/adaptors/pagination.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import Pagination from '@/components/pagination'
-import * as Utils from '@/lib/utils'
-import { IPgAdaptor } from '.'
-
-interface IPaginationAdaptor extends IPgAdaptor {}
-
-/**
- * 翻页形式的分页
- */
-export default {
- createInstance(conf) {
- const instance = new Pagination(conf.total, {
- pageSize: conf.pageSize,
- onChange: async (o) => {
- conf.list.ctx.editorResetState() // 防止评论框被吞
-
- await conf.list.fetchComments(o)
-
- // 滚动到第一个评论的位置
- const repositionAt = conf.list.getOptions().repositionAt
- if (repositionAt) {
- const at = conf.list.getOptions().scrollListenerAt || window
- at.scroll({
- top: repositionAt ? Utils.getOffset(repositionAt).top : 0,
- left: 0,
- })
- }
- }
- })
-
- return [instance, instance.$el]
- },
- setLoading(val) {
- this.instance.setLoading(val)
- },
- update(offset, total) {
- this.instance.update(offset, total)
- },
- next() {
- this.instance.next()
- }
-}
diff --git a/ui/packages/artalk/src/list/paginator/adaptors/read-more.ts b/ui/packages/artalk/src/list/paginator/adaptors/read-more.ts
deleted file mode 100644
index 08b550a60..000000000
--- a/ui/packages/artalk/src/list/paginator/adaptors/read-more.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import ReadMoreBtn from '@/components/read-more-btn'
-import $t from '@/i18n'
-import * as Ui from '@/lib/ui'
-import { IPgAdaptor } from '.'
-
-interface IReadMoreAdaptor extends IPgAdaptor {
- autoLoadScrollEvent?: () => void
-}
-
-/**
- * 阅读更多形式的分页
- */
-export default {
- createInstance(conf) {
- const readMoreBtn = new ReadMoreBtn({
- pageSize: conf.pageSize,
- onClick: async (o) => {
- await conf.list.fetchComments(o)
- },
- text: $t('loadMore'),
- })
-
- // 滚动到底部自动加载
- if (conf.list.conf.pagination.autoLoad) {
- // 添加滚动事件监听
- const at = conf.list.getOptions().scrollListenerAt || document
- if (this.autoLoadScrollEvent) at.removeEventListener('scroll', this.autoLoadScrollEvent) // 解除原有
- this.autoLoadScrollEvent = () => {
- if (conf.mode !== 'read-more'
- || !readMoreBtn?.hasMore
- || conf.list.getLoading()
- ) return
-
- const $target = conf.list.$el.querySelector('.atk-list-comments-wrap > .atk-comment-wrap:nth-last-child(3)') // 获取倒数第3个评论元素
- if ($target && Ui.isVisible($target, conf.list.getOptions().scrollListenerAt)) {
- readMoreBtn.click() // 自动点击加载更多按钮
- }
- }
- at.addEventListener('scroll', this.autoLoadScrollEvent)
- }
-
- return [readMoreBtn, readMoreBtn.$el]
- },
- setLoading(val) {
- this.instance.setLoading(val)
- },
- update(offset, total) {
- this.instance.update(offset, total)
- },
- showErr(msg) {
- this.instance.showErr(msg)
- },
- next() {
- this.instance.click()
- }
-}
diff --git a/ui/packages/artalk/src/list/paginator/index.ts b/ui/packages/artalk/src/list/paginator/index.ts
index d5bd4ec6d..75dcee918 100644
--- a/ui/packages/artalk/src/list/paginator/index.ts
+++ b/ui/packages/artalk/src/list/paginator/index.ts
@@ -1,65 +1,26 @@
-import List from '../list'
-import Adaptors from './adaptors'
+import type ContextApi from '~/types/context'
+import type { ListFetchParams } from '~/types/artalk-data'
+import UpDownPaginator from './up-down'
+import ReadMorePaginator from './read-more'
-export type TPgMode = 'pagination'|'read-more'
-
-export interface IPgHolderConf {
- list: List
- mode: TPgMode
+export interface IPgHolderOpt {
+ ctx: ContextApi
total: number
pageSize: number
-}
-
-/**
- * 分页方式持有者(调度器)
- */
-export default class PgHolder {
- private conf: IPgHolderConf
-
- constructor(conf: IPgHolderConf) {
- this.conf = conf
- this.init()
- }
-
- public getAdaptor() {
- return Adaptors[this.conf.mode]
- }
-
- public init() {
- const adaptor = this.getAdaptor()
- const [instance, el] = adaptor.createInstance(this.conf)
- adaptor.instance = instance
- adaptor.el = el
- this.conf.list.$el.append(adaptor.el)
- }
- public setLoading(val: boolean) {
- this.getAdaptor().setLoading(val)
- }
-
- public update(offset: number, total: number) {
- this.getAdaptor().update(offset, total)
- }
-
- public getEl() {
- return this.getAdaptor().el
- }
-
- public showErr(msg: string) {
- const that = this.getAdaptor()
- const func = that.showErr
- if (func) func.bind(that)(msg)
- }
-
- public setMode(val: TPgMode) {
- if (val !== this.conf.mode) {
- this.getEl().remove()
- this.conf.mode = val
- this.init()
- }
- }
+ readMoreAutoLoad?: boolean
+}
- public next() {
- this.getAdaptor().next()
- }
+export interface Paginator {
+ create(opts: IPgHolderOpt): HTMLElement
+ setLoading(val: boolean): void
+ update(offset: number, total: number): void
+ next(): void
+ showErr?(msg: string): void
+ getHasMore(): boolean
+ /** Clear comments when fetch new page data */
+ getIsClearComments(params: Partial): boolean
+ dispose(): void
}
+
+export default { UpDownPaginator, ReadMorePaginator }
diff --git a/ui/packages/artalk/src/list/paginator/read-more.ts b/ui/packages/artalk/src/list/paginator/read-more.ts
new file mode 100644
index 000000000..4239c1538
--- /dev/null
+++ b/ui/packages/artalk/src/list/paginator/read-more.ts
@@ -0,0 +1,68 @@
+import type { ListFetchParams } from '~/types/artalk-data'
+import ReadMoreBtn from '@/components/read-more-btn'
+import $t from '@/i18n'
+import { Paginator, IPgHolderOpt } from '.'
+
+/**
+ * 阅读更多形式的分页
+ */
+export default class ReadMorePaginator implements Paginator {
+ private instance!: ReadMoreBtn
+ private onReachedBottom: (() => void) | null = null
+ private opt!: IPgHolderOpt
+
+ create(opt: IPgHolderOpt) {
+ this.opt = opt
+
+ this.instance = new ReadMoreBtn({
+ pageSize: opt.pageSize,
+ onClick: async (o) => {
+ opt.ctx.fetch({
+ offset: o,
+ })
+ },
+ text: $t('loadMore'),
+ })
+
+ // 滚动到底部自动加载
+ if (opt.readMoreAutoLoad) {
+ this.onReachedBottom = () => {
+ if (!this.instance.hasMore || opt.ctx.getData().getLoading()) return
+ this.instance.click()
+ }
+
+ opt.ctx.on('list-reach-bottom', this.onReachedBottom)
+ }
+
+ return this.instance.$el
+ }
+
+ setLoading(val: boolean) {
+ this.instance.setLoading(val)
+ }
+
+ update(offset: number, total: number) {
+ this.instance.update(offset, total)
+ }
+
+ showErr(msg: string) {
+ this.instance.showErr(msg)
+ }
+
+ next() {
+ this.instance.click()
+ }
+
+ getHasMore(): boolean {
+ return this.instance.hasMore
+ }
+
+ getIsClearComments(params: Partial): boolean {
+ return params.offset === 0
+ }
+
+ dispose(): void {
+ if (this.onReachedBottom) this.opt.ctx.off('list-reach-bottom', this.onReachedBottom)
+ this.instance.$el.remove()
+ }
+}
diff --git a/ui/packages/artalk/src/list/paginator/up-down.ts b/ui/packages/artalk/src/list/paginator/up-down.ts
new file mode 100644
index 000000000..dd86efd8a
--- /dev/null
+++ b/ui/packages/artalk/src/list/paginator/up-down.ts
@@ -0,0 +1,51 @@
+import PaginationComponent from '@/components/pagination'
+import { Paginator, IPgHolderOpt } from '.'
+
+/**
+ * 翻页形式的分页
+ */
+export default class UpDownPaginator implements Paginator {
+ private instance!: PaginationComponent
+
+ create(opt: IPgHolderOpt) {
+ this.instance = new PaginationComponent(opt.total, {
+ pageSize: opt.pageSize,
+ onChange: async (o) => {
+ opt.ctx.editorResetState() // 防止评论框被吞
+
+ opt.ctx.fetch({
+ offset: o,
+ onSuccess: () => {
+ opt.ctx.listGotoFirst()
+ }
+ })
+ }
+ })
+
+ return this.instance.$el
+ }
+
+ setLoading(val: boolean) {
+ this.instance.setLoading(val)
+ }
+
+ update(offset: number, total: number) {
+ this.instance.update(offset, total)
+ }
+
+ next() {
+ this.instance.next()
+ }
+
+ getHasMore(): boolean {
+ return this.instance.getHasMore()
+ }
+
+ getIsClearComments(): boolean {
+ return true
+ }
+
+ dispose(): void {
+ this.instance.$el.remove()
+ }
+}
diff --git a/ui/packages/artalk/src/plugins/editor/header-user.ts b/ui/packages/artalk/src/plugins/editor/header-user.ts
index 49ba6a481..560463a7f 100644
--- a/ui/packages/artalk/src/plugins/editor/header-user.ts
+++ b/ui/packages/artalk/src/plugins/editor/header-user.ts
@@ -79,7 +79,7 @@ export default class HeaderUser extends EditorPlug {
if (!data.is_login) User.logout()
// Update unread notifies
- this.kit.useGlobalCtx().updateUnreadList(data.unread)
+ this.kit.useGlobalCtx().getData().updateUnreads(data.unread)
// If user is admin and not login,
if (User.checkHasBasicUserInfo() && !data.is_login && data.user?.is_admin) {
diff --git a/ui/packages/artalk/src/plugins/editor/state-edit.ts b/ui/packages/artalk/src/plugins/editor/state-edit.ts
index 0b7f90e2b..ee8eb31e5 100644
--- a/ui/packages/artalk/src/plugins/editor/state-edit.ts
+++ b/ui/packages/artalk/src/plugins/editor/state-edit.ts
@@ -41,7 +41,7 @@ export default class StateEdit extends EditorPlug {
return nComment
},
post: (nComment: CommentData) => {
- this.kit.useGlobalCtx().updateComment(nComment)
+ this.kit.useGlobalCtx().getData().updateComment(nComment)
}
})
})
diff --git a/ui/packages/artalk/src/plugins/editor/submit-add.ts b/ui/packages/artalk/src/plugins/editor/submit-add.ts
index 938f92ef6..122f1a760 100644
--- a/ui/packages/artalk/src/plugins/editor/submit-add.ts
+++ b/ui/packages/artalk/src/plugins/editor/submit-add.ts
@@ -28,6 +28,6 @@ export default class SubmitAddPreset {
postSubmitAdd(commentNew: CommentData) {
// insert the new comment to list
- this.kit.useGlobalCtx().insertComment(commentNew)
+ this.kit.useGlobalCtx().getData().insertComment(commentNew)
}
}
diff --git a/ui/packages/artalk/src/plugins/index.ts b/ui/packages/artalk/src/plugins/index.ts
index f4e4dd3b6..fb869d8c8 100644
--- a/ui/packages/artalk/src/plugins/index.ts
+++ b/ui/packages/artalk/src/plugins/index.ts
@@ -1,27 +1,10 @@
-import ArtalkPlugin from '~/types/plugin'
+import type ArtalkPlugin from '~/types/plugin'
import { EditorKit } from './editor-kit'
-import * as Stat from './stat'
-import { ListCloseEditor } from './list-close-editor'
+import { ListPlugins } from './list'
+import { PvCountWidget } from './stat'
import { VersionCheck } from './version-check'
-import { Unread } from './unread'
-import { ListCount } from './list-count'
-import { ListSidebarBtn } from './list-sidebar-btn'
-import { ListUnreadBadge } from './list-unread-badge'
-import { ListGoto } from './list-goto'
-import { ListCopyright } from './list-copyright'
-import { ListNoComment } from './list-no-comment'
-import { ListDropdown } from './list-dropdown'
-import { ListTimeTicking } from './list-time-ticking'
-import { ListErrorDialog } from './list-error-dialog'
-
-const ListPlugins: ArtalkPlugin[] = [
- ListCloseEditor, ListCount, ListSidebarBtn,
- ListUnreadBadge, ListDropdown, ListGoto, ListNoComment, ListCopyright,
- ListTimeTicking, ListErrorDialog
-]
export const DefaultPlugins: ArtalkPlugin[] = [
- EditorKit, Stat.PvCountWidget, VersionCheck, Unread,
-
- ...ListPlugins
+ EditorKit, ...ListPlugins,
+ PvCountWidget, VersionCheck
]
diff --git a/ui/packages/artalk/src/plugins/list-goto.ts b/ui/packages/artalk/src/plugins/list-goto.ts
deleted file mode 100644
index aba0ebf87..000000000
--- a/ui/packages/artalk/src/plugins/list-goto.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import ArtalkPlugin from '~/types/plugin'
-import * as Utils from '@/lib/utils'
-import * as Ui from '@/lib/ui'
-import Comment from '@/comment/comment'
-
-export const ListGoto: ArtalkPlugin = (ctx) => {
- const check = (delayGoto = true) => {
- const list = ctx.get('list')
- if (!list) return
-
- const commentID = extractCommentID()
- if (!commentID) return
-
- // 自动翻页
- const comment = ctx.findComment(commentID)
- if (!comment) { // 若找不到评论
- // TODO 自动范围改为直接跳转到计算后的页面
- list.pgHolder?.next()
- return
- }
-
- // trigger event
- ctx.trigger('list-goto', commentID)
-
- // goto comment
- gotoComment(comment, delayGoto)
- }
-
- // bind events
- ctx.on('list-loaded', () => { check() })
- window.addEventListener('hashchange', () => { check(false) })
-}
-
-function extractCommentID(): number|null {
- // try get from query
- let commentId = Number(Utils.getQueryParam('atk_comment')) // same as backend GetReplyLink()
-
- // fail over to get from hash
- if (!commentId) {
- const match = window.location.hash.match(/#atk-comment-([0-9]+)/)
- if (!match || !match[1] || Number.isNaN(Number(match[1]))) return null
- commentId = Number(match[1])
- }
-
- return commentId || null
-}
-
-function gotoComment(comment: Comment, delayGoto: boolean = true) {
- // 若父评论存在 “子评论部分” 限高,取消限高
- comment.getParents().forEach((p) => {
- p.getRender().heightLimitRemoveForChildren()
- })
-
- const goTo = () => {
- Ui.scrollIntoView(comment.getEl(), false)
-
- comment.getEl().classList.remove('atk-flash-once')
- window.setTimeout(() => {
- comment.getEl().classList.add('atk-flash-once')
- }, 150)
- }
-
- if (!delayGoto) goTo()
- else window.setTimeout(() => goTo(), 350)
-}
diff --git a/ui/packages/artalk/src/plugins/list-copyright.ts b/ui/packages/artalk/src/plugins/list/copyright.ts
similarity index 89%
rename from ui/packages/artalk/src/plugins/list-copyright.ts
rename to ui/packages/artalk/src/plugins/list/copyright.ts
index ab7aee692..859ea7d7c 100644
--- a/ui/packages/artalk/src/plugins/list-copyright.ts
+++ b/ui/packages/artalk/src/plugins/list/copyright.ts
@@ -1,7 +1,7 @@
import ArtalkPlugin from '~/types/plugin'
import { version as ARTALK_VERSION } from '~/package.json'
-export const ListCopyright: ArtalkPlugin = (ctx) => {
+export const Copyright: ArtalkPlugin = (ctx) => {
ctx.on('conf-loaded', () => {
const list = ctx.get('list')
if (!list) return
diff --git a/ui/packages/artalk/src/plugins/list-count.ts b/ui/packages/artalk/src/plugins/list/count.ts
similarity index 55%
rename from ui/packages/artalk/src/plugins/list-count.ts
rename to ui/packages/artalk/src/plugins/list/count.ts
index 3e4152032..2772d2657 100644
--- a/ui/packages/artalk/src/plugins/list-count.ts
+++ b/ui/packages/artalk/src/plugins/list/count.ts
@@ -2,7 +2,7 @@ import ArtalkPlugin from '~/types/plugin'
import * as Utils from '@/lib/utils'
import $t from '@/i18n'
-export const ListCount: ArtalkPlugin = (ctx) => {
+export const Count: ArtalkPlugin = (ctx) => {
const refreshCountNumEl = () => {
const list = ctx.get('list')
if (!list) return
@@ -10,11 +10,23 @@ export const ListCount: ArtalkPlugin = (ctx) => {
const $count = list.$el.querySelector('.atk-comment-count .atk-text')
if (!$count) return
- const text = Utils.htmlEncode($t('counter', { count: `${Number(list.getData()?.total) || 0}` }))
+ const text = Utils.htmlEncode($t('counter', { count: `${Number(ctx.getData().getListLastFetch()?.data?.total) || 0}` }))
$count.innerHTML = text.replace(/(\d+)/, '')
}
ctx.on('list-loaded', () => {
refreshCountNumEl()
})
+
+ ctx.on('comment-inserted', () => {
+ // 评论数增加 1
+ const last = ctx.getData().getListLastFetch()
+ if (last?.data) last.data.total += 1
+ })
+
+ ctx.on('comment-deleted', () => {
+ // 评论数减 1
+ const last = ctx.getData().getListLastFetch()
+ if (last?.data) last.data.total -= 1
+ })
}
diff --git a/ui/packages/artalk/src/plugins/list-dropdown.ts b/ui/packages/artalk/src/plugins/list/dropdown.ts
similarity index 94%
rename from ui/packages/artalk/src/plugins/list-dropdown.ts
rename to ui/packages/artalk/src/plugins/list/dropdown.ts
index 94369710d..7f1ccfcf9 100644
--- a/ui/packages/artalk/src/plugins/list-dropdown.ts
+++ b/ui/packages/artalk/src/plugins/list/dropdown.ts
@@ -2,9 +2,9 @@ import ArtalkPlugin from '~/types/plugin'
import * as Utils from '@/lib/utils'
import $t from '@/i18n'
-export const ListDropdown: ArtalkPlugin = (ctx) => {
+export const Dropdown: ArtalkPlugin = (ctx) => {
const reloadUseParamsEditor = (func: (p: any) => void) => {
- ctx.get('list')!.getOptions().paramsEditor = func // TODO impl common request manager instead of list
+ ctx.conf.listFetchParamsModifier = func
ctx.reload()
}
diff --git a/ui/packages/artalk/src/plugins/list-error-dialog.ts b/ui/packages/artalk/src/plugins/list/error-dialog.ts
similarity index 54%
rename from ui/packages/artalk/src/plugins/list-error-dialog.ts
rename to ui/packages/artalk/src/plugins/list/error-dialog.ts
index 5c44ff0fa..9bf9f7b96 100644
--- a/ui/packages/artalk/src/plugins/list-error-dialog.ts
+++ b/ui/packages/artalk/src/plugins/list/error-dialog.ts
@@ -1,22 +1,32 @@
import type ArtalkPlugin from '~/types/plugin'
+import ContextApi from '~/types/context'
import List from '@/list/list'
-import * as Utils from '../lib/utils'
-import * as Ui from '../lib/ui'
-import User from '../lib/user'
-import $t from '../i18n'
+import * as Utils from '../../lib/utils'
+import * as Ui from '../../lib/ui'
+import User from '../../lib/user'
+import $t from '../../i18n'
+
+export const ErrorDialog: ArtalkPlugin = (ctx) => {
+ ctx.on('list-fetch', () => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ // clear the original error when a new fetch is triggered
+ Ui.setError(list.$el, null)
+ })
-export const ListErrorDialog: ArtalkPlugin = (ctx) => {
ctx.on('list-error', (err) => {
- const list = ctx.get('list')!
- Ui.setError(list.$el, renderErrorDialog(list, err.msg, err.data))
+ const list = ctx.get('list')
+ if (!list) return
+ Ui.setError(list.$el, renderErrorDialog(ctx, err.msg, err.data))
})
}
-export function renderErrorDialog(list: List, errMsg: string, errData?: any): HTMLElement {
+export function renderErrorDialog(ctx: ContextApi, errMsg: string, errData?: any): HTMLElement {
const errEl = Utils.createElement(`${errMsg},${$t('listLoadFailMsg')}
`)
const $retryBtn = Utils.createElement(`${$t('listRetry')}`)
- $retryBtn.onclick = () => (list.fetchComments(0))
+ $retryBtn.onclick = () => (ctx.fetch({ offset: 0 }))
errEl.appendChild($retryBtn)
const adminBtn = Utils.createElement(' | 打开控制台')
@@ -28,13 +38,13 @@ export function renderErrorDialog(list: List, errMsg: string, errData?: any): HT
// 找不到站点错误,打开侧边栏并填入创建站点表单
if (errData?.err_no_site) {
const viewLoadParam = {
- create_name: list.ctx.conf.site,
+ create_name: ctx.conf.site,
create_urls: `${window.location.protocol}//${window.location.host}`
}
sidebarView = `sites|${JSON.stringify(viewLoadParam)}`
}
- adminBtn.onclick = () => list.ctx.showSidebar({
+ adminBtn.onclick = () => ctx.showSidebar({
view: sidebarView as any
})
diff --git a/ui/packages/artalk/src/plugins/list/fetch.ts b/ui/packages/artalk/src/plugins/list/fetch.ts
new file mode 100644
index 000000000..c04e07f7e
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/fetch.ts
@@ -0,0 +1,93 @@
+import type ArtalkConfig from '~/types/artalk-config'
+import type { ListFetchParams } from '~/types/artalk-data'
+import type ContextApi from '~/types/context'
+import type ArtalkPlugin from '~/types/plugin'
+import { handleBackendRefConf } from '../../config'
+
+export const Fetch: ArtalkPlugin = (ctx) => {
+ ctx.on('list-fetch', (_params) => {
+ if (ctx.getData().getLoading()) return
+
+ const params: ListFetchParams = {
+ // default params
+ offset: 0,
+ limit: ctx.conf.pagination.pageSize,
+ flatMode: ctx.conf.flatMode as boolean, // always be boolean because had been handled in Artalk.init
+ ..._params
+ }
+
+ // must before other function call
+ ctx.getData().setListLastFetch({
+ params
+ })
+
+ ctx.getApi().comment
+ .get(params.offset, params.limit, params.flatMode)
+ .then((data) => {
+ // Must before all other function call and event trigger,
+ // because it will depend on the lastData
+ // TODO this is global variable, easy to use, but not good, consider to refactor
+ ctx.getData().setListLastFetch({ params, data })
+
+ // 装载后端提供的配置
+ loadConf(ctx, {
+ useBackendConf: ctx.conf.useBackendConf,
+ conf: data.conf.frontend_conf,
+ apiVersion: data.api_version.version
+ })
+
+ // 装置评论
+ ctx.getData().loadComments(data.comments)
+
+ // 更新页面数据
+ ctx.getData().updatePage(data.page)
+
+ // 未读消息提示功能
+ ctx.getData().updateUnreads(data.unread || [])
+
+ params.onSuccess && params.onSuccess(data)
+
+ ctx.trigger('list-fetched', { params, data })
+ })
+ .catch((e) => {
+ // 显示错误对话框
+ const error = {
+ msg: e.msg || String(e),
+ data: e.data
+ }
+
+ params.onError && params.onError(error)
+
+ ctx.trigger('list-error', error)
+ ctx.trigger('list-fetched', { params, error })
+
+ throw e
+ })
+ })
+
+ // When list load error
+ ctx.on('list-error', (err) => {
+ if (!confLoaded) {
+ ctx.updateConf({})
+ }
+ })
+}
+
+let confLoaded = false
+
+function loadConf(ctx: ContextApi, apiRes: { useBackendConf: boolean, conf: any, apiVersion: string }) {
+ if (!confLoaded) { // 仅应用一次配置
+ let conf: Partial = {
+ apiVersion: apiRes.apiVersion
+ }
+
+ // reference conf from backend
+ if (ctx.conf.useBackendConf) {
+ if (!apiRes.conf) throw new Error('The remote backend does not respond to the frontend conf, but `useBackendConf` conf is enabled')
+ conf = { ...conf, ...handleBackendRefConf(apiRes.conf) }
+ }
+
+ ctx.updateConf(conf)
+ confLoaded = true
+ }
+}
diff --git a/ui/packages/artalk/src/plugins/list/goto-first.ts b/ui/packages/artalk/src/plugins/list/goto-first.ts
new file mode 100644
index 000000000..ba1d6a3e4
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/goto-first.ts
@@ -0,0 +1,24 @@
+import type ArtalkPlugin from '~/types/plugin'
+import * as Utils from '@/lib/utils'
+
+/** List scroll to the first comment */
+export const GotoFirst: ArtalkPlugin = (ctx) => {
+ const handler = () => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ // TODO support set custom value to replace (`window`, `list.$el`) with (`conf.xxxAt`, `conf.list.repositionAt`)
+ ;(ctx.conf.listScrollListenerAt || window).scroll({
+ top: Utils.getOffset(list.$el).top,
+ left: 0,
+ })
+ }
+
+ ctx.on('inited', () => {
+ ctx.on('list-goto-first', handler)
+ })
+
+ ctx.on('destroy', () => {
+ ctx.off('list-goto-first', handler)
+ })
+}
diff --git a/ui/packages/artalk/src/plugins/list/goto.ts b/ui/packages/artalk/src/plugins/list/goto.ts
new file mode 100644
index 000000000..9fd296426
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/goto.ts
@@ -0,0 +1,68 @@
+import ArtalkPlugin from '~/types/plugin'
+import * as Utils from '@/lib/utils'
+import * as Ui from '@/lib/ui'
+
+export const Goto: ArtalkPlugin = (ctx) => {
+ let delayGoto = true
+
+ const check = () => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ const commentID = extractCommentID()
+ if (!commentID) return
+
+ // trigger event
+ ctx.trigger('list-goto', commentID)
+
+ // reset delayGoto
+ delayGoto = true
+ }
+
+ // bind events
+ ctx.on('list-loaded', () => { check() })
+ window.addEventListener('hashchange', () => {
+ delayGoto = false
+ check()
+ })
+
+ ctx.on('list-goto', (commentID) => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ // TODO remove get from list
+ const comment = list.getCommentNodes().find(c => c.getID() === commentID)
+ if (!comment) return
+
+ // 若父评论存在 “子评论部分” 限高,取消限高
+ comment.getParents().forEach((p) => {
+ p.getRender().heightLimitRemoveForChildren()
+ })
+
+ const goTo = () => {
+ Ui.scrollIntoView(comment.getEl(), false)
+
+ comment.getEl().classList.remove('atk-flash-once')
+ window.setTimeout(() => {
+ comment.getEl().classList.add('atk-flash-once')
+ }, 150)
+ }
+
+ if (!delayGoto) goTo()
+ else window.setTimeout(() => goTo(), 350)
+ })
+}
+
+function extractCommentID(): number|null {
+ // try get from query
+ let commentId = Number(Utils.getQueryParam('atk_comment')) // same as backend GetReplyLink()
+
+ // fail over to get from hash
+ if (!commentId) {
+ const match = window.location.hash.match(/#atk-comment-([0-9]+)/)
+ if (!match || !match[1] || Number.isNaN(Number(match[1]))) return null
+ commentId = Number(match[1])
+ }
+
+ return commentId || null
+}
diff --git a/ui/packages/artalk/src/plugins/list/index.ts b/ui/packages/artalk/src/plugins/list/index.ts
new file mode 100644
index 000000000..129287a00
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/index.ts
@@ -0,0 +1,25 @@
+import type ArtalkPlugin from '~/types/plugin'
+import { WithEditor } from './with-editor'
+import { Unread } from './unread'
+import { Count } from './count'
+import { SidebarBtn } from './sidebar-btn'
+import { UnreadBadge } from './unread-badge'
+import { Goto } from './goto'
+import { Copyright } from './copyright'
+import { NoComment } from './no-comment'
+import { Dropdown } from './dropdown'
+import { TimeTicking } from './time-ticking'
+import { ErrorDialog } from './error-dialog'
+import { Loading } from './loading'
+import { Fetch } from './fetch'
+import { ReachBottom } from './reach-bottom'
+import { GotoFirst } from './goto-first'
+
+const ListPlugins: ArtalkPlugin[] = [
+ Fetch, Loading, Unread,
+ WithEditor, Count, SidebarBtn, UnreadBadge,
+ Dropdown, Goto, NoComment, Copyright,
+ TimeTicking, ErrorDialog, ReachBottom, GotoFirst
+]
+
+export { ListPlugins }
diff --git a/ui/packages/artalk/src/plugins/list/loading.ts b/ui/packages/artalk/src/plugins/list/loading.ts
new file mode 100644
index 000000000..d6519c29f
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/loading.ts
@@ -0,0 +1,19 @@
+import type ArtalkPlugin from '~/types/plugin'
+import * as Ui from '@/lib/ui'
+
+export const Loading: ArtalkPlugin = (ctx) => {
+ ctx.on('list-fetch', (p) => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ if (p.offset === 0) // only show loading when fetch first page
+ Ui.setLoading(true, list.$el)
+ // else if not first page, show loading in paginator (code not there)
+ })
+
+ ctx.on('list-fetched', () => {
+ const list = ctx.get('list')
+ if (!list) return
+ Ui.setLoading(false, list.$el)
+ })
+}
diff --git a/ui/packages/artalk/src/plugins/list-no-comment.ts b/ui/packages/artalk/src/plugins/list/no-comment.ts
similarity index 68%
rename from ui/packages/artalk/src/plugins/list-no-comment.ts
rename to ui/packages/artalk/src/plugins/list/no-comment.ts
index 59bddb369..ff9e67b5f 100644
--- a/ui/packages/artalk/src/plugins/list-no-comment.ts
+++ b/ui/packages/artalk/src/plugins/list/no-comment.ts
@@ -1,12 +1,12 @@
import type ArtalkPlugin from '~/types/plugin'
import * as Utils from '@/lib/utils'
-export const ListNoComment: ArtalkPlugin = (ctx) => {
- ctx.on('list-loaded', () => {
+export const NoComment: ArtalkPlugin = (ctx) => {
+ ctx.on('list-loaded', (comments) => {
const list = ctx.get('list')!
// 无评论
- const isNoComment = list.ctx.getCommentList().length <= 0
+ const isNoComment = comments.length <= 0
let $noComment = list.getCommentsWrapEl().querySelector('.atk-list-no-comment')
if (isNoComment) {
@@ -14,7 +14,7 @@ export const ListNoComment: ArtalkPlugin = (ctx) => {
$noComment = Utils.createElement('')
// TODO POTENTIAL SECURITY RISK: prefer use insane to filter html tags before set innerHTML
- $noComment.innerHTML = list.getOptions().noCommentText || list.ctx.conf.noComment || list.ctx.$t('noComment')
+ $noComment.innerHTML = list.ctx.conf.noComment || list.ctx.$t('noComment')
list.getCommentsWrapEl().appendChild($noComment)
}
} else {
diff --git a/ui/packages/artalk/src/plugins/list/reach-bottom.ts b/ui/packages/artalk/src/plugins/list/reach-bottom.ts
new file mode 100644
index 000000000..fc856451b
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/reach-bottom.ts
@@ -0,0 +1,37 @@
+import type ArtalkPlugin from '~/types/plugin'
+
+export const ReachBottom: ArtalkPlugin = (ctx) => {
+ const scrollEvtAt = document.documentElement // TODO support ref ctx.conf
+ let observer: IntersectionObserver|null = null
+
+ ctx.on('inited', () => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ // use IntersectionObserver to detect reach bottom
+ const $target = list.$el.querySelector('.atk-list-comments-wrap')
+
+ // check IntersectionObserver support
+ if (!('IntersectionObserver' in window)) {
+ console.warn('IntersectionObserver api not supported')
+ return
+ }
+
+ // eslint-disable-next-line compat/compat
+ observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.intersectionRatio > 0) {
+ ctx.trigger('list-reach-bottom')
+ }
+ })
+ }, {
+ root: scrollEvtAt,
+ threshold: 0,
+ })
+ observer.observe($target!)
+ })
+
+ ctx.on('destroy', () => {
+ observer?.disconnect()
+ })
+}
diff --git a/ui/packages/artalk/src/plugins/list-sidebar-btn.ts b/ui/packages/artalk/src/plugins/list/sidebar-btn.ts
similarity index 94%
rename from ui/packages/artalk/src/plugins/list-sidebar-btn.ts
rename to ui/packages/artalk/src/plugins/list/sidebar-btn.ts
index 8b2b6502c..9f4ea2ed3 100644
--- a/ui/packages/artalk/src/plugins/list-sidebar-btn.ts
+++ b/ui/packages/artalk/src/plugins/list/sidebar-btn.ts
@@ -1,7 +1,7 @@
import ArtalkPlugin from '~/types/plugin'
import $t from '@/i18n'
-export const ListSidebarBtn: ArtalkPlugin = (ctx) => {
+export const SidebarBtn: ArtalkPlugin = (ctx) => {
let $openSidebarBtn: HTMLElement|null = null
ctx.on('conf-loaded', () => {
diff --git a/ui/packages/artalk/src/plugins/list-time-ticking.ts b/ui/packages/artalk/src/plugins/list/time-ticking.ts
similarity index 91%
rename from ui/packages/artalk/src/plugins/list-time-ticking.ts
rename to ui/packages/artalk/src/plugins/list/time-ticking.ts
index 3a62f1cc3..b027e8d3b 100644
--- a/ui/packages/artalk/src/plugins/list-time-ticking.ts
+++ b/ui/packages/artalk/src/plugins/list/time-ticking.ts
@@ -2,7 +2,7 @@ import type ArtalkPlugin from '~/types/plugin'
import * as Utils from '@/lib/utils'
/** 评论时间自动更新 */
-export const ListTimeTicking: ArtalkPlugin = (ctx) => {
+export const TimeTicking: ArtalkPlugin = (ctx) => {
let timer: number|null = null
ctx.on('inited', () => {
diff --git a/ui/packages/artalk/src/plugins/list-unread-badge.ts b/ui/packages/artalk/src/plugins/list/unread-badge.ts
similarity index 78%
rename from ui/packages/artalk/src/plugins/list-unread-badge.ts
rename to ui/packages/artalk/src/plugins/list/unread-badge.ts
index f29f873e1..6c2f3000a 100644
--- a/ui/packages/artalk/src/plugins/list-unread-badge.ts
+++ b/ui/packages/artalk/src/plugins/list/unread-badge.ts
@@ -1,6 +1,6 @@
import ArtalkPlugin from '~/types/plugin'
-export const ListUnreadBadge: ArtalkPlugin = (ctx) => {
+export const UnreadBadge: ArtalkPlugin = (ctx) => {
let $unreadBadge: HTMLElement|null = null
const showUnreadBadge = (count: number) => {
@@ -21,7 +21,7 @@ export const ListUnreadBadge: ArtalkPlugin = (ctx) => {
$unreadBadge = list.$el.querySelector('.atk-unread-badge')
})
- ctx.on('unread-updated', (unreadList) => {
- showUnreadBadge(unreadList.length || 0)
+ ctx.on('unreads-updated', (unreads) => {
+ showUnreadBadge(unreads.length || 0)
})
}
diff --git a/ui/packages/artalk/src/plugins/list/unread.ts b/ui/packages/artalk/src/plugins/list/unread.ts
new file mode 100644
index 000000000..c9a99506f
--- /dev/null
+++ b/ui/packages/artalk/src/plugins/list/unread.ts
@@ -0,0 +1,41 @@
+import ArtalkPlugin from '~/types/plugin'
+import * as Utils from '@/lib/utils'
+
+export const Unread: ArtalkPlugin = (ctx) => {
+ ctx.on('comment-rendered', (comment) => {
+ const list = ctx.get('list')
+ if (!list) return
+
+ // comment unread highlight
+ if (ctx.conf.listUnreadHighlight === true) {
+ const unreads = ctx.getData().getUnreads()
+ const notify = unreads.find(o => o.comment_id === comment.getID())
+
+ if (notify) {
+ // if comment contains in unread list
+ comment.getRender().setUnread(true)
+ comment.getRender().setOpenAction(() => {
+ window.open(notify.read_link)
+
+ // remove notify which has been read
+ ctx.getData().updateUnreads(unreads.filter(o => o.comment_id !== comment.getID()))
+ })
+ } else {
+ // comment not in unread list
+ comment.getRender().setUnread(false)
+ }
+ }
+ })
+
+ ctx.on('list-goto', (commentID) => {
+ const notifyKey = Utils.getQueryParam('atk_notify_key')
+ if (notifyKey) {
+ // mark as read
+ ctx.getApi().user.markRead(commentID, notifyKey)
+ .then(() => {
+ // remove from unread list
+ ctx.getData().updateUnreads(ctx.getData().getUnreads().filter(o => o.comment_id !== commentID))
+ })
+ }
+ })
+}
diff --git a/ui/packages/artalk/src/plugins/list-close-editor.ts b/ui/packages/artalk/src/plugins/list/with-editor.ts
similarity index 88%
rename from ui/packages/artalk/src/plugins/list-close-editor.ts
rename to ui/packages/artalk/src/plugins/list/with-editor.ts
index 8f919beca..0436fd9cc 100644
--- a/ui/packages/artalk/src/plugins/list-close-editor.ts
+++ b/ui/packages/artalk/src/plugins/list/with-editor.ts
@@ -13,7 +13,7 @@ function ensureListEditor(ctx: ContextApi) {
return { list, editor }
}
-export const ListCloseEditor: ArtalkPlugin = (ctx) => {
+export const WithEditor: ArtalkPlugin = (ctx) => {
let $closeCommentBtn: HTMLElement|undefined
// on Artalk inited
@@ -25,7 +25,7 @@ export const ListCloseEditor: ArtalkPlugin = (ctx) => {
// bind editor close button click event
$closeCommentBtn.addEventListener('click', () => {
- const page = ctx.getPage()
+ const page = ctx.getData().getPage()
if (!page) throw new Error('Page data not found')
page.admin_only = !page.admin_only
@@ -48,6 +48,11 @@ export const ListCloseEditor: ArtalkPlugin = (ctx) => {
$closeCommentBtn && ($closeCommentBtn.innerText = $t('closeComment'))
}
})
+
+ ctx.on('list-loaded', (comments) => {
+ // 防止评论框被吞
+ ctx.editorResetState()
+ })
}
/** 管理员设置页面信息 */
@@ -55,7 +60,7 @@ function adminPageEditSave(ctx: ContextApi, page: PageData) {
ctx.editorShowLoading()
ctx.getApi().page.pageEdit(page)
.then((respPage) => {
- ctx.updatePage(respPage)
+ ctx.getData().updatePage(respPage)
})
.catch(err => {
ctx.editorShowNotify(`${$t('editFail')}: ${err.msg || String(err)}`, 'e')
diff --git a/ui/packages/artalk/src/plugins/unread.ts b/ui/packages/artalk/src/plugins/unread.ts
deleted file mode 100644
index 4b6e77ea2..000000000
--- a/ui/packages/artalk/src/plugins/unread.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import ArtalkPlugin from '~/types/plugin'
-import * as Utils from '@/lib/utils'
-
-export const Unread: ArtalkPlugin = (ctx) => {
- ctx.on('unread-updated', (unreadList) => {
- const list = ctx.get('list')
- if (!list) return
-
- // comment unread highlight
- if (list.getOptions().unreadHighlight === true) {
- ctx.getCommentList().forEach((comment) => {
- const notify = unreadList.find(o => o.comment_id === comment.getID())
-
- if (notify) {
- // if comment contains in unread list
- comment.getRender().setUnread(true)
- comment.getRender().setOpenAction(() => {
- window.open(notify.read_link)
-
- // remove notify which has been read
- ctx.updateUnreadList(unreadList.filter(o => o.comment_id !== comment.getID()))
- })
- } else {
- // comment not in unread list
- comment.getRender().setUnread(false)
- }
- })
- }
- })
-
- ctx.on('list-goto', (commentID) => {
- const notifyKey = Utils.getQueryParam('atk_notify_key')
- if (notifyKey) {
- // mark as read
- ctx.getApi().user.markRead(commentID, notifyKey)
- .then(() => {
- // remove from unread list
- ctx.updateUnreadList(ctx.getUnreadList().filter(o => o.comment_id !== commentID))
- })
- }
- })
-}
diff --git a/ui/packages/artalk/src/plugins/version-check.ts b/ui/packages/artalk/src/plugins/version-check.ts
index 67edc5aec..754066780 100644
--- a/ui/packages/artalk/src/plugins/version-check.ts
+++ b/ui/packages/artalk/src/plugins/version-check.ts
@@ -30,7 +30,7 @@ function versionCheck(list: List, feVer: string, beVer: string) {
ignoreBtn.onclick = () => {
Ui.setError(list.$el.parentElement!, null)
IgnoreVersionCheck = true
- list.fetchComments(0)
+ list.ctx.fetch({ offset: 0 })
}
errEl.append(ignoreBtn)
Ui.setError(list.$el.parentElement!, errEl, 'Artalk Warn')
diff --git a/ui/packages/artalk/src/service.ts b/ui/packages/artalk/src/service.ts
index dd8b0bea8..bc48f3417 100644
--- a/ui/packages/artalk/src/service.ts
+++ b/ui/packages/artalk/src/service.ts
@@ -64,8 +64,10 @@ const services = {
const list = new List(ctx)
ctx.$root.appendChild(list.$el)
- // 评论获取
- list.fetchComments(0)
+ ctx.on('inited', () => {
+ // 评论获取
+ ctx.fetch({ offset: 0 })
+ })
return list
},
diff --git a/ui/packages/artalk/types/artalk-config.d.ts b/ui/packages/artalk/types/artalk-config.d.ts
index 6971ec3d2..d7e8e4f40 100644
--- a/ui/packages/artalk/types/artalk-config.d.ts
+++ b/ui/packages/artalk/types/artalk-config.d.ts
@@ -118,6 +118,14 @@ export default interface ArtalkConfig {
/** 后端版本 (系统数据,用户不允许更改) */
apiVersion?: string
+
+ /** 列表请求参数修改器 */
+ listFetchParamsModifier?: (params: any) => void
+
+ // TODO consider merge list related config into one object, or flatten all to keep simple (keep consistency)
+ listLiteMode?: boolean
+ listUnreadHighlight?: boolean
+ listScrollListenerAt?: HTMLElement
}
/**
diff --git a/ui/packages/artalk/types/artalk-data.d.ts b/ui/packages/artalk/types/artalk-data.d.ts
index 9f9f96909..03cb68c73 100644
--- a/ui/packages/artalk/types/artalk-data.d.ts
+++ b/ui/packages/artalk/types/artalk-data.d.ts
@@ -228,3 +228,41 @@ export interface ApiVersionData {
/** API 程序 CommitHash */
commit_hash: string
}
+
+export interface ListFetchParams {
+ offset: number
+ limit: number
+ flatMode: boolean
+ onSuccess?: (data: ListData) => void
+ onError?: (err: any) => void
+}
+
+export interface ListLastFetchData {
+ params: ListFetchParams
+ data?: ListData
+}
+
+export interface DataManagerApi {
+ getLoading(): boolean
+ setLoading(val: boolean): void
+
+ getListLastFetch(): ListLastFetchData|undefined
+ setListLastFetch(val: ListLastFetchData): void
+
+ getComments(): CommentData[]
+ findComment(id: number): CommentData|undefined
+
+ fetchComments(params: Partial): void
+ loadComments(comments: CommentData[]): void
+ clearComments(): void
+
+ insertComment(comment: CommentData): void
+ updateComment(comment: CommentData): void
+ deleteComment(id: number): void
+
+ getUnreads(): NotifyData[]
+ updateUnreads(unreads: NotifyData[]): void
+
+ getPage(): PageData|undefined
+ updatePage(pageData: PageData): void
+}
diff --git a/ui/packages/artalk/types/context.d.ts b/ui/packages/artalk/types/context.d.ts
index 1c05c218e..2b18f3c3b 100644
--- a/ui/packages/artalk/types/context.d.ts
+++ b/ui/packages/artalk/types/context.d.ts
@@ -1,11 +1,9 @@
import ArtalkConfig from './artalk-config'
-import { CommentData, NotifyData, PageData } from './artalk-data'
+import { CommentData, DataManagerApi, ListFetchParams } from './artalk-data'
import type { EventPayloadMap } from './event'
import type { EventManagerFuncs } from '../src/lib/event-manager'
-import { internal as internalLocales, I18n } from '../src/i18n'
+import { I18n } from '../src/i18n'
import Api from '../src/api'
-import User from '../src/lib/user'
-import Comment from '../src/comment'
import { SidebarShowPayload } from '../src/layer/sidebar-layer'
import { CheckerCaptchaPayload, CheckerPayload } from '../src/lib/checker'
import type { TMarked } from '../src/lib/marked'
@@ -39,29 +37,8 @@ export default interface ContextApi extends EventManagerFuncs {
/** 获取 API 以供 HTTP 请求 */
getApi(): Api
- /** 获取评论实例对象列表 */
- getCommentList(): Comment[]
-
- /** 清空评论数据列表 */
- clearCommentList(): void
-
- /** 获取评论数据列表 */
- getCommentDataList(): CommentData[]
-
- /** 查找评论 */
- findComment(id: number): Comment|undefined
-
- /** 删除评论 */
- deleteComment(id: number): void
-
- /** 清空评论 */
- clearAllComments(): void
-
- /** 插入评论 */
- insertComment(commentData: CommentData): void
-
- /** 更新评论 */
- updateComment(commentData: CommentData): void
+ /** 获取数据管理器对象 */
+ getData(): DataManagerApi
/** 评论回复 */
replyComment(commentData: CommentData, $comment: HTMLElement): void
@@ -69,24 +46,15 @@ export default interface ContextApi extends EventManagerFuncs {
/** 编辑评论 */
editComment(commentData: CommentData, $comment: HTMLElement): void
- /** 获取页面数据 */
- getPage(): PageData|undefined
-
- /** 更新页面数据 */
- updatePage(pageData: PageData): void
-
- /** 获取未读列表 */
- getUnreadList(): NotifyData[]
+ /** 获取评论数据 */
+ fetch(params: Partial): void
- /** 更新未读通知数据 */
- updateUnreadList(unreadList: NotifyData[]): void
-
- /** 列表 - 重新加载数据 */
- listReload(): void
-
- /** 列表 - 重新加载数据 (别名) */
+ /** 重载评论数据 */
reload(): void
+ /** 列表滚动到第一个评论的位置 */
+ listGotoFirst(): void
+
/** 显示侧边栏 */
showSidebar(payload?: SidebarShowPayload): void
diff --git a/ui/packages/artalk/types/event.d.ts b/ui/packages/artalk/types/event.d.ts
index ac4c30549..11f3bdda0 100644
--- a/ui/packages/artalk/types/event.d.ts
+++ b/ui/packages/artalk/types/event.d.ts
@@ -1,22 +1,39 @@
-import { CommentData, NotifyData, PageData } from './artalk-data'
-import { LocalUser } from './artalk-config'
+import type Comment from '~/src/comment'
+import { CommentData, ListData, ListFetchParams, NotifyData, PageData } from './artalk-data'
+import ArtalkConfig, { LocalUser } from './artalk-config'
+
+interface ErrorData { msg: string, data?: any }
+interface ListFetchedArgs { params: Partial, data?: ListData, error?: ErrorData }
/** EventName to EventPayload Type */
export interface EventPayloadMap {
'inited': undefined // Artalk 初始化后
'destroy': undefined // Artalk 销毁时
- 'list-load': undefined // 评论加载时
- 'list-loaded': undefined // 评论装载后
- 'list-inserted': CommentData // 评论插入后
- 'list-deleted': CommentData // 评论删除后
- 'list-error': { msg: string, data?: any } // 评论加载错误时
+ 'list-fetch': Partial // 评论列表请求时
+ 'list-fetched': ListFetchedArgs // 评论列表请求后
+ 'list-load': CommentData[] // 评论装载前
+
+ // TODO merge 'list-loaded' and 'list-error', for purpose of `once` 'list-loaded' and simplify
+ // example need bind 'list-loaded' and 'list-error' to same handler
+ // consider name it to 'list-fetched': { data: CommentData[], error?: { msg: string, data?: any } }
+ // and remove `list-load`
+ 'list-loaded': CommentData[] // 评论装载后
+ 'list-error': ErrorData // 评论加载错误时
+
+ 'list-goto-first': undefined // 评论列表归位时
+ 'list-reach-bottom': undefined // 评论列表滚动到底部时
+
+ 'comment-inserted': CommentData // 评论插入后
+ 'comment-updated': CommentData // 评论更新后
+ 'comment-deleted': CommentData // 评论删除后
+ 'comment-rendered': Comment // 评论节点渲染后
+ 'unreads-updated': NotifyData[] // 未读消息变更时
'list-goto': number // 评论跳转时
'page-loaded': PageData // 页面数据更新后
'editor-submit': undefined // 编辑器提交时
'editor-submitted': undefined // 编辑器提交后
'user-changed': LocalUser // 本地用户数据变更时
- 'conf-loaded': undefined // Artalk 配置变更时
- 'unread-updated': NotifyData[] // 未读消息变更时
+ 'conf-loaded': ArtalkConfig // Artalk 配置变更时
'sidebar-show': undefined // 侧边栏显示
'sidebar-hide': undefined // 侧边栏隐藏
}