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: `
${t('noContent')}
`, - 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: `
${t('noContent')}
`, + 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+)/, '$1') } 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 // 侧边栏隐藏 }