diff --git a/package.json b/package.json
index a7089589..22fda070 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,8 @@
"build:auth": "pnpm -F @artalk/plugin-auth build",
"build:plugin-kit": "pnpm -F @artalk/plugin-kit build",
"build:eslint-plugin": "pnpm -F eslint-plugin-artalk build",
- "build:docs": "pnpm build && pnpm -F=docs-landing build && pnpm -F=docs-swagger swagger:build && pnpm -F=docs build:docs && pnpm patch:docs",
+ "build:typedoc": "pnpm typedoc",
+ "build:docs": "pnpm build && pnpm -F=docs-landing build && pnpm -F=docs-swagger swagger:build && pnpm -F=docs build:docs && pnpm patch:docs && pnpm build:typedoc",
"patch:docs": "cp -rf docs/landing/dist/* docs/swagger/dist/* docs/docs/.vitepress/dist",
"lint:eslint": "eslint .",
"lint:prettier": "prettier --check .",
@@ -52,6 +53,7 @@
"sass": "1.78.0",
"terser": "5.33.0",
"tsx": "^4.19.1",
+ "typedoc": "^0.26.10",
"typescript": "5.6.2",
"typescript-eslint": "^8.6.0",
"vite": "5.4.6",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 54090b92..f608f815 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -89,6 +89,9 @@ importers:
tsx:
specifier: ^4.19.1
version: 4.19.1
+ typedoc:
+ specifier: ^0.26.10
+ version: 0.26.10(typescript@5.6.2)
typescript:
specifier: 5.6.2
version: 5.6.2
@@ -3212,6 +3215,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3288,6 +3294,10 @@ packages:
mark.js@8.11.1:
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
+ markdown-it@14.1.0:
+ resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+ hasBin: true
+
marked@14.1.2:
resolution: {integrity: sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==}
engines: {node: '>= 18'}
@@ -3307,6 +3317,9 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
meow@13.2.0:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
@@ -3745,6 +3758,10 @@ packages:
psl@1.9.0:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -4362,6 +4379,13 @@ packages:
resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
engines: {node: '>= 0.4'}
+ typedoc@0.26.10:
+ resolution: {integrity: sha512-xLmVKJ8S21t+JeuQLNueebEuTVphx6IrP06CdV7+0WVflUSW3SPmR+h1fnWVdAR/FQePEgsSWCUHXqKKjzuUAw==}
+ engines: {node: '>= 18'}
+ hasBin: true
+ peerDependencies:
+ typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x
+
typescript-eslint@8.6.0:
resolution: {integrity: sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -4381,6 +4405,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
ufo@1.5.4:
resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
@@ -8103,6 +8130,10 @@ snapshots:
lines-and-columns@1.2.4: {}
+ linkify-it@5.0.0:
+ dependencies:
+ uc.micro: 2.1.0
+
load-tsconfig@0.2.5: {}
local-pkg@0.5.0:
@@ -8173,6 +8204,15 @@ snapshots:
mark.js@8.11.1: {}
+ markdown-it@14.1.0:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.0
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
marked@14.1.2: {}
marked@4.3.0: {}
@@ -8195,6 +8235,8 @@ snapshots:
mdn-data@2.0.30:
optional: true
+ mdurl@2.0.0: {}
+
meow@13.2.0:
optional: true
@@ -8603,6 +8645,8 @@ snapshots:
psl@1.9.0: {}
+ punycode.js@2.3.1: {}
+
punycode@2.3.1: {}
querystringify@2.2.0: {}
@@ -9389,6 +9433,15 @@ snapshots:
is-typed-array: 1.1.13
possible-typed-array-names: 1.0.0
+ typedoc@0.26.10(typescript@5.6.2):
+ dependencies:
+ lunr: 2.3.9
+ markdown-it: 14.1.0
+ minimatch: 9.0.5
+ shiki: 1.17.7
+ typescript: 5.6.2
+ yaml: 2.5.1
+
typescript-eslint@8.6.0(eslint@9.10.0)(typescript@5.6.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.6.0(@typescript-eslint/parser@8.6.0(eslint@9.10.0)(typescript@5.6.2))(eslint@9.10.0)(typescript@5.6.2)
@@ -9404,6 +9457,8 @@ snapshots:
typescript@5.6.2: {}
+ uc.micro@2.1.0: {}
+
ufo@1.5.4: {}
uglify-js@3.19.3:
diff --git a/typedoc.json b/typedoc.json
new file mode 100644
index 00000000..fde0bc99
--- /dev/null
+++ b/typedoc.json
@@ -0,0 +1,6 @@
+{
+ "out": "docs/docs/.vitepress/dist/typedoc",
+ "entryPoints": ["ui/artalk/src/main.ts"],
+ "tsconfig": "ui/artalk/tsconfig.json",
+ "logLevel": "Error"
+}
diff --git a/ui/artalk/src/artalk.ts b/ui/artalk/src/artalk.ts
index 63065b55..4cd40ee5 100644
--- a/ui/artalk/src/artalk.ts
+++ b/ui/artalk/src/artalk.ts
@@ -1,14 +1,19 @@
import './style/main.scss'
-import type { EventHandler } from './lib/event-manager'
import Context from './context'
-import { handelCustomConf, convertApiOptions } from './config'
-import Services from './service'
+import { handelCustomConf, convertApiOptions, getRootEl } from './config'
import * as Stat from './plugins/stat'
import { Api } from './api'
-import type { TInjectedServices } from './service'
-import { GlobalPlugins, PluginOptions, load } from './load'
-import type { ArtalkConfigPartial, EventPayloadMap, ArtalkPlugin, ContextApi } from '@/types'
+import { GlobalPlugins, PluginOptions, mount } from './mount'
+import { ConfigService } from './services/config'
+import { EventsService } from './services/events'
+import type {
+ ConfigPartial,
+ EventPayloadMap,
+ ArtalkPlugin,
+ Context as IContext,
+ EventHandler,
+} from '@/types'
/**
* Artalk
@@ -16,25 +21,45 @@ import type { ArtalkConfigPartial, EventPayloadMap, ArtalkPlugin, ContextApi } f
* @see https://artalk.js.org
*/
export default class Artalk {
- public ctx!: ContextApi
+ public ctx: IContext
- constructor(conf: ArtalkConfigPartial) {
- // Init Config
- const handledConf = handelCustomConf(conf, true)
+ constructor(conf: ConfigPartial) {
+ // Init Root Element
+ const $root = getRootEl(conf)
+ $root.classList.add('artalk')
+ $root.innerHTML = ''
+ conf.darkMode == true && $root.classList.add('atk-dark-mode')
// Init Context
- this.ctx = new Context(handledConf)
+ const ctx = (this.ctx = new Context($root))
- // Init Services
- Object.entries(Services).forEach(([name, initService]) => {
- const obj = initService(this.ctx)
- obj && this.ctx.inject(name as keyof TInjectedServices, obj) // auto inject deps to ctx
- })
+ // Init required services
+ ;(() => {
+ // Init event manager
+ EventsService(ctx)
+
+ // Init config service
+ ConfigService(ctx)
+ })()
+
+ // Apply local conf first
+ ctx.updateConf(conf)
+
+ // Trigger created event
+ ctx.trigger('created')
+
+ // Load plugins and remote config, then mount Artalk
+ const mountArtalk = async () => {
+ await mount(conf, ctx)
+
+ // Trigger mounted event
+ ctx.trigger('mounted')
+ }
if (import.meta.env.DEV && import.meta.env.VITEST) {
- global.devLoadArtalk = () => load(this.ctx)
+ global.devMountArtalk = mountArtalk
} else {
- load(this.ctx)
+ mountArtalk()
}
}
@@ -45,13 +70,12 @@ export default class Artalk {
/** Get the root element of Artalk */
public getEl() {
- return this.ctx.$root
+ return this.ctx.getEl()
}
/** Update config of Artalk */
- public update(conf: ArtalkConfigPartial) {
+ public update(conf: ConfigPartial) {
this.ctx.updateConf(conf)
- return this
}
/** Reload comment list of Artalk */
@@ -61,10 +85,7 @@ export default class Artalk {
/** Destroy instance of Artalk */
public destroy() {
- this.ctx.trigger('unmounted')
- while (this.ctx.$root.firstChild) {
- this.ctx.$root.removeChild(this.ctx.$root.firstChild)
- }
+ this.ctx.destroy()
}
/** Add an event listener */
@@ -92,7 +113,7 @@ export default class Artalk {
// ===========================
/** Init Artalk */
- public static init(conf: ArtalkConfigPartial): Artalk {
+ public static init(conf: ConfigPartial): Artalk {
return new Artalk(conf)
}
@@ -103,7 +124,7 @@ export default class Artalk {
}
/** Load count widget */
- public static loadCountWidget(c: ArtalkConfigPartial) {
+ public static loadCountWidget(c: ConfigPartial) {
const conf = handelCustomConf(c, true)
Stat.initCountWidget({
@@ -119,14 +140,15 @@ export default class Artalk {
// ===========================
// Deprecated
// ===========================
-
/** @deprecated Please use `getEl()` instead */
public get $root() {
- return this.ctx.$root
+ console.warn('`$root` is deprecated, please use `getEl()` instead')
+ return this.getEl()
}
/** @description Please use `getConf()` instead */
public get conf() {
- return this.ctx.getConf()
+ console.warn('`conf` is deprecated, please use `getConf()` instead')
+ return this.getConf()
}
}
diff --git a/ui/artalk/src/comment/comment-node.ts b/ui/artalk/src/comment/comment-node.ts
index eec375d0..065e634b 100644
--- a/ui/artalk/src/comment/comment-node.ts
+++ b/ui/artalk/src/comment/comment-node.ts
@@ -6,7 +6,7 @@ import UADetect from '../lib/detect'
import CommentUI from './render'
import CommentActions from './actions'
import $t from '@/i18n'
-import type { CommentData, ArtalkConfig, ContextApi } from '@/types'
+import type { CommentData, Config, Context } from '@/types'
export interface CommentOptions {
// Hooks
@@ -22,16 +22,16 @@ export interface CommentOptions {
voteDown: boolean
uaBadge: boolean
nestMax: number
- gravatar: ArtalkConfig['gravatar']
- heightLimit: ArtalkConfig['heightLimit']
- avatarURLBuilder: ArtalkConfig['avatarURLBuilder']
- scrollRelativeTo: ArtalkConfig['scrollRelativeTo']
- dateFormatter: ArtalkConfig['dateFormatter']
+ gravatar: Config['gravatar']
+ heightLimit: Config['heightLimit']
+ avatarURLBuilder: Config['avatarURLBuilder']
+ scrollRelativeTo: Config['scrollRelativeTo']
+ dateFormatter: Config['dateFormatter']
// TODO: Move to plugin folder and remove from core
getApi: () => Api
- replyComment: ContextApi['replyComment']
- editComment: ContextApi['editComment']
+ replyComment: Context['replyComment']
+ editComment: Context['editComment']
}
export default class CommentNode {
diff --git a/ui/artalk/src/components/checker/index.ts b/ui/artalk/src/components/checker/index.ts
index ed7d4a05..1bd5920c 100644
--- a/ui/artalk/src/components/checker/index.ts
+++ b/ui/artalk/src/components/checker/index.ts
@@ -3,9 +3,8 @@ import AdminChecker from './admin'
import type { Api } from '@/api'
import Dialog from '@/components/dialog'
import $t from '@/i18n'
-import type { ContextApi } from '@/types'
-import type User from '@/lib/user'
import * as Utils from '@/lib/utils'
+import type { UserManager, CheckerManager as ICheckerManager, LayerManager } from '@/types'
export interface CheckerCaptchaPayload extends CheckerPayload {
img_data?: string
@@ -19,8 +18,9 @@ export interface CheckerPayload {
}
export interface CheckerLauncherOptions {
- getCtx: () => ContextApi
getApi: () => Api
+ getLayers: () => LayerManager
+ getUser: () => UserManager
getCaptchaIframeURL: () => string
onReload: () => void
}
@@ -43,25 +43,25 @@ function wrapPromise
(fn: (p: P) => vo
}
/**
- * Checker 发射台
+ * Checker Launcher
*/
-export default class CheckerLauncher {
+export class CheckerLauncher implements ICheckerManager {
constructor(private opts: CheckerLauncherOptions) {}
public checkCaptcha: (payload: CheckerCaptchaPayload) => Promise = wrapPromise((p) => {
- this.fire(CaptchaChecker, p, (ctx) => {
+ this.check(CaptchaChecker, p, (ctx) => {
ctx.set('img_data', p.img_data)
ctx.set('iframe', p.iframe)
})
})
public checkAdmin: (payload: CheckerPayload) => Promise = wrapPromise((p) => {
- this.fire(AdminChecker, p)
+ this.check(AdminChecker, p)
})
- public fire(checker: Checker, payload: CheckerPayload, postFire?: (c: CheckerCtx) => void) {
+ public check(checker: Checker, payload: CheckerPayload, beforeCheck?: (c: CheckerCtx) => void) {
// 显示层
- const layer = this.opts.getCtx().get('layerManager').create(`checker-${new Date().getTime()}`)
+ const layer = this.opts.getLayers().create(`checker-${new Date().getTime()}`)
layer.show()
const close = () => {
@@ -77,7 +77,7 @@ export default class CheckerLauncher {
},
get: (key) => checkerStore[key],
getOpts: () => this.opts,
- getUser: () => this.opts.getCtx().get('user'),
+ getUser: () => this.opts.getUser(),
getApi: () => this.opts.getApi(),
hideInteractInput: () => {
hideInteractInput = true
@@ -93,7 +93,7 @@ export default class CheckerLauncher {
},
}
- if (postFire) postFire(checkerCtx)
+ if (beforeCheck) beforeCheck(checkerCtx)
// 创建表单
const formEl = Utils.createElement()
@@ -202,7 +202,7 @@ export interface CheckerCtx {
set(key: K, val: CheckerStore[K]): void
getOpts(): CheckerLauncherOptions
getApi(): Api
- getUser(): User
+ getUser(): UserManager
hideInteractInput(): void
triggerSuccess(): void
cancel(): void
diff --git a/ui/artalk/src/config.ts b/ui/artalk/src/config.ts
index 385f9017..009a642a 100644
--- a/ui/artalk/src/config.ts
+++ b/ui/artalk/src/config.ts
@@ -1,8 +1,8 @@
import type { ApiOptions } from './api/options'
import { mergeDeep } from './lib/merge-deep'
-import { createApiHandlers } from './api'
-import Defaults from './defaults'
-import type { ArtalkConfig, ArtalkConfigPartial, ContextApi } from '@/types'
+import type { ApiHandlers } from './api'
+import { Defaults } from './defaults'
+import type { Config, ConfigPartial, UserManager } from '@/types'
/**
* Handle the custom config which is provided by the user
@@ -11,40 +11,28 @@ import type { ArtalkConfig, ArtalkConfigPartial, ContextApi } from '@/types'
* @param full - If `full` is `true`, the return value will be the complete config for Artalk instance creation
* @returns The config for Artalk instance creation
*/
-export function handelCustomConf(customConf: ArtalkConfigPartial, full: true): ArtalkConfig
-export function handelCustomConf(customConf: ArtalkConfigPartial, full?: false): ArtalkConfigPartial
-export function handelCustomConf(customConf: ArtalkConfigPartial, full = false) {
- // 合并默认配置
- const conf: ArtalkConfigPartial = full ? mergeDeep(Defaults, customConf) : customConf
+export function handelCustomConf(customConf: ConfigPartial, full: true): Config
+export function handelCustomConf(customConf: ConfigPartial, full?: false): ConfigPartial
+export function handelCustomConf(customConf: ConfigPartial, full = false) {
+ // Merge default config
+ const conf: ConfigPartial = full ? mergeDeep(Defaults, customConf) : customConf
- // 绑定元素
- if (conf.el && typeof conf.el === 'string') {
- try {
- const findEl = document.querySelector(conf.el)
- if (!findEl) throw Error(`Target element "${conf.el}" was not found.`)
- conf.el = findEl
- } catch (e) {
- console.error(e)
- throw new Error('Please check your Artalk `el` config.')
- }
- }
+ // Default pageKey
+ if (conf.pageKey === '') conf.pageKey = `${window.location.pathname}` // @see http://bl.ocks.org/abernier/3070589
- // 默认 pageKey
- if (conf.pageKey === '') conf.pageKey = `${window.location.pathname}` // @link http://bl.ocks.org/abernier/3070589
-
- // 默认 pageTitle
+ // Default pageTitle
if (conf.pageTitle === '') conf.pageTitle = `${document.title}`
- // 服务器配置
+ // Server
if (conf.server) conf.server = conf.server.replace(/\/$/, '').replace(/\/api\/?$/, '')
- // 自适应语言
+ // Language auto-detection
if (conf.locale === 'auto') conf.locale = navigator.language
- // 自动判断启用平铺模式
+ // Flat mode auto-detection
if (conf.flatMode === 'auto') conf.flatMode = window.matchMedia('(max-width: 768px)').matches
- // flatMode
+ // Change flatMode by nestMax
if (typeof conf.nestMax === 'number' && Number(conf.nestMax) <= 1) conf.flatMode = true
return conf
@@ -56,8 +44,8 @@ export function handelCustomConf(customConf: ArtalkConfigPartial, full = false)
* @param conf - The Server response config for control the frontend of Artalk remotely
* @returns The config for Artalk instance creation
*/
-export function handleConfFormServer(conf: ArtalkConfigPartial): ArtalkConfigPartial {
- const ExcludedKeys: (keyof ArtalkConfig)[] = [
+export function handleConfFormServer(conf: ConfigPartial): ConfigPartial {
+ const ExcludedKeys: (keyof Config)[] = [
'el',
'pageKey',
'pageTitle',
@@ -86,6 +74,26 @@ export function handleConfFormServer(conf: ArtalkConfigPartial): ArtalkConfigPar
return conf
}
+/**
+ * Get the root element of Artalk
+ *
+ * @param conf - Artalk config
+ * @returns The root element of Artalk
+ */
+export function getRootEl(conf: ConfigPartial): HTMLElement {
+ let $root: HTMLElement
+ if (typeof conf.el === 'string') {
+ const el = document.querySelector(conf.el)
+ if (!el) throw new Error(`Element "${conf.el}" not found.`)
+ $root = el
+ } else if (conf.el instanceof HTMLElement) {
+ $root = conf.el
+ } else {
+ throw new Error('Please provide a valid `el` config for Artalk.')
+ }
+ return $root
+}
+
/**
* Convert Artalk Config to ApiOptions for Api client
*
@@ -93,28 +101,24 @@ export function handleConfFormServer(conf: ArtalkConfigPartial): ArtalkConfigPar
* @param ctx - If `ctx` not provided, `checkAdmin` and `checkCaptcha` will be disabled
* @returns ApiOptions for Api client instance creation
*/
-export function convertApiOptions(conf: ArtalkConfigPartial, ctx?: ContextApi): ApiOptions {
+export function convertApiOptions(
+ conf: ConfigPartial,
+ user?: UserManager,
+ handlers?: ApiHandlers,
+): ApiOptions {
return {
baseURL: `${conf.server}/api/v2`,
siteName: conf.site || '',
pageKey: conf.pageKey || '',
pageTitle: conf.pageTitle || '',
timeout: conf.reqTimeout,
- getApiToken: () => ctx?.get('user').getData().token,
- userInfo: ctx?.get('user').checkHasBasicUserInfo()
+ getApiToken: () => user?.getData().token,
+ userInfo: user?.checkHasBasicUserInfo()
? {
- name: ctx?.get('user').getData().name,
- email: ctx?.get('user').getData().email,
+ name: user?.getData().name,
+ email: user?.getData().email,
}
: undefined,
- handlers: ctx?.getApiHandlers(),
+ handlers,
}
}
-
-export function createNewApiHandlers(ctx: ContextApi) {
- const h = createApiHandlers()
- h.add('need_captcha', (res) => ctx.checkCaptcha(res))
- h.add('need_login', () => ctx.checkAdmin({}))
-
- return h
-}
diff --git a/ui/artalk/src/context.ts b/ui/artalk/src/context.ts
index 45fe9053..a2f9a804 100644
--- a/ui/artalk/src/context.ts
+++ b/ui/artalk/src/context.ts
@@ -1,193 +1,206 @@
-/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
-import type { TInjectedServices } from './service'
-import { Api, ApiHandlers } from './api'
-
-import * as marked from './lib/marked'
-import { mergeDeep } from './lib/merge-deep'
-import { CheckerCaptchaPayload, CheckerPayload } from './components/checker'
-
-import { DataManager } from './data'
-import * as I18n from './i18n'
-
-import EventManager from './lib/event-manager'
-import { convertApiOptions, createNewApiHandlers, handelCustomConf } from './config'
-import { watchConf } from './lib/watch-conf'
-
import type {
- ArtalkConfig,
- ArtalkConfigPartial,
+ Config,
+ ConfigPartial,
CommentData,
ListFetchParams,
- ContextApi,
- EventPayloadMap,
+ Context as IContext,
SidebarShowPayload,
-} from '@/types'
-
-// Auto dependency injection
-// eslint-disable-next-line @typescript-eslint/no-empty-object-type
-interface Context extends TInjectedServices {}
+ Services,
+} from './types'
+import * as I18n from './i18n'
+import * as marked from './lib/marked'
+import { createInjectionContainer } from './lib/injection'
+import type { CheckerCaptchaPayload, CheckerPayload } from './components/checker'
/**
* Artalk Context
*/
-class Context implements ContextApi {
- /* 运行参数 */
- conf: ArtalkConfig
- data: DataManager
- $root: HTMLElement
-
- /* Event Manager */
- private events = new EventManager()
- private mounted = false
+class Context implements IContext {
+ private _deps = createInjectionContainer()
- constructor(conf: ArtalkConfig) {
- this.conf = conf
+ constructor(private _$root: HTMLElement) {}
- this.$root = conf.el as HTMLElement
- this.$root.classList.add('artalk')
- this.$root.innerHTML = ''
- conf.darkMode && this.$root.classList.add('atk-dark-mode')
-
- this.data = new DataManager(this.events)
+ getEl(): HTMLElement {
+ return this._$root
+ }
- this.on('mounted', () => {
- this.mounted = true
- })
+ destroy(): void {
+ this.trigger('unmounted')
+ while (this._$root.firstChild) {
+ this._$root.removeChild(this._$root.firstChild)
+ }
}
- inject(depName: string, obj: any) {
- this[depName] = obj
+ // -------------------------------------------------------------------
+ // Dependency Injection
+ // -------------------------------------------------------------------
+ provide: IContext['provide'] = (key, impl, deps, opts) => {
+ this._deps.provide(key, impl, deps, opts)
}
- get(depName: string) {
- return this[depName]
+ inject: IContext['inject'] = (key) => {
+ return this._deps.inject(key)
}
+ get = this.inject
- getApi() {
- return new Api(convertApiOptions(this.conf, this))
+ // -------------------------------------------------------------------
+ // Event Manager
+ // -------------------------------------------------------------------
+ on: IContext['on'] = (name, handler) => {
+ this.inject('events').on(name, handler)
}
- private apiHandlers = null
- getApiHandlers() {
- if (!this.apiHandlers) this.apiHandlers = createNewApiHandlers(this)
- return this.apiHandlers
+ off: IContext['off'] = (name, handler) => {
+ this.inject('events').off(name, handler)
}
- getData() {
- return this.data
+ trigger: IContext['trigger'] = (name, payload) => {
+ this.inject('events').trigger(name, payload)
}
- replyComment(commentData: CommentData, $comment: HTMLElement): void {
- this.editor.setReply(commentData, $comment)
+ // -------------------------------------------------------------------
+ // Configurations
+ // -------------------------------------------------------------------
+ getConf(): Config {
+ return this.inject('config').get()
}
- editComment(commentData: CommentData, $comment: HTMLElement): void {
- this.editor.setEditComment(commentData, $comment)
+ updateConf(conf: ConfigPartial): void {
+ this.inject('config').update(conf)
}
- fetch(params: Partial): void {
- this.data.fetchComments(params)
+ watchConf(
+ keys: T,
+ effect: (conf: Pick) => void,
+ ): void {
+ this.inject('config').watchConf(keys, effect)
}
- reload(): void {
- this.data.fetchComments({ offset: 0 })
+ getMarked() {
+ return marked.getInstance()
}
- /* List */
- listGotoFirst(): void {
- this.events.trigger('list-goto-first')
+ setDarkMode(darkMode: boolean | 'auto'): void {
+ this.updateConf({ darkMode })
}
- getCommentNodes() {
- return this.list.getCommentNodes()
+ get conf() {
+ return this.getConf()
+ }
+ set conf(val) {
+ console.error('Cannot set config directly, please call updateConf()')
+ }
+ get $root() {
+ return this.getEl()
+ }
+ set $root(val) {
+ console.error('set $root is prohibited')
}
- getComments() {
- return this.data.getComments()
+ // -------------------------------------------------------------------
+ // I18n: Internationalization
+ // -------------------------------------------------------------------
+ $t(key: I18n.I18nKeys, args: { [key: string]: string } = {}): string {
+ return I18n.t(key, args)
}
- getCommentList = this.getCommentNodes
- getCommentDataList = this.getComments
+ // -------------------------------------------------------------------
+ // HTTP API Client
+ // -------------------------------------------------------------------
+ getApi() {
+ return this.inject('api')
+ }
- /* Editor */
- editorShowLoading(): void {
- this.editor.showLoading()
+ getApiHandlers() {
+ return this.inject('apiHandlers')
}
- editorHideLoading(): void {
- this.editor.hideLoading()
+ // -------------------------------------------------------------------
+ // User Manager
+ // -------------------------------------------------------------------
+ getUser() {
+ return this.inject('user')
}
- editorShowNotify(msg, type): void {
- this.editor.showNotify(msg, type)
+ // -------------------------------------------------------------------
+ // Data Manager
+ // -------------------------------------------------------------------
+ getData() {
+ return this.inject('data')
}
- editorResetState(): void {
- this.editor.resetState()
+ fetch(params: Partial): void {
+ this.getData().fetchComments(params)
}
- /* Sidebar */
- showSidebar(payload?: SidebarShowPayload): void {
- this.sidebarLayer.show(payload)
+ reload(): void {
+ this.getData().fetchComments({ offset: 0 })
}
- hideSidebar(): void {
- this.sidebarLayer.hide()
+ // -------------------------------------------------------------------
+ // List
+ // -------------------------------------------------------------------
+ listGotoFirst(): void {
+ this.trigger('list-goto-first')
}
- /* Checker */
- checkAdmin(payload: CheckerPayload): Promise {
- return this.checkerLauncher.checkAdmin(payload)
+ getCommentList = this.getCommentNodes
+ getCommentNodes() {
+ return this.inject('list').getCommentNodes()
}
- checkCaptcha(payload: CheckerCaptchaPayload): Promise {
- return this.checkerLauncher.checkCaptcha(payload)
+ getCommentDataList = this.getComments
+ getComments() {
+ return this.getData().getComments()
}
- /* Events */
- on(name: any, handler: any) {
- this.events.on(name, handler)
+ // -------------------------------------------------------------------
+ // Editor
+ // -------------------------------------------------------------------
+ replyComment(commentData: CommentData, $comment: HTMLElement): void {
+ this.inject('editor').setReplyComment(commentData, $comment)
}
- off(name: any, handler: any) {
- this.events.off(name, handler)
+ editComment(commentData: CommentData, $comment: HTMLElement): void {
+ this.inject('editor').setEditComment(commentData, $comment)
}
- trigger(name: any, payload?: any) {
- this.events.trigger(name, payload)
+ editorShowLoading(): void {
+ this.inject('editor').showLoading()
}
- /* i18n */
- $t(key: I18n.I18nKeys, args: { [key: string]: string } = {}): string {
- return I18n.t(key, args)
+ editorHideLoading(): void {
+ this.inject('editor').hideLoading()
}
- setDarkMode(darkMode: boolean | 'auto'): void {
- this.updateConf({ darkMode })
+ editorShowNotify(msg, type): void {
+ this.inject('editor').showNotify(msg, type)
}
- updateConf(nConf: ArtalkConfigPartial): void {
- this.conf = mergeDeep(this.conf, handelCustomConf(nConf, false))
- this.mounted && this.events.trigger('updated', this.conf)
+ editorResetState(): void {
+ this.inject('editor').resetState()
}
- getConf(): ArtalkConfig {
- return this.conf
+ // -------------------------------------------------------------------
+ // Sidebar
+ // -------------------------------------------------------------------
+ showSidebar(payload?: SidebarShowPayload): void {
+ this.inject('sidebar').show(payload)
}
- getEl(): HTMLElement {
- return this.$root
+ hideSidebar(): void {
+ this.inject('sidebar').hide()
}
- getMarked() {
- return marked.getInstance()
+ // -------------------------------------------------------------------
+ // Checker
+ // -------------------------------------------------------------------
+ checkAdmin(payload: CheckerPayload): Promise {
+ return this.inject('checkers').checkAdmin(payload)
}
- watchConf(
- keys: T,
- effect: (conf: Pick) => void,
- ): void {
- watchConf(this, keys, effect)
+ checkCaptcha(payload: CheckerCaptchaPayload): Promise {
+ return this.inject('checkers').checkCaptcha(payload)
}
}
diff --git a/ui/artalk/src/data.ts b/ui/artalk/src/data.ts
index fd28d490..3657160e 100644
--- a/ui/artalk/src/data.ts
+++ b/ui/artalk/src/data.ts
@@ -1,22 +1,30 @@
-import EventManager from './lib/event-manager'
import type {
NotifyData,
PageData,
CommentData,
- DataManagerApi,
+ DataManager as IDataManager,
ListFetchParams,
ListLastFetchData,
- EventPayloadMap,
+ EventManager,
} from '@/types'
-export class DataManager implements DataManagerApi {
+export class DataManager implements IDataManager {
+ /** Loading status */
private loading: boolean = false
+
+ /** List last fetch data */
private listLastFetch?: ListLastFetchData
- private comments: CommentData[] = [] // Note: 无层级结构 + 无须排列
+
+ /** Comment list (Flatten list and unordered) */
+ private comments: CommentData[] = []
+
+ /** Notify list */
private notifies: NotifyData[] = []
+
+ /** Page data */
private page?: PageData
- constructor(protected events: EventManager) {}
+ constructor(protected events: EventManager) {}
getLoading() {
return this.loading
diff --git a/ui/artalk/src/defaults.ts b/ui/artalk/src/defaults.ts
index ff3096c5..e2ca3e94 100644
--- a/ui/artalk/src/defaults.ts
+++ b/ui/artalk/src/defaults.ts
@@ -1,13 +1,6 @@
-import type { ArtalkConfig } from '@/types'
+import type { Config } from '@/types'
-type RequiredExcept = Required> & Pick
-type FunctionKeys = Exclude<
- { [K in keyof T]: NonNullable extends (...args: any[]) => any ? K : never }[keyof T],
- undefined
->
-type ExcludedKeys = FunctionKeys
-
-const defaults: RequiredExcept = {
+export const Defaults: Readonly> = {
el: '',
pageKey: '',
pageTitle: '',
@@ -24,15 +17,17 @@ const defaults: RequiredExcept = {
nestMax: 2,
nestSort: 'DATE_ASC',
- emoticons: 'https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json',
+ emoticons: ARTALK_LITE
+ ? false
+ : 'https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json',
pageVote: true,
- vote: true,
+ vote: ARTALK_LITE ? false : true,
voteDown: false,
- uaBadge: true,
+ uaBadge: ARTALK_LITE ? false : true,
listSort: true,
- preview: true,
+ preview: ARTALK_LITE ? false : true,
countEl: '.artalk-comment-count',
pvEl: '.artalk-pv-count',
statPageKeyAttr: 'data-page-key',
@@ -54,13 +49,14 @@ const defaults: RequiredExcept = {
scrollable: false,
},
- pvAdd: true,
imgUpload: true,
- imgLazyLoad: 'native',
+ imgLazyLoad: false,
reqTimeout: 15000,
versionCheck: true,
useBackendConf: true,
listUnreadHighlight: false,
+ pvAdd: true,
+ fetchCommentsOnInit: true,
locale: 'en',
apiVersion: '',
@@ -69,11 +65,9 @@ const defaults: RequiredExcept = {
markedOptions: {},
}
-if (ARTALK_LITE) {
- defaults.vote = false
- defaults.uaBadge = false
- defaults.emoticons = false
- defaults.preview = false
-}
-
-export default defaults
+type RequiredExcept = Required> & Pick
+type FunctionKeys = Exclude<
+ { [K in keyof T]: NonNullable extends (...args: any[]) => any ? K : never }[keyof T],
+ undefined
+>
+type ExcludedKeys = FunctionKeys
diff --git a/ui/artalk/src/editor/editor.ts b/ui/artalk/src/editor/editor.ts
index 6a3678d7..f729d76b 100644
--- a/ui/artalk/src/editor/editor.ts
+++ b/ui/artalk/src/editor/editor.ts
@@ -1,26 +1,24 @@
-import Component from '../lib/component'
import * as Ui from '../lib/ui'
import marked from '../lib/marked'
-import { render, EditorUI } from './ui'
-import EditorStateManager from './state'
-import type { CommentData, ContextApi, EditorApi } from '@/types'
+import { render, type EditorUI } from './ui'
+import { EditorStateManager } from './state'
+import type { ConfigManager, CommentData, Editor as IEditor, EventManager } from '@/types'
+import type { PluginManager } from '@/plugins/editor-kit'
+
+export interface EditorOptions {
+ getEvents: () => EventManager
+ getConf: () => ConfigManager
+}
-class Editor extends Component implements EditorApi {
+export class Editor implements IEditor {
+ private opts: EditorOptions
+ private $el: HTMLElement
private ui: EditorUI
private state: EditorStateManager
+ private plugins?: PluginManager
- getUI() {
- return this.ui
- }
- getPlugs() {
- return this.ctx.get('editorPlugs')
- }
- getState() {
- return this.state.get()
- }
-
- constructor(ctx: ContextApi) {
- super(ctx)
+ constructor(opts: EditorOptions) {
+ this.opts = opts
// init editor ui
this.ui = render()
@@ -30,6 +28,30 @@ class Editor extends Component implements EditorApi {
this.state = new EditorStateManager(this)
}
+ getOptions() {
+ return this.opts
+ }
+
+ getEl() {
+ return this.$el
+ }
+
+ getUI() {
+ return this.ui
+ }
+
+ getPlugins() {
+ return this.plugins
+ }
+
+ setPlugins(plugins: PluginManager) {
+ this.plugins = plugins
+ }
+
+ getState() {
+ return this.state.get()
+ }
+
getHeaderInputEls() {
return { name: this.ui.$name, email: this.ui.$email, link: this.ui.$link }
}
@@ -37,9 +59,9 @@ class Editor extends Component implements EditorApi {
getContentFinal() {
let content = this.getContentRaw()
- // plug hook: final content transformer
- const plugs = this.getPlugs()
- if (plugs) content = plugs.getTransformedContent(content)
+ // plugin hook: final content transformer
+ const plugins = this.getPlugins()
+ if (plugins) content = plugins.getTransformedContent(content)
return content
}
@@ -56,7 +78,7 @@ class Editor extends Component implements EditorApi {
this.ui.$textarea.value = val
// plug hook: content updated
- this.getPlugs()?.getEvents().trigger('content-updated', val)
+ this.getPlugins()?.getEvents().trigger('content-updated', val)
}
insertContent(val: string) {
@@ -96,7 +118,7 @@ class Editor extends Component implements EditorApi {
this.state.switch('normal')
}
- setReply(comment: CommentData, $comment: HTMLElement) {
+ setReplyComment(comment: CommentData, $comment: HTMLElement) {
this.state.switch('reply', { comment, $comment })
}
@@ -117,13 +139,15 @@ class Editor extends Component implements EditorApi {
}
submit() {
- const next = () => this.ctx.trigger('editor-submit')
- if (this.ctx.conf.beforeSubmit) {
- this.ctx.conf.beforeSubmit(this, next)
+ const next = () => {
+ this.getPlugins()?.getEvents().trigger('editor-submit')
+ this.opts.getEvents().trigger('editor-submit')
+ }
+ const beforeSubmit = this.opts.getConf().get().beforeSubmit
+ if (beforeSubmit) {
+ beforeSubmit(this, next)
} else {
next()
}
}
}
-
-export default Editor
diff --git a/ui/artalk/src/editor/state.ts b/ui/artalk/src/editor/state.ts
index 4b7badba..d1b1d8f8 100644
--- a/ui/artalk/src/editor/state.ts
+++ b/ui/artalk/src/editor/state.ts
@@ -1,9 +1,8 @@
import Mover from '../plugins/editor/mover'
-import type Editor from './editor'
-import type { EditorState, CommentData } from '@/types'
+import type { EditorState, CommentData, Editor } from '@/types'
import * as Ui from '@/lib/ui'
-export default class EditorStateManager {
+export class EditorStateManager {
constructor(private editor: Editor) {}
private stateCurt: EditorState = 'normal'
@@ -27,24 +26,23 @@ export default class EditorStateManager {
this.stateUnmountFn = null
// move editor back to the initial position
- this.editor.getPlugs()?.get(Mover)?.back()
+ this.editor.getPlugins()?.get(Mover)?.back()
}
// invoke effect function and save unmount function
if (state !== 'normal' && payload) {
// move editor position
let moveAfterEl = payload.$comment
- if (!this.editor.conf.flatMode)
+ if (!this.editor.getOptions().getConf().get().flatMode)
moveAfterEl = moveAfterEl.querySelector('.atk-footer')!
- this.editor.getPlugs()?.get(Mover)?.move(moveAfterEl)
+ this.editor.getPlugins()?.get(Mover)?.move(moveAfterEl)
- const $relative =
- this.editor.ctx.conf.scrollRelativeTo && this.editor.ctx.conf.scrollRelativeTo()
+ const $relative = this.editor.getOptions().getConf().get().scrollRelativeTo?.()
Ui.scrollIntoView(this.editor.getUI().$el, true, $relative)
const plugin = this.editor
- .getPlugs()
- ?.getPlugs()
+ .getPlugins()
+ ?.getPlugins()
.find((p) => p.editorStateEffectWhen === state)
if (plugin && plugin.editorStateEffect) {
this.stateUnmountFn = plugin.editorStateEffect(payload.comment)
diff --git a/ui/artalk/src/layer/layer-manager.ts b/ui/artalk/src/layer/layer-manager.ts
index da47e1e4..73be931a 100644
--- a/ui/artalk/src/layer/layer-manager.ts
+++ b/ui/artalk/src/layer/layer-manager.ts
@@ -1,19 +1,11 @@
import { getScrollbarHelper } from './scrollbar-helper'
import { LayerWrap } from './wrap'
-import type { ContextApi } from '@/types'
+import type { LayerManager as ILayerManager } from '@/types'
-export class LayerManager {
- private wrap: LayerWrap
+export class LayerManager implements ILayerManager {
+ private wrap = new LayerWrap()
- constructor(ctx: ContextApi) {
- this.wrap = new LayerWrap()
- document.body.appendChild(this.wrap.getWrap())
-
- ctx.on('unmounted', () => {
- this.wrap.getWrap().remove()
- })
-
- // 记录页面原始 CSS 属性
+ constructor() {
getScrollbarHelper().init()
}
@@ -24,4 +16,8 @@ export class LayerManager {
create(name: string, el?: HTMLElement) {
return this.wrap.createItem(name, el)
}
+
+ destroy() {
+ this.wrap.getWrap().remove()
+ }
}
diff --git a/ui/artalk/src/layer/layer.ts b/ui/artalk/src/layer/layer.ts
index f60319c5..da64471b 100644
--- a/ui/artalk/src/layer/layer.ts
+++ b/ui/artalk/src/layer/layer.ts
@@ -1,11 +1,11 @@
-import type { Layer as LayerApi } from '@/types/layer'
+import type { Layer as ILayer } from '@/types/layer'
export interface LayerOptions {
onShow: () => void
onHide: () => void
}
-export class Layer implements LayerApi {
+export class Layer implements ILayer {
private allowMaskClose = true
private onAfterHide?: () => void
diff --git a/ui/artalk/src/layer/sidebar-layer.ts b/ui/artalk/src/layer/sidebar-layer.ts
index 3b241af7..3ab71d9e 100644
--- a/ui/artalk/src/layer/sidebar-layer.ts
+++ b/ui/artalk/src/layer/sidebar-layer.ts
@@ -1,20 +1,39 @@
import SidebarHTML from './sidebar-layer.html?raw'
-import type { Layer } from './layer'
-import type { ContextApi, SidebarShowPayload } from '@/types'
-import Component from '@/lib/component'
+import type {
+ SidebarShowPayload,
+ ConfigManager,
+ UserManager,
+ LayerManager,
+ Layer,
+ SidebarLayer as ISidebarLayer,
+ CheckerManager,
+} from '@/types'
import * as Utils from '@/lib/utils'
import * as Ui from '@/lib/ui'
+import type { Api } from '@/api'
-export default class SidebarLayer extends Component {
+export interface SidebarLayerOptions {
+ onShow?: () => void
+ onHide?: () => void
+
+ getCheckers: () => CheckerManager
+ getApi: () => Api
+ getConf: () => ConfigManager
+ getUser: () => UserManager
+ getLayers: () => LayerManager
+}
+
+export class SidebarLayer implements ISidebarLayer {
+ private opts: SidebarLayerOptions
+ public $el: HTMLElement
public layer?: Layer
public $header: HTMLElement
public $closeBtn: HTMLElement
public $iframeWrap: HTMLElement
public $iframe?: HTMLIFrameElement
- constructor(ctx: ContextApi) {
- super(ctx)
-
+ constructor(opts: SidebarLayerOptions) {
+ this.opts = opts
this.$el = Utils.createElement(SidebarHTML)
this.$header = this.$el.querySelector('.atk-sidebar-header')!
this.$closeBtn = this.$header.querySelector('.atk-sidebar-close')!
@@ -23,11 +42,10 @@ export default class SidebarLayer extends Component {
this.$closeBtn.onclick = () => {
this.hide()
}
+ }
- // event
- this.ctx.on('user-changed', () => {
- this.refreshWhenShow = true
- })
+ public async onUserChanged() {
+ this.refreshWhenShow = true
}
/** Refresh iFrame when show */
@@ -72,11 +90,7 @@ export default class SidebarLayer extends Component {
this.animTimer = undefined
this.$el.style.transform = 'translate(0, 0)'
- setTimeout(() => {
- this.ctx.getData().updateNotifies([])
- }, 0)
-
- this.ctx.trigger('sidebar-show')
+ this.opts.onShow?.()
}, 100)
}
@@ -89,15 +103,15 @@ export default class SidebarLayer extends Component {
private async authCheck(opts: { onSuccess: () => void }) {
const data = (
- await this.ctx.getApi().user.getUserStatus({
- ...this.ctx.getApi().getUserFields(),
+ await this.opts.getApi().user.getUserStatus({
+ ...this.opts.getApi().getUserFields(),
})
).data
if (data.is_admin && !data.is_login) {
this.refreshWhenShow = true
// show checker layer
- this.ctx.checkAdmin({
+ this.opts.getCheckers().checkAdmin({
onSuccess: () => {
setTimeout(() => {
opts.onSuccess()
@@ -116,11 +130,8 @@ export default class SidebarLayer extends Component {
private initLayer() {
if (this.layer) return
- this.layer = this.ctx.get('layerManager').create('sidebar', this.$el)
+ this.layer = this.opts.getLayers().create('sidebar', this.$el)
this.layer.setOnAfterHide(() => {
- // 防止评论框被吞
- this.ctx.editorResetState()
-
// interrupt animation
this.animTimer && clearTimeout(this.animTimer)
@@ -128,7 +139,7 @@ export default class SidebarLayer extends Component {
this.$el.style.transform = ''
// trigger event
- this.ctx.trigger('sidebar-hide')
+ this.opts.onHide?.()
})
}
@@ -141,14 +152,14 @@ export default class SidebarLayer extends Component {
const baseURL = import.meta.env.DEV
? 'http://localhost:23367/'
: Utils.getURLBasedOnApi({
- base: this.ctx.conf.server,
+ base: this.opts.getConf().get().server,
path: '/sidebar/',
})
const query: any = {
- pageKey: this.conf.pageKey,
- site: this.conf.site || '',
- user: JSON.stringify(this.ctx.get('user').getData()),
+ pageKey: this.opts.getConf().get().pageKey,
+ site: this.opts.getConf().get().site || '',
+ user: JSON.stringify(this.opts.getUser().getData()),
time: +new Date(),
}
@@ -162,9 +173,9 @@ export default class SidebarLayer extends Component {
}
private getDarkMode() {
- return this.conf.darkMode === 'auto'
+ return this.opts.getConf().get().darkMode === 'auto'
? window.matchMedia('(prefers-color-scheme: dark)').matches
- : this.conf.darkMode
+ : this.opts.getConf().get().darkMode
}
private iframeLoad($iframe: HTMLIFrameElement, src: string) {
diff --git a/ui/artalk/src/lib/component.ts b/ui/artalk/src/lib/component.ts
deleted file mode 100644
index c1793d25..00000000
--- a/ui/artalk/src/lib/component.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { ContextApi } from '@/types'
-
-export default abstract class Component {
- public $el!: HTMLElement
- public get conf() {
- return this.ctx.conf
- }
-
- public constructor(public ctx: ContextApi) {}
-
- getEl() {
- return this.$el
- }
-}
diff --git a/ui/artalk/src/lib/event-manager.ts b/ui/artalk/src/lib/event-manager.ts
index ac23e9fa..fa730392 100644
--- a/ui/artalk/src/lib/event-manager.ts
+++ b/ui/artalk/src/lib/event-manager.ts
@@ -1,37 +1,15 @@
-export type EventHandler = (payload: T) => void
-export interface Event
- extends EventOptions {
- name: K
- handler: EventHandler
-}
-export interface EventOptions {
- once?: boolean
-}
-
-export interface EventManagerFuncs {
- on(
- name: K,
- handler: EventHandler,
- opts?: EventOptions,
- ): void
- off(name: K, handler: EventHandler): void
- trigger(name: K, payload?: PayloadMap[K]): void
-}
+import type { EventManager as IEventManager, Event, EventHandler, EventOptions } from '@/types'
-export default class EventManager implements EventManagerFuncs {
- private events: Event[] = []
+export class EventManager implements IEventManager {
+ private events: Event[] = []
/**
* Add an event listener for a specific event name
*/
- public on(
- name: K,
- handler: EventHandler,
- opts: EventOptions = {},
- ) {
+ public on(name: K, handler: EventHandler, opts: EventOptions = {}) {
this.events.push({
name,
- handler: handler as EventHandler,
+ handler: handler as EventHandler,
...opts,
})
}
@@ -39,7 +17,7 @@ export default class EventManager implements EventManagerFuncs(name: K, handler: EventHandler) {
+ public off(name: K, handler: EventHandler) {
if (!handler) return // not allow remove all events with same name
this.events = this.events.filter((evt) => !(evt.name === name && evt.handler === handler))
}
@@ -47,7 +25,7 @@ export default class EventManager implements EventManagerFuncs(name: K, payload?: PayloadMap[K]) {
+ public trigger(name: K, payload?: T[K]) {
this.events
.slice(0) // make a copy, in case listeners are removed while iterating
.filter((evt) => evt.name === name && typeof evt.handler === 'function')
diff --git a/ui/artalk/src/lib/marked-renderer.ts b/ui/artalk/src/lib/marked-renderer.ts
index e9c841e5..e9f7113c 100644
--- a/ui/artalk/src/lib/marked-renderer.ts
+++ b/ui/artalk/src/lib/marked-renderer.ts
@@ -1,9 +1,9 @@
import { marked, Tokens } from 'marked'
import { renderCode } from './highlight'
-import type { ArtalkConfig } from '@/types'
+import type { Config } from '@/types'
export interface RendererOptions {
- imgLazyLoad: ArtalkConfig['imgLazyLoad']
+ imgLazyLoad: Config['imgLazyLoad']
}
export function getRenderer(options: RendererOptions) {
diff --git a/ui/artalk/src/lib/marked.ts b/ui/artalk/src/lib/marked.ts
index f09f1ab9..8dc36b83 100644
--- a/ui/artalk/src/lib/marked.ts
+++ b/ui/artalk/src/lib/marked.ts
@@ -4,7 +4,7 @@ import type { MarkedOptions } from 'marked'
import { sanitize } from './sanitizer'
import { renderCode } from './highlight'
import { getRenderer } from './marked-renderer'
-import type { ArtalkConfig } from '@/types'
+import type { Config } from '@/types'
type Replacer = (raw: string) => string
@@ -27,8 +27,8 @@ export function setReplacers(arr: Replacer[]) {
}
export interface MarkedInitOptions {
- markedOptions: ArtalkConfig['markedOptions']
- imgLazyLoad: ArtalkConfig['imgLazyLoad']
+ markedOptions: Config['markedOptions']
+ imgLazyLoad: Config['imgLazyLoad']
}
/** 初始化 marked */
diff --git a/ui/artalk/src/lib/user.ts b/ui/artalk/src/lib/user.ts
index 8d0de67a..4ff524f9 100644
--- a/ui/artalk/src/lib/user.ts
+++ b/ui/artalk/src/lib/user.ts
@@ -1,4 +1,4 @@
-import type { LocalUser } from '@/types'
+import type { LocalUser, UserManager as IUserManager } from '@/types'
const LOCAL_USER_KEY = 'ArtalkUser'
@@ -6,7 +6,7 @@ interface UserOpts {
onUserChanged?: (user: LocalUser) => void
}
-class User {
+export class UserManager implements IUserManager {
private data: LocalUser
constructor(private opts: UserOpts) {
@@ -55,4 +55,4 @@ class User {
}
}
-export default User
+export default UserManager
diff --git a/ui/artalk/src/lib/watch-conf.ts b/ui/artalk/src/lib/watch-conf.ts
index d28c47a5..f0a7b7bb 100644
--- a/ui/artalk/src/lib/watch-conf.ts
+++ b/ui/artalk/src/lib/watch-conf.ts
@@ -1,13 +1,19 @@
-import type { ArtalkConfig, ContextApi } from '@/types'
+import type { Config, EventManager } from '@/types'
-export function watchConf(
- ctx: ContextApi,
- keys: T,
- effect: (conf: Pick) => void,
-): void {
+export function watchConf({
+ keys,
+ effect,
+ getConf,
+ getEvents,
+}: {
+ keys: T
+ effect: (conf: Pick) => void
+ getConf: () => Config
+ getEvents: () => EventManager
+}): void {
const deepEqual = (a: any, b: any) => JSON.stringify(a) === JSON.stringify(b)
const val = () => {
- const conf = ctx.getConf()
+ const conf = getConf()
const res: any = {}
keys.forEach((key) => {
res[key] = conf[key]
@@ -24,6 +30,6 @@ export function watchConf(
effect(newVal)
}
}
- ctx.on('mounted', handler)
- ctx.on('updated', handler)
+ getEvents().on('mounted', handler)
+ getEvents().on('updated', handler)
}
diff --git a/ui/artalk/src/list/comment.ts b/ui/artalk/src/list/comment.ts
index 753cc0f9..708adbd8 100644
--- a/ui/artalk/src/list/comment.ts
+++ b/ui/artalk/src/list/comment.ts
@@ -1,48 +1,54 @@
-import type { ContextApi, CommentData } from '@/types'
+import type { ConfigManager, EventManager, CommentData, DataManager } from '@/types'
import { CommentNode } from '@/comment'
+import type { Api } from '@/api'
interface CreateCommentNodeOptions {
+ getApi: () => Api
+ getEvents: () => EventManager
+ getData: () => DataManager
+ getConf: () => ConfigManager
+ replyComment: (c: CommentData, $el: HTMLElement) => void
+ editComment: (c: CommentData, $el: HTMLElement) => void
forceFlatMode?: boolean
}
export function createCommentNode(
- ctx: ContextApi,
+ opts: CreateCommentNodeOptions,
comment: CommentData,
replyComment?: CommentData,
- opts?: CreateCommentNodeOptions,
): CommentNode {
+ const conf = opts.getConf().get()
+
const instance = new CommentNode(comment, {
onAfterRender: () => {
- ctx.trigger('comment-rendered', instance)
+ opts.getEvents().trigger('comment-rendered', instance)
},
onDelete: (c: CommentNode) => {
- ctx.getData().deleteComment(c.getID())
+ opts.getData().deleteComment(c.getID())
},
replyTo: replyComment,
// TODO simplify reference
flatMode:
- typeof opts?.forceFlatMode === 'boolean'
- ? opts?.forceFlatMode
- : (ctx.conf.flatMode as boolean),
- gravatar: ctx.conf.gravatar,
- nestMax: ctx.conf.nestMax,
- heightLimit: ctx.conf.heightLimit,
- avatarURLBuilder: ctx.conf.avatarURLBuilder,
- scrollRelativeTo: ctx.conf.scrollRelativeTo,
- vote: ctx.conf.vote,
- voteDown: ctx.conf.voteDown,
- uaBadge: ctx.conf.uaBadge,
- dateFormatter: ctx.conf.dateFormatter,
+ typeof opts?.forceFlatMode === 'boolean' ? opts?.forceFlatMode : (conf.flatMode as boolean),
+ gravatar: conf.gravatar,
+ nestMax: conf.nestMax,
+ heightLimit: conf.heightLimit,
+ avatarURLBuilder: conf.avatarURLBuilder,
+ scrollRelativeTo: conf.scrollRelativeTo,
+ vote: conf.vote,
+ voteDown: conf.voteDown,
+ uaBadge: conf.uaBadge,
+ dateFormatter: conf.dateFormatter,
// TODO: move to plugin folder and remove from core
- getApi: () => ctx.getApi(),
- replyComment: (c, $el) => ctx.replyComment(c, $el),
- editComment: (c, $el) => ctx.editComment(c, $el),
+ getApi: opts.getApi,
+ replyComment: opts.replyComment,
+ editComment: opts.editComment,
})
- // 渲染元素
+ // Render comment
instance.render()
return instance
diff --git a/ui/artalk/src/list/layout/flat.ts b/ui/artalk/src/list/layout/flat.ts
index 1fa269c0..a0dc07e6 100644
--- a/ui/artalk/src/list/layout/flat.ts
+++ b/ui/artalk/src/list/layout/flat.ts
@@ -1,6 +1,5 @@
import type { LayoutStrategyCreator, LayoutOptions } from '.'
import type { CommentData } from '@/types'
-import * as Ui from '@/lib/ui'
export const createFlatStrategy: LayoutStrategyCreator = (opts) => ({
import: (comments) => {
diff --git a/ui/artalk/src/list/layout/nest.ts b/ui/artalk/src/list/layout/nest.ts
index c08574dd..7e2aade8 100644
--- a/ui/artalk/src/list/layout/nest.ts
+++ b/ui/artalk/src/list/layout/nest.ts
@@ -1,6 +1,5 @@
import type { LayoutStrategyCreator } from '.'
import type { CommentNode } from '@/comment'
-import * as Ui from '@/lib/ui'
import * as ListNest from '@/list/nest'
export const createNestStrategy: LayoutStrategyCreator = (opts) => ({
diff --git a/ui/artalk/src/list/list.ts b/ui/artalk/src/list/list.ts
index 6bcbbe95..bc172809 100644
--- a/ui/artalk/src/list/list.ts
+++ b/ui/artalk/src/list/list.ts
@@ -2,47 +2,69 @@ import ListHTML from './list.html?raw'
import { ListLayout } from './layout'
import { createCommentNode } from './comment'
import { initListPaginatorFunc } from './page'
-import type { ContextApi } from '@/types'
-import Component from '@/lib/component'
+import type { CommentData, EventManager, DataManager, ConfigManager, List as IList } from '@/types'
import * as Utils from '@/lib/utils'
import { CommentNode } from '@/comment'
+import type { Api } from '@/api'
-export default class List extends Component {
- /** 列表评论集区域元素 */
- $commentsWrap!: HTMLElement
- public getCommentsWrapEl() {
+export interface ListOptions {
+ getApi: () => Api
+ getEvents: () => EventManager
+ getConf: () => ConfigManager
+ getData: () => DataManager
+
+ replyComment: (c: CommentData, $el: HTMLElement) => void
+ editComment: (c: CommentData, $el: HTMLElement) => void
+ resetEditorState: () => void
+ onListGotoFirst?: () => void
+}
+
+export class List implements IList {
+ private opts: ListOptions
+ private $el: HTMLElement
+ getEl() {
+ return this.$el
+ }
+
+ private $commentsWrap: HTMLElement
+ getCommentsWrapEl() {
return this.$commentsWrap
}
- protected commentNodes: CommentNode[] = []
+ private commentNodes: CommentNode[] = []
getCommentNodes() {
return this.commentNodes
}
- constructor(ctx: ContextApi) {
- super(ctx)
+ constructor(opts: ListOptions) {
+ this.opts = opts
// Init base element
this.$el = Utils.createElement(ListHTML)
this.$commentsWrap = this.$el.querySelector('.atk-list-comments-wrap')!
// Init paginator
- initListPaginatorFunc(ctx)
+ initListPaginatorFunc({
+ getList: () => this,
+ ...opts,
+ })
// Bind events
this.initCrudEvents()
}
- getListLayout({ forceFlatMode }: { forceFlatMode?: boolean } = {}) {
+ getLayout({ forceFlatMode }: { forceFlatMode?: boolean } = {}) {
return new ListLayout({
$commentsWrap: this.$commentsWrap,
- nestSortBy: this.ctx.conf.nestSort,
- nestMax: this.ctx.conf.nestMax,
+ nestSortBy: this.opts.getConf().get().nestSort,
+ nestMax: this.opts.getConf().get().nestMax,
flatMode:
- typeof forceFlatMode === 'boolean' ? forceFlatMode : (this.ctx.conf.flatMode as boolean),
+ typeof forceFlatMode === 'boolean'
+ ? forceFlatMode
+ : (this.opts.getConf().get().flatMode as boolean),
// flatMode must be boolean because it had been handled when Artalk.init
createCommentNode: (d, r) => {
- const node = createCommentNode(this.ctx, d, r, { forceFlatMode })
+ const node = createCommentNode({ forceFlatMode, ...this.opts }, d, r)
this.commentNodes.push(node) // store node instance
return node
},
@@ -51,12 +73,12 @@ export default class List extends Component {
}
private initCrudEvents() {
- this.ctx.on('list-load', (comments) => {
+ this.opts.getEvents().on('list-load', (comments) => {
// 导入数据
- this.getListLayout().import(comments)
+ this.getLayout().import(comments)
})
- this.ctx.on('list-loaded', (comments) => {
+ this.opts.getEvents().on('list-loaded', (comments) => {
if (comments.length === 0) {
this.commentNodes = []
this.$commentsWrap.innerHTML = ''
@@ -64,15 +86,15 @@ export default class List extends Component {
})
// When comment insert
- this.ctx.on('comment-inserted', (comment) => {
+ this.opts.getEvents().on('comment-inserted', (comment) => {
const replyComment = comment.rid
? this.commentNodes.find((c) => c.getID() === comment.rid)?.getData()
: undefined
- this.getListLayout().insert(comment, replyComment)
+ this.getLayout().insert(comment, replyComment)
})
// When comment delete
- this.ctx.on('comment-deleted', (comment) => {
+ this.opts.getEvents().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`)
@@ -84,7 +106,7 @@ export default class List extends Component {
})
// When comment update
- this.ctx.on('comment-updated', (comment) => {
+ this.opts.getEvents().on('comment-updated', (comment) => {
const node = this.commentNodes.find((c) => c.getID() === comment.id)
node && node.setData(comment)
})
diff --git a/ui/artalk/src/list/page.ts b/ui/artalk/src/list/page.ts
index 803ba1dc..139136f2 100644
--- a/ui/artalk/src/list/page.ts
+++ b/ui/artalk/src/list/page.ts
@@ -1,19 +1,19 @@
+import type { ListOptions } from './list'
import { Paginator } from './paginator'
import ReadMorePaginator from './paginator/read-more'
import UpDownPaginator from './paginator/up-down'
import $t from '@/i18n'
-import type { ArtalkConfig, ContextApi } from '@/types'
+import type { Config, List, ListLastFetchData } from '@/types'
-function createPaginatorByConf(conf: Pick): Paginator {
+function createPaginatorByConf(conf: Pick): Paginator {
if (conf.pagination.readMore) return new ReadMorePaginator()
return new UpDownPaginator()
}
-function getPageDataByLastData(ctx: ContextApi): {
+function getPageDataByLastData(last: ListLastFetchData | undefined): {
offset: number
total: number
} {
- const last = ctx.getData().getListLastFetch()
const r = { offset: 0, total: 0 }
if (!last) return r
@@ -23,12 +23,17 @@ function getPageDataByLastData(ctx: ContextApi): {
return r
}
-export const initListPaginatorFunc = (ctx: ContextApi) => {
+export interface PaginatorInitOptions extends ListOptions {
+ getList: () => List
+}
+
+export const initListPaginatorFunc = (opts: PaginatorInitOptions) => {
let paginator: Paginator | null = null
// Init paginator when conf loaded
- ctx.watchConf(['pagination', 'locale'], (conf) => {
- const list = ctx.get('list')
+ opts.getConf().watchConf(['pagination', 'locale'], (conf) => {
+ const list = opts.getList()
+ const data = opts.getData()
if (paginator) paginator.dispose() // if had been init, dispose it
@@ -36,47 +41,48 @@ export const initListPaginatorFunc = (ctx: ContextApi) => {
paginator = createPaginatorByConf(conf)
// create paginator dom
- const { offset, total } = getPageDataByLastData(ctx)
+ const { offset, total } = getPageDataByLastData(data.getListLastFetch())
const $paginator = paginator.create({
- ctx,
pageSize: conf.pagination.pageSize,
total,
readMoreAutoLoad: conf.pagination.autoLoad,
+
+ ...opts,
})
// mount paginator dom
- list.$commentsWrap.after($paginator)
+ list.getCommentsWrapEl().after($paginator)
// update paginator info
paginator?.update(offset, total)
})
// When list loaded
- ctx.on('list-loaded', (comments) => {
+ opts.getEvents().on('list-loaded', (comments) => {
// update paginator info
- const { offset, total } = getPageDataByLastData(ctx)
+ const { offset, total } = getPageDataByLastData(opts.getData().getListLastFetch())
paginator?.update(offset, total)
})
// When list fetch
- ctx.on('list-fetch', (params) => {
+ opts.getEvents().on('list-fetch', (params) => {
// if clear comments when fetch new page data
- if (ctx.getData().getComments().length > 0 && paginator?.getIsClearComments(params)) {
- ctx.getData().clearComments()
+ if (opts.getData().getComments().length > 0 && paginator?.getIsClearComments(params)) {
+ opts.getData().clearComments()
}
})
// When list error
- ctx.on('list-failed', () => {
+ opts.getEvents().on('list-failed', () => {
paginator?.showErr?.($t('loadFail'))
})
// loading
- ctx.on('list-fetch', (params) => {
+ opts.getEvents().on('list-fetch', (params) => {
paginator?.setLoading(true)
})
- ctx.on('list-fetched', ({ params }) => {
+ opts.getEvents().on('list-fetched', ({ params }) => {
paginator?.setLoading(false)
})
}
diff --git a/ui/artalk/src/list/paginator/index.ts b/ui/artalk/src/list/paginator/index.ts
index 27da01f5..f476eca0 100644
--- a/ui/artalk/src/list/paginator/index.ts
+++ b/ui/artalk/src/list/paginator/index.ts
@@ -1,9 +1,8 @@
+import type { ListOptions } from '../list'
import UpDownPaginator from './up-down'
import ReadMorePaginator from './read-more'
-import type { ContextApi, ListFetchParams } from '@/types'
-export interface IPgHolderOpt {
- ctx: ContextApi
+export interface IPgHolderOpt extends ListOptions {
total: number
pageSize: number
diff --git a/ui/artalk/src/list/paginator/read-more.ts b/ui/artalk/src/list/paginator/read-more.ts
index 1eb885e1..664a5f86 100644
--- a/ui/artalk/src/list/paginator/read-more.ts
+++ b/ui/artalk/src/list/paginator/read-more.ts
@@ -1,5 +1,4 @@
import type { Paginator, IPgHolderOpt } from '.'
-import type { ListFetchParams } from '@/types'
import ReadMoreBtn from '@/components/read-more-btn'
import $t from '@/i18n'
@@ -17,7 +16,7 @@ export default class ReadMorePaginator implements Paginator {
this.instance = new ReadMoreBtn({
pageSize: opt.pageSize,
onClick: async (o) => {
- opt.ctx.fetch({
+ opt.getData().fetchComments({
offset: o,
})
},
@@ -27,10 +26,10 @@ export default class ReadMorePaginator implements Paginator {
// 滚动到底部自动加载
if (opt.readMoreAutoLoad) {
this.onReachedBottom = () => {
- if (!this.instance.hasMore || this.opt.ctx.getData().getLoading()) return
+ if (!this.instance.hasMore || this.opt.getData().getLoading()) return
this.instance.click()
}
- this.opt.ctx.on('list-reach-bottom', this.onReachedBottom)
+ this.opt.getEvents().on('list-reach-bottom', this.onReachedBottom)
}
return this.instance.$el
@@ -61,7 +60,7 @@ export default class ReadMorePaginator implements Paginator {
}
dispose(): void {
- this.onReachedBottom && this.opt.ctx.off('list-reach-bottom', this.onReachedBottom)
+ this.onReachedBottom && this.opt.getEvents().off('list-reach-bottom', this.onReachedBottom)
this.instance.$el.remove()
}
}
diff --git a/ui/artalk/src/list/paginator/up-down.ts b/ui/artalk/src/list/paginator/up-down.ts
index 3ec10c50..f39c2c2b 100644
--- a/ui/artalk/src/list/paginator/up-down.ts
+++ b/ui/artalk/src/list/paginator/up-down.ts
@@ -11,12 +11,12 @@ export default class UpDownPaginator implements Paginator {
this.instance = new PaginationComponent(opt.total, {
pageSize: opt.pageSize,
onChange: async (o) => {
- opt.ctx.editorResetState() // 防止评论框被吞
+ opt.resetEditorState()
- opt.ctx.fetch({
+ opt.getData().fetchComments({
offset: o,
onSuccess: () => {
- opt.ctx.listGotoFirst()
+ opt.onListGotoFirst?.()
},
})
},
diff --git a/ui/artalk/src/load.ts b/ui/artalk/src/load.ts
deleted file mode 100644
index 91e67153..00000000
--- a/ui/artalk/src/load.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { DefaultPlugins } from './plugins'
-import type { ArtalkConfigPartial, ArtalkPlugin, ContextApi } from '@/types'
-import { handleConfFormServer } from '@/config'
-import { showErrorDialog } from '@/components/error-dialog'
-
-/**
- * Global Plugins for all Artalk instances
- */
-export const GlobalPlugins: Set = new Set([...DefaultPlugins])
-
-/**
- * Plugin options for plugin initialization
- */
-export const PluginOptions: WeakMap = new WeakMap()
-
-export async function load(ctx: ContextApi) {
- const loadedPlugins = new Set()
- const loadPlugins = (plugins: Set) => {
- plugins.forEach((plugin) => {
- if (typeof plugin === 'function' && !loadedPlugins.has(plugin)) {
- plugin(ctx, PluginOptions.get(plugin))
- loadedPlugins.add(plugin)
- }
- })
- }
-
- // Load local plugins
- loadPlugins(GlobalPlugins)
-
- // Get conf from server
- const { data } = await ctx
- .getApi()
- .conf.conf()
- .catch((err) => {
- onLoadErr(ctx, err)
- throw err
- })
-
- // Initial config
- let conf: ArtalkConfigPartial = {
- apiVersion: data.version?.version, // version info
- }
-
- // Reference conf from backend
- if (ctx.conf.useBackendConf) {
- if (!data.frontend_conf)
- throw new Error(
- 'The remote backend does not respond to the frontend conf, but `useBackendConf` conf is enabled',
- )
- conf = { ...conf, ...handleConfFormServer(data.frontend_conf) }
- }
-
- // Apply conf modifier
- ctx.conf.remoteConfModifier && ctx.conf.remoteConfModifier(conf)
-
- // Dynamically load network plugins
- conf.pluginURLs &&
- (await loadNetworkPlugins(conf.pluginURLs, ctx.conf.server)
- .then((plugins) => {
- loadPlugins(plugins)
- })
- .catch((err) => {
- console.error('Failed to load plugin', err)
- }))
-
- // After all plugins are loaded
- ctx.trigger('created')
-
- // Apply conf updating
- ctx.updateConf(conf)
-
- // Trigger mounted event
- ctx.trigger('mounted')
-
- // Load comment list
- if (!ctx.conf.remoteConfModifier) {
- // only auto fetch when no remoteConfModifier
- ctx.fetch({ offset: 0 })
- }
-}
-
-/**
- * Dynamically load plugins from Network
- */
-async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise> {
- const networkPlugins = new Set()
- if (!scripts || !Array.isArray(scripts)) return networkPlugins
-
- const tasks: Promise[] = []
-
- scripts.forEach((url) => {
- // check url valid
- if (!/^(http|https):\/\//.test(url))
- url = `${apiBase.replace(/\/$/, '')}/${url.replace(/^\//, '')}`
-
- tasks.push(
- new Promise((resolve) => {
- // check if loaded
- if (document.querySelector(`script[src="${url}"]`)) {
- resolve()
- return
- }
-
- // load script
- const script = document.createElement('script')
- script.src = url
- document.head.appendChild(script)
- script.onload = () => resolve()
- script.onerror = (err) => {
- console.error('[artalk] Failed to load plugin', err)
- resolve()
- }
- }),
- )
- })
-
- await Promise.all(tasks)
-
- // Read ArtalkPlugins object from window
- Object.values(window.ArtalkPlugins || {}).forEach((plugin) => {
- if (typeof plugin === 'function') networkPlugins.add(plugin)
- })
-
- return networkPlugins
-}
-
-export function onLoadErr(ctx: ContextApi, err: any) {
- let sidebarOpenView = ''
-
- // if response err_no_site, modify the sidebar open view to create site
- if (err.data?.err_no_site) {
- const viewLoadParam = {
- create_name: ctx.conf.site,
- create_urls: `${window.location.protocol}//${window.location.host}`,
- }
- sidebarOpenView = `sites|${JSON.stringify(viewLoadParam)}`
- }
-
- showErrorDialog({
- $err: ctx.get('list').$el,
- errMsg: err.msg || String(err),
- errData: err.data,
- retryFn: () => load(ctx),
- onOpenSidebar: ctx.get('user').getData().is_admin
- ? () =>
- ctx.showSidebar({
- view: sidebarOpenView as any,
- })
- : undefined, // only show open sidebar button when user is admin
- })
-}
diff --git a/ui/artalk/src/main.ts b/ui/artalk/src/main.ts
index 4be9b61a..f34ff1de 100644
--- a/ui/artalk/src/main.ts
+++ b/ui/artalk/src/main.ts
@@ -1,16 +1,17 @@
import Artalk from './artalk'
import type * as ArtalkType from './types'
+import { Defaults } from './defaults'
export type * from './types'
-export { ArtalkType }
+export { ArtalkType, Defaults }
export default Artalk
-// Expose the static methods from the Artalk class
-// because direct export of static methods is not supported
-// for adapting to different environments like CommonJS and browser IIFE
-// for example, we can use `Artalk.init()` rather than `Artalk.default.init()`
-// therefore, we need to manually expose the static methods in the Artalk class.
+// Manually expose the static methods from the Artalk class.
+// Directly exporting static methods from a class is not supported,
+// and it's important to maintain consistency in the library's API.
+// To ensure compatibility across different environments, such as CommonJS and browser IIFE,
+// this approach allows us to use `Artalk.init()` instead of `Artalk.default.init()`.
export const init = Artalk.init
export const use = Artalk.use
export const loadCountWidget = Artalk.loadCountWidget
diff --git a/ui/artalk/src/mount.ts b/ui/artalk/src/mount.ts
new file mode 100644
index 00000000..b3e2d6d6
--- /dev/null
+++ b/ui/artalk/src/mount.ts
@@ -0,0 +1,106 @@
+import { handleConfFormServer } from './config'
+import { DefaultPlugins } from './plugins'
+import { mergeDeep } from './lib/merge-deep'
+import { MountError } from './plugins/mount-error'
+import type { ConfigPartial, ArtalkPlugin, Context } from '@/types'
+
+/**
+ * Global Plugins for all Artalk instances
+ */
+export const GlobalPlugins: Set = new Set([...DefaultPlugins])
+
+/**
+ * Plugin options for plugin initialization
+ */
+export const PluginOptions: WeakMap = new WeakMap()
+
+export async function mount(localConf: ConfigPartial, ctx: Context) {
+ const loaded = new Set()
+ const loadPlugins = (plugins: Set) => {
+ plugins.forEach((plugin) => {
+ if (typeof plugin !== 'function') return
+ if (loaded.has(plugin)) return
+ plugin(ctx, PluginOptions.get(plugin))
+ loaded.add(plugin)
+ })
+ }
+
+ // Load local plugins
+ loadPlugins(GlobalPlugins)
+
+ // Get conf from server
+ const { data } = await ctx
+ .getApi()
+ .conf.conf()
+ .catch((err) => {
+ MountError(ctx, { err, onRetry: () => mount(localConf, ctx) })
+ throw err
+ })
+
+ // Merge remote and local config
+ let conf: ConfigPartial = {
+ ...localConf,
+ apiVersion: data.version?.version, // server version info
+ }
+
+ const remoteConf = handleConfFormServer(data.frontend_conf || {})
+ conf = mergeDeep(conf, remoteConf)
+
+ // Apply local + remote conf
+ ctx.updateConf(conf)
+
+ // Load remote plugins
+ conf.pluginURLs &&
+ (await loadNetworkPlugins(conf.pluginURLs, ctx.getConf().server)
+ .then((plugins) => {
+ loadPlugins(plugins)
+ })
+ .catch((err) => {
+ console.error('Failed to load plugin', err)
+ }))
+}
+
+/**
+ * Dynamically load plugins from Network
+ */
+async function loadNetworkPlugins(scripts: string[], apiBase: string): Promise> {
+ const networkPlugins = new Set()
+ if (!scripts || !Array.isArray(scripts)) return networkPlugins
+
+ const tasks: Promise[] = []
+
+ scripts.forEach((url) => {
+ // check url valid
+ if (!/^(http|https):\/\//.test(url))
+ url = `${apiBase.replace(/\/$/, '')}/${url.replace(/^\//, '')}`
+
+ tasks.push(
+ new Promise((resolve) => {
+ // check if loaded
+ if (document.querySelector(`script[src="${url}"]`)) {
+ resolve()
+ return
+ }
+
+ // load script
+ const script = document.createElement('script')
+ script.src = url
+ document.head.appendChild(script)
+ script.onload = () => resolve()
+ script.onerror = (err) => {
+ console.error('[artalk] Failed to load plugin', err)
+ resolve()
+ }
+ }),
+ )
+ })
+
+ await Promise.all(tasks)
+
+ // Read ArtalkPlugins object from window
+ Object.values(window.ArtalkPlugins || {}).forEach((plugin) => {
+ if (typeof plugin === 'function') networkPlugins.add(plugin)
+ })
+
+ return networkPlugins
+}
diff --git a/ui/artalk/src/plugins/admin-only-elem.ts b/ui/artalk/src/plugins/admin-only-elem.ts
index 7f7b3c1b..af6c4469 100644
--- a/ui/artalk/src/plugins/admin-only-elem.ts
+++ b/ui/artalk/src/plugins/admin-only-elem.ts
@@ -1,11 +1,13 @@
import type { ArtalkPlugin } from '@/types'
export const AdminOnlyElem: ArtalkPlugin = (ctx) => {
+ const user = ctx.inject('user')
+
const scanApply = () => {
applyAdminOnlyEls(
- ctx.get('user').getData().is_admin,
+ user.getData().is_admin,
getAdminOnlyEls({
- $root: ctx.$root,
+ $root: ctx.getEl(),
}),
)
}
diff --git a/ui/artalk/src/plugins/dark-mode.ts b/ui/artalk/src/plugins/dark-mode.ts
index 7f26c7fb..6d9fc669 100644
--- a/ui/artalk/src/plugins/dark-mode.ts
+++ b/ui/artalk/src/plugins/dark-mode.ts
@@ -14,12 +14,15 @@ function updateClassnames($els: HTMLElement[], darkMode: boolean) {
}
export const DarkMode: ArtalkPlugin = (ctx) => {
+ const conf = ctx.inject('config')
+ const layers = ctx.inject('layers')
+
// the handler bind to Artalk instance, don't forget to remove it when Artalk instance destroyed
let darkModeAutoHandler: ((evt: MediaQueryListEvent) => void) | undefined
const sync = (darkMode: boolean | 'auto') => {
// the elements that classnames need to be updated when darkMode changed
- const $els = [ctx.$root, ctx.get('layerManager').getEl()]
+ const $els = [ctx.getEl(), layers.getEl()]
// init darkModeMedia if not exists, and only create once
if (!darkModeMedia) {
@@ -49,7 +52,7 @@ export const DarkMode: ArtalkPlugin = (ctx) => {
}
ctx.watchConf(['darkMode'], (conf) => sync(conf.darkMode))
- ctx.on('created', () => sync(ctx.conf.darkMode))
+ ctx.on('created', () => sync(conf.get().darkMode))
ctx.on('unmounted', () => {
// if handler exists, don't forget to remove it, or it will cause memory leak
darkModeAutoHandler && darkModeMedia?.removeEventListener('change', darkModeAutoHandler)
diff --git a/ui/artalk/src/plugins/editor-kit.ts b/ui/artalk/src/plugins/editor-kit.ts
index bb8419b5..66c31c95 100644
--- a/ui/artalk/src/plugins/editor-kit.ts
+++ b/ui/artalk/src/plugins/editor-kit.ts
@@ -1,8 +1,17 @@
import { getEnabledPlugs } from './editor'
-import EditorPlug from './editor/_plug'
+import EditorPlugin from './editor/_plug'
import PlugKit from './editor/_kit'
-import EventManager from '@/lib/event-manager'
-import type { EditorApi, ArtalkPlugin } from '@/types'
+import { EventManager } from '@/lib/event-manager'
+import type {
+ Editor,
+ ArtalkPlugin,
+ DataManager,
+ ConfigManager,
+ UserManager,
+ EditorPluginManager as IEditorPluginManager,
+ CheckerManager,
+} from '@/types'
+import type { Api } from '@/api'
export interface EditorEventPayloadMap {
mounted: undefined
@@ -10,44 +19,58 @@ export interface EditorEventPayloadMap {
'header-input': { field: string; $input: HTMLInputElement }
'header-change': { field: string; $input: HTMLInputElement }
'content-updated': string
- 'panel-show': EditorPlug
- 'panel-hide': EditorPlug
+ 'panel-show': EditorPlugin
+ 'panel-hide': EditorPlugin
'panel-close': undefined
'editor-close': undefined
'editor-open': undefined
+ 'editor-submit': undefined
+ 'editor-submitted': undefined
}
export const EditorKit: ArtalkPlugin = (ctx) => {
- const editor = ctx.get('editor')
+ ctx.provide(
+ 'editorPlugs',
+ (editor, config, user, data, checkers, api) => {
+ const pluginManager = new PluginManager({
+ getArtalkRootEl: () => ctx.getEl(),
+ getEditor: () => editor,
+ getConf: () => config,
+ getUser: () => user,
+ getApi: () => api,
+ getData: () => data,
+ getCheckers: () => checkers,
+ onSubmitted: () => ctx.trigger('editor-submitted'),
+ })
+ editor.setPlugins(pluginManager)
+ return pluginManager
+ },
+ ['editor', 'config', 'user', 'data', 'checkers', 'api'] as const,
+ )
+}
- const editorPlugs = new PlugManager(editor)
- ctx.inject('editorPlugs', editorPlugs)
+export interface PluginManagerOptions {
+ getArtalkRootEl: () => HTMLElement
+ getEditor: () => Editor
+ getConf: () => ConfigManager
+ getUser: () => UserManager
+ getApi: () => Api
+ getData: () => DataManager
+ getCheckers: () => CheckerManager
+ onSubmitted: () => void
}
-export class PlugManager {
- private plugs: EditorPlug[] = []
- private openedPlug: EditorPlug | null = null
+export class PluginManager implements IEditorPluginManager {
+ private plugins: EditorPlugin[] = []
+ private openedPlug: EditorPlugin | null = null
private events = new EventManager()
- getPlugs() {
- return this.plugs
- }
- getEvents() {
- return this.events
- }
-
- private clear() {
- this.plugs = []
- this.events = new EventManager()
- if (this.openedPlug) this.closePlugPanel()
- }
-
- constructor(public editor: EditorApi) {
+ constructor(public opts: PluginManagerOptions) {
let confLoaded = false // config not loaded at first time
- this.editor.ctx.watchConf(
- ['imgUpload', 'emoticons', 'preview', 'editorTravel', 'locale'],
- (conf) => {
+ this.opts
+ .getConf()
+ .watchConf(['imgUpload', 'emoticons', 'preview', 'editorTravel', 'locale'], (conf) => {
// trigger unmount event will call all plugs' unmount function
// (this will only be called while conf reloaded, not be called at first time)
confLoaded && this.getEvents().trigger('unmounted')
@@ -59,7 +82,7 @@ export class PlugManager {
getEnabledPlugs(conf).forEach((Plug) => {
// create the plug instance
const kit = new PlugKit(this)
- this.plugs.push(new Plug(kit))
+ this.plugins.push(new Plug(kit))
})
// trigger event for plug initialization
@@ -68,33 +91,56 @@ export class PlugManager {
// refresh the plug UI
this.loadPluginUI()
- },
- )
+ })
+
+ this.events.on('panel-close', () => this.closePluginPanel())
+ this.events.on('editor-submitted', () => opts.onSubmitted())
+ }
+
+ getPlugins() {
+ return this.plugins
+ }
+
+ getEvents() {
+ return this.events
+ }
- this.events.on('panel-close', () => this.closePlugPanel())
+ getEditor() {
+ return this.opts.getEditor()
+ }
+
+ getOptions() {
+ return this.opts
+ }
+
+ private clear() {
+ this.plugins = []
+ this.events = new EventManager()
+ if (this.openedPlug) this.closePluginPanel()
}
private loadPluginUI() {
// handle ui, clear and reset the plug btns and plug panels
- this.editor.getUI().$plugPanelWrap.innerHTML = ''
- this.editor.getUI().$plugPanelWrap.style.display = 'none'
- this.editor.getUI().$plugBtnWrap.innerHTML = ''
+ this.getEditor().getUI().$plugPanelWrap.innerHTML = ''
+ this.getEditor().getUI().$plugPanelWrap.style.display = 'none'
+ this.getEditor().getUI().$plugBtnWrap.innerHTML = ''
// load the plug UI
- this.plugs.forEach((plug) => this.loadPluginItem(plug))
+ this.plugins.forEach((plug) => this.loadPluginItem(plug))
}
/** Load the plug btn and plug panel on editor ui */
- private loadPluginItem(plug: EditorPlug) {
+ private loadPluginItem(plug: EditorPlugin) {
const $btn = plug.$btn
if (!$btn) return
- this.editor.getUI().$plugBtnWrap.appendChild($btn)
+ this.getEditor().getUI().$plugBtnWrap.appendChild($btn)
// bind the event when click plug btn
!$btn.onclick &&
($btn.onclick = () => {
// removing the active class from all the buttons
- this.editor
+ this.opts
+ .getEditor()
.getUI()
.$plugBtnWrap.querySelectorAll('.active')
.forEach((item) => item.classList.remove('active'))
@@ -102,13 +148,13 @@ export class PlugManager {
// if the plug is not the same as the openedPlug,
if (plug !== this.openedPlug) {
// then open the plug current clicked plug panel
- this.openPlugPanel(plug)
+ this.openPluginPanel(plug)
// add active class for current plug panel
$btn.classList.add('active')
} else {
// then close the plug
- this.closePlugPanel()
+ this.closePluginPanel()
}
})
@@ -116,17 +162,17 @@ export class PlugManager {
const $panel = plug.$panel
if ($panel) {
$panel.style.display = 'none'
- this.editor.getUI().$plugPanelWrap.appendChild($panel)
+ this.getEditor().getUI().$plugPanelWrap.appendChild($panel)
}
}
- get(plug: T) {
- return this.plugs.find((p) => p instanceof plug) as InstanceType | undefined
+ get(plug: T) {
+ return this.plugins.find((p) => p instanceof plug) as InstanceType | undefined
}
/** Open the editor plug panel */
- openPlugPanel(plug: EditorPlug) {
- this.plugs.forEach((aPlug) => {
+ openPluginPanel(plug: EditorPlugin) {
+ this.plugins.forEach((aPlug) => {
const plugPanel = aPlug.$panel
if (!plugPanel) return
@@ -139,15 +185,15 @@ export class PlugManager {
}
})
- this.editor.getUI().$plugPanelWrap.style.display = ''
+ this.getEditor().getUI().$plugPanelWrap.style.display = ''
this.openedPlug = plug
}
- /** Close the editor plug panel */
- closePlugPanel() {
+ /** Close the editor plugin panel */
+ closePluginPanel() {
if (!this.openedPlug) return
- this.editor.getUI().$plugPanelWrap.style.display = 'none'
+ this.getEditor().getUI().$plugPanelWrap.style.display = 'none'
this.events.trigger('panel-hide', this.openedPlug)
this.openedPlug = null
}
@@ -155,7 +201,7 @@ export class PlugManager {
/** Get the content which is transformed by plugs */
getTransformedContent(rawContent: string) {
let result = rawContent
- this.plugs.forEach((aPlug) => {
+ this.plugins.forEach((aPlug) => {
if (!aPlug.contentTransformer) return
result = aPlug.contentTransformer(result)
})
diff --git a/ui/artalk/src/plugins/editor/_kit.ts b/ui/artalk/src/plugins/editor/_kit.ts
index e2f01718..f22f42f1 100644
--- a/ui/artalk/src/plugins/editor/_kit.ts
+++ b/ui/artalk/src/plugins/editor/_kit.ts
@@ -1,49 +1,50 @@
-import type { PlugManager } from '../editor-kit'
-import type EditorPlug from './_plug'
+import type { PluginManager } from '../editor-kit'
+import type EditorPlugin from './_plug'
/**
* PlugKit provides a set of methods to help you develop editor plug
*/
export default class PlugKit {
- constructor(private plugs: PlugManager) {}
+ constructor(private plugins: PluginManager) {}
/** Use the editor */
useEditor() {
- return this.plugs.editor
- }
-
- /**
- * Use the context of global
- *
- * @deprecated The calls to this function should be reduced as much as possible
- */
- useGlobalCtx() {
- return this.plugs.editor.ctx
+ return this.plugins.getEditor()
}
/** Use the config of Artalk */
useConf() {
- return this.plugs.editor.ctx.conf
+ return this.plugins.getOptions().getConf().get()
}
/** Use the http api client */
useApi() {
- return this.plugs.editor.ctx.getApi()
+ return this.plugins.getOptions().getApi()
+ }
+
+ /** Use the data manager */
+ useData() {
+ return this.plugins.getOptions().getData()
}
/** Use the user manager */
useUser() {
- return this.plugs.editor.ctx.get('user')
+ return this.plugins.getOptions().getUser()
+ }
+
+ /** Use the checkers */
+ useCheckers() {
+ return this.plugins.getOptions().getCheckers()
}
/** Use the ui of editor */
useUI() {
- return this.plugs.editor.getUI()
+ return this.plugins.getEditor().getUI()
}
/** Use the events in editor scope */
useEvents() {
- return this.plugs.getEvents()
+ return this.plugins.getEvents()
}
/** Listen the event when plug is mounted */
@@ -57,7 +58,12 @@ export default class PlugKit {
}
/** Use the deps of other plug */
- useDeps(plug: T) {
- return this.plugs.get(plug)
+ useDeps(plug: T) {
+ return this.plugins.get(plug)
+ }
+
+ /** Use the root element of artalk */
+ useArtalkRootEl() {
+ return this.plugins.getOptions().getArtalkRootEl()
}
}
diff --git a/ui/artalk/src/plugins/editor/_plug.ts b/ui/artalk/src/plugins/editor/_plug.ts
index b2768031..a2c49c86 100644
--- a/ui/artalk/src/plugins/editor/_plug.ts
+++ b/ui/artalk/src/plugins/editor/_plug.ts
@@ -5,7 +5,7 @@ import * as Utils from '@/lib/utils'
/**
* Editor 插件
*/
-class EditorPlug {
+export class EditorPlugin {
$btn?: HTMLElement
$panel?: HTMLElement
contentTransformer?(rawContent: string): string
@@ -52,4 +52,4 @@ class EditorPlug {
}
}
-export default EditorPlug
+export default EditorPlugin
diff --git a/ui/artalk/src/plugins/editor/_sample.ts b/ui/artalk/src/plugins/editor/_sample.ts
index 0f6683e3..5229ea0e 100644
--- a/ui/artalk/src/plugins/editor/_sample.ts
+++ b/ui/artalk/src/plugins/editor/_sample.ts
@@ -1,7 +1,7 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
-export default class SamplePlug extends EditorPlug {
+export default class SamplePlug extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
}
diff --git a/ui/artalk/src/plugins/editor/closable.ts b/ui/artalk/src/plugins/editor/closable.ts
index 58ac8959..cdb96b1a 100644
--- a/ui/artalk/src/plugins/editor/closable.ts
+++ b/ui/artalk/src/plugins/editor/closable.ts
@@ -1,9 +1,9 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import * as Utils from '@/lib/utils'
import $t from '@/i18n'
-export default class Closable extends EditorPlug {
+export default class Closable extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
diff --git a/ui/artalk/src/plugins/editor/emoticons.ts b/ui/artalk/src/plugins/editor/emoticons.ts
index 942cf5e3..92c070aa 100644
--- a/ui/artalk/src/plugins/editor/emoticons.ts
+++ b/ui/artalk/src/plugins/editor/emoticons.ts
@@ -1,6 +1,6 @@
import './emoticons.scss'
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import type { EmoticonListData, EmoticonGrpData } from '@/types'
import * as Utils from '@/lib/utils'
@@ -14,7 +14,7 @@ type OwOFormatType = {
}
}
-export default class Emoticons extends EditorPlug {
+export default class Emoticons extends EditorPlugin {
private emoticons: EmoticonListData = []
private loadingTask: Promise | null = null
diff --git a/ui/artalk/src/plugins/editor/header-event.ts b/ui/artalk/src/plugins/editor/header-event.ts
index 8a512538..ab93dac5 100644
--- a/ui/artalk/src/plugins/editor/header-event.ts
+++ b/ui/artalk/src/plugins/editor/header-event.ts
@@ -1,7 +1,7 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
-export default class HeaderEvent extends EditorPlug {
+export default class HeaderEvent extends EditorPlugin {
private get $inputs() {
return this.kit.useEditor().getHeaderInputEls()
}
diff --git a/ui/artalk/src/plugins/editor/header-link.ts b/ui/artalk/src/plugins/editor/header-link.ts
index 7cfbb66c..25e1f2b2 100644
--- a/ui/artalk/src/plugins/editor/header-link.ts
+++ b/ui/artalk/src/plugins/editor/header-link.ts
@@ -1,7 +1,7 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
-export default class HeaderLink extends EditorPlug {
+export default class HeaderLink extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
diff --git a/ui/artalk/src/plugins/editor/header-user.ts b/ui/artalk/src/plugins/editor/header-user.ts
index 1eb5113c..79d33d19 100644
--- a/ui/artalk/src/plugins/editor/header-user.ts
+++ b/ui/artalk/src/plugins/editor/header-user.ts
@@ -1,9 +1,9 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import $t from '@/i18n'
import type { UserInfoApiResponseData } from '@/types'
-export default class HeaderUser extends EditorPlug {
+export default class HeaderUser extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
@@ -87,12 +87,12 @@ export default class HeaderUser extends EditorPlug {
if (!data.is_login) this.kit.useUser().logout()
// Update unread notifies
- this.kit.useGlobalCtx().getData().updateNotifies(data.notifies)
+ this.kit.useData().updateNotifies(data.notifies)
// If user is admin and not login,
if (this.kit.useUser().checkHasBasicUserInfo() && !data.is_login && data.user?.is_admin) {
// then show login window
- this.kit.useGlobalCtx().checkAdmin({
+ this.kit.useCheckers().checkAdmin({
onSuccess: () => {},
})
}
diff --git a/ui/artalk/src/plugins/editor/index.ts b/ui/artalk/src/plugins/editor/index.ts
index 23baf841..c882c392 100644
--- a/ui/artalk/src/plugins/editor/index.ts
+++ b/ui/artalk/src/plugins/editor/index.ts
@@ -1,4 +1,4 @@
-import type EditorPlug from './_plug'
+import type EditorPlugin from './_plug'
import LocalStorage from './local-storage'
import Textarea from './textarea'
import SubmitBtn from './submit-btn'
@@ -13,10 +13,10 @@ import Mover from './mover'
import Emoticons from './emoticons'
import Upload from './upload'
import Preview from './preview'
-import type { ArtalkConfig } from '@/types'
+import type { Config } from '@/types'
/** The default enabled plugs */
-const EDITOR_PLUGS: (typeof EditorPlug)[] = [
+const EDITOR_PLUGS: (typeof EditorPlugin)[] = [
// Core
LocalStorage,
HeaderEvent,
@@ -40,11 +40,11 @@ const EDITOR_PLUGS: (typeof EditorPlug)[] = [
* Get the enabled plugs by config
*/
export function getEnabledPlugs(
- conf: Pick,
-): (typeof EditorPlug)[] {
+ conf: Pick,
+): (typeof EditorPlugin)[] {
// The reference map of config and plugs
// (for check if the plug is enabled)
- const confRefs = new Map()
+ const confRefs = new Map()
confRefs.set(Upload, conf.imgUpload)
confRefs.set(Emoticons, conf.emoticons)
confRefs.set(Preview, conf.preview)
diff --git a/ui/artalk/src/plugins/editor/local-storage.ts b/ui/artalk/src/plugins/editor/local-storage.ts
index d76132cf..7087008a 100644
--- a/ui/artalk/src/plugins/editor/local-storage.ts
+++ b/ui/artalk/src/plugins/editor/local-storage.ts
@@ -1,10 +1,10 @@
import type PlugKit from './_kit'
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import $t from '@/i18n'
const LocalStorageKey = 'ArtalkContent'
-export default class LocalStorage extends EditorPlug {
+export default class LocalStorage extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
diff --git a/ui/artalk/src/plugins/editor/mover.ts b/ui/artalk/src/plugins/editor/mover.ts
index ca3cb1d2..87502ab8 100644
--- a/ui/artalk/src/plugins/editor/mover.ts
+++ b/ui/artalk/src/plugins/editor/mover.ts
@@ -1,7 +1,7 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import * as Utils from '@/lib/utils'
-export default class Mover extends EditorPlug {
+export default class Mover extends EditorPlugin {
private isMoved = false
move(afterEl: HTMLElement) {
@@ -24,8 +24,8 @@ export default class Mover extends EditorPlug {
if (!this.isMoved) return
this.isMoved = false
this.kit
- .useGlobalCtx()
- .$root.querySelector('.atk-editor-travel-placeholder')
+ .useArtalkRootEl()
+ .querySelector('.atk-editor-travel-placeholder')
?.replaceWith(this.kit.useUI().$el)
this.kit.useUI().$el.classList.remove('editor-traveling')
}
diff --git a/ui/artalk/src/plugins/editor/preview.ts b/ui/artalk/src/plugins/editor/preview.ts
index 6efc4d15..82eeb336 100644
--- a/ui/artalk/src/plugins/editor/preview.ts
+++ b/ui/artalk/src/plugins/editor/preview.ts
@@ -1,11 +1,10 @@
import './preview.scss'
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
-import * as marked from '@/lib/marked'
import $t from '@/i18n'
-export default class Preview extends EditorPlug {
+export default class Preview extends EditorPlugin {
private isPlugPanelShow = false
constructor(kit: PlugKit) {
diff --git a/ui/artalk/src/plugins/editor/state-edit.ts b/ui/artalk/src/plugins/editor/state-edit.ts
index 834d69d7..72267434 100644
--- a/ui/artalk/src/plugins/editor/state-edit.ts
+++ b/ui/artalk/src/plugins/editor/state-edit.ts
@@ -1,11 +1,11 @@
import type PlugKit from './_kit'
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import Submit from './submit'
import type { CommentData } from '@/types'
import $t from '@/i18n'
import * as Utils from '@/lib/utils'
-export default class StateEdit extends EditorPlug {
+export default class StateEdit extends EditorPlugin {
private comment?: CommentData
constructor(kit: PlugKit) {
@@ -42,7 +42,7 @@ export default class StateEdit extends EditorPlug {
return nComment.data
},
post: (nComment: CommentData) => {
- this.kit.useGlobalCtx().getData().updateComment(nComment)
+ this.kit.useData().updateComment(nComment)
},
})
})
diff --git a/ui/artalk/src/plugins/editor/state-reply.ts b/ui/artalk/src/plugins/editor/state-reply.ts
index 3c3c3f2c..9b202866 100644
--- a/ui/artalk/src/plugins/editor/state-reply.ts
+++ b/ui/artalk/src/plugins/editor/state-reply.ts
@@ -1,13 +1,12 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import Submit from './submit'
import SubmitAddPreset from './submit-add'
import type { CommentData } from '@/types'
import * as Utils from '@/lib/utils'
-import * as Ui from '@/lib/ui'
import $t from '@/i18n'
-export default class StateReply extends EditorPlug {
+export default class StateReply extends EditorPlugin {
private comment?: CommentData
constructor(kit: PlugKit) {
diff --git a/ui/artalk/src/plugins/editor/submit-add.ts b/ui/artalk/src/plugins/editor/submit-add.ts
index 62ca0d62..58d19325 100644
--- a/ui/artalk/src/plugins/editor/submit-add.ts
+++ b/ui/artalk/src/plugins/editor/submit-add.ts
@@ -33,6 +33,6 @@ export default class SubmitAddPreset {
postSubmitAdd(commentNew: CommentData) {
// insert the new comment to list
- this.kit.useGlobalCtx().getData().insertComment(commentNew)
+ this.kit.useData().insertComment(commentNew)
}
}
diff --git a/ui/artalk/src/plugins/editor/submit-btn.ts b/ui/artalk/src/plugins/editor/submit-btn.ts
index 139e5f5e..06ff2e1b 100644
--- a/ui/artalk/src/plugins/editor/submit-btn.ts
+++ b/ui/artalk/src/plugins/editor/submit-btn.ts
@@ -1,8 +1,8 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import $t from '@/i18n'
-export default class SubmitBtn extends EditorPlug {
+export default class SubmitBtn extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
diff --git a/ui/artalk/src/plugins/editor/submit.ts b/ui/artalk/src/plugins/editor/submit.ts
index 5a9ee966..e4920818 100644
--- a/ui/artalk/src/plugins/editor/submit.ts
+++ b/ui/artalk/src/plugins/editor/submit.ts
@@ -1,4 +1,4 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import SubmitAddPreset from './submit-add'
import $t from '@/i18n'
@@ -12,7 +12,7 @@ interface CustomSubmit {
post?: (nComment: CommentData) => void
}
-export default class Submit extends EditorPlug {
+export default class Submit extends EditorPlugin {
private customs: CustomSubmit[] = []
private defaultPreset: SubmitAddPreset
@@ -25,10 +25,10 @@ export default class Submit extends EditorPlug {
this.kit.useMounted(() => {
// invoke `do()` when event `editor-submit` is triggered
- this.kit.useGlobalCtx().on('editor-submit', onEditorSubmit)
+ this.kit.useEvents().on('editor-submit', onEditorSubmit)
})
this.kit.useUnmounted(() => {
- this.kit.useGlobalCtx().off('editor-submit', onEditorSubmit)
+ this.kit.useEvents().off('editor-submit', onEditorSubmit)
})
}
@@ -69,6 +69,6 @@ export default class Submit extends EditorPlug {
}
this.kit.useEditor().reset() // 复原编辑器
- this.kit.useGlobalCtx().trigger('editor-submitted')
+ this.kit.useEvents().trigger('editor-submitted')
}
}
diff --git a/ui/artalk/src/plugins/editor/textarea.ts b/ui/artalk/src/plugins/editor/textarea.ts
index cdaf1f4e..6deaf895 100644
--- a/ui/artalk/src/plugins/editor/textarea.ts
+++ b/ui/artalk/src/plugins/editor/textarea.ts
@@ -1,8 +1,8 @@
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import type PlugKit from './_kit'
import $t from '@/i18n'
-export default class Textarea extends EditorPlug {
+export default class Textarea extends EditorPlugin {
constructor(kit: PlugKit) {
super(kit)
diff --git a/ui/artalk/src/plugins/editor/upload.ts b/ui/artalk/src/plugins/editor/upload.ts
index 93225e27..62c00d12 100644
--- a/ui/artalk/src/plugins/editor/upload.ts
+++ b/ui/artalk/src/plugins/editor/upload.ts
@@ -1,12 +1,12 @@
import type PlugKit from './_kit'
-import EditorPlug from './_plug'
+import EditorPlugin from './_plug'
import * as Utils from '@/lib/utils'
import $t from '@/i18n'
/** 允许的图片格式 */
const AllowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
-export default class Upload extends EditorPlug {
+export default class Upload extends EditorPlugin {
private $imgUploadInput?: HTMLInputElement
constructor(kit: PlugKit) {
diff --git a/ui/artalk/src/plugins/index.ts b/ui/artalk/src/plugins/index.ts
index b930c67c..cb310947 100644
--- a/ui/artalk/src/plugins/index.ts
+++ b/ui/artalk/src/plugins/index.ts
@@ -7,9 +7,11 @@ import { VersionCheck } from './version-check'
import { AdminOnlyElem } from './admin-only-elem'
import { DarkMode } from './dark-mode'
import { PageVoteWidget } from './page-vote'
+import { Services } from '@/services'
import type { ArtalkPlugin } from '@/types'
export const DefaultPlugins: ArtalkPlugin[] = [
+ ...Services,
Markdown,
EditorKit,
AdminOnlyElem,
diff --git a/ui/artalk/src/plugins/list/copyright.ts b/ui/artalk/src/plugins/list/copyright.ts
index a98af869..350276a7 100644
--- a/ui/artalk/src/plugins/list/copyright.ts
+++ b/ui/artalk/src/plugins/list/copyright.ts
@@ -2,10 +2,10 @@ import type { ArtalkPlugin } from '@/types'
import { version as ARTALK_VERSION } from '~/package.json'
export const Copyright: ArtalkPlugin = (ctx) => {
- ctx.on('mounted', () => {
- const list = ctx.get('list')
+ const list = ctx.inject('list')
- const $copyright = list.$el.querySelector('.atk-copyright')
+ ctx.on('mounted', () => {
+ const $copyright = list.getEl().querySelector('.atk-copyright')
if (!$copyright) return
$copyright.innerHTML =
diff --git a/ui/artalk/src/plugins/list/count.ts b/ui/artalk/src/plugins/list/count.ts
index cf7ed045..3699e60c 100644
--- a/ui/artalk/src/plugins/list/count.ts
+++ b/ui/artalk/src/plugins/list/count.ts
@@ -3,10 +3,10 @@ import * as Utils from '@/lib/utils'
import $t from '@/i18n'
export const Count: ArtalkPlugin = (ctx) => {
- const refreshCountNumEl = () => {
- const list = ctx.get('list')
+ const list = ctx.inject('list')
- const $count = list.$el.querySelector('.atk-comment-count .atk-text')
+ const refreshCountNumEl = () => {
+ const $count = list.getEl().querySelector('.atk-comment-count .atk-text')
if (!$count) return
const text = Utils.htmlEncode(
diff --git a/ui/artalk/src/plugins/list/dropdown.ts b/ui/artalk/src/plugins/list/dropdown.ts
index 2daf09b8..82ba7fe8 100644
--- a/ui/artalk/src/plugins/list/dropdown.ts
+++ b/ui/artalk/src/plugins/list/dropdown.ts
@@ -3,8 +3,13 @@ import * as Utils from '@/lib/utils'
import $t from '@/i18n'
export const Dropdown: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
+ const conf = ctx.inject('config')
+
const reloadUseParamsEditor = (func: (p: any) => void) => {
- ctx.conf.listFetchParamsModifier = func
+ conf.update({
+ listFetchParamsModifier: func,
+ })
ctx.reload()
}
@@ -49,9 +54,7 @@ export const Dropdown: ArtalkPlugin = (ctx) => {
}
ctx.watchConf(['listSort', 'locale'], (conf) => {
- const list = ctx.get('list')
-
- const $count = list.$el.querySelector('.atk-comment-count')
+ const $count = list.getEl().querySelector('.atk-comment-count')
if (!$count) return
// 评论列表排序 Dropdown 下拉选择层
diff --git a/ui/artalk/src/plugins/list/error-dialog.ts b/ui/artalk/src/plugins/list/error-dialog.ts
index 450ecbfd..7e0fc224 100644
--- a/ui/artalk/src/plugins/list/error-dialog.ts
+++ b/ui/artalk/src/plugins/list/error-dialog.ts
@@ -1,18 +1,18 @@
import * as Ui from '../../lib/ui'
-import type { ArtalkPlugin, ContextApi } from '@/types'
+import type { ArtalkPlugin } from '@/types'
import { showErrorDialog } from '@/components/error-dialog'
export const ErrorDialog: ArtalkPlugin = (ctx) => {
- ctx.on('list-fetch', () => {
- const list = ctx.get('list')
+ const list = ctx.inject('list')
+ ctx.on('list-fetch', () => {
// clear the original error when a new fetch is triggered
- Ui.setError(list.$el, null)
+ Ui.setError(list.getEl(), null)
})
ctx.on('list-failed', (err) => {
showErrorDialog({
- $err: ctx.get('list').$el,
+ $err: list.getEl(),
errMsg: err.msg,
errData: err.data,
retryFn: () => ctx.fetch({ offset: 0 }),
diff --git a/ui/artalk/src/plugins/list/fetch.ts b/ui/artalk/src/plugins/list/fetch.ts
index f81701c2..7c279e42 100644
--- a/ui/artalk/src/plugins/list/fetch.ts
+++ b/ui/artalk/src/plugins/list/fetch.ts
@@ -1,6 +1,8 @@
import type { ListFetchParams, ArtalkPlugin } from '@/types'
export const Fetch: ArtalkPlugin = (ctx) => {
+ const conf = ctx.inject('config')
+
ctx.on('list-fetch', (_params) => {
if (ctx.getData().getLoading()) return
ctx.getData().setLoading(true)
@@ -8,9 +10,9 @@ export const Fetch: ArtalkPlugin = (ctx) => {
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
- paramsModifier: ctx.conf.listFetchParamsModifier,
+ limit: conf.get().pagination.pageSize,
+ flatMode: conf.get().flatMode as boolean, // always be boolean because had been handled in Artalk.init
+ paramsModifier: conf.get().listFetchParamsModifier,
..._params,
}
@@ -24,8 +26,8 @@ export const Fetch: ArtalkPlugin = (ctx) => {
limit: params.limit,
offset: params.offset,
flat_mode: params.flatMode,
- page_key: ctx.getConf().pageKey,
- site_name: ctx.getConf().site,
+ page_key: conf.get().pageKey,
+ site_name: conf.get().site,
}
// call the modifier function
diff --git a/ui/artalk/src/plugins/list/goto-first.ts b/ui/artalk/src/plugins/list/goto-first.ts
index e8410728..82689498 100644
--- a/ui/artalk/src/plugins/list/goto-first.ts
+++ b/ui/artalk/src/plugins/list/goto-first.ts
@@ -3,12 +3,13 @@ import * as Utils from '@/lib/utils'
/** List scroll to the first comment */
export const GotoFirst: ArtalkPlugin = (ctx) => {
- const handler = () => {
- const list = ctx.get('list')
+ const list = ctx.inject('list')
+ const conf = ctx.inject('config')
- const $relative = ctx.conf.scrollRelativeTo && ctx.conf.scrollRelativeTo()
+ const handler = () => {
+ const $relative = conf.get().scrollRelativeTo?.()
;($relative || window).scroll({
- top: Utils.getOffset(list.$el, $relative).top,
+ top: Utils.getOffset(list.getEl(), $relative).top,
left: 0,
})
}
diff --git a/ui/artalk/src/plugins/list/goto-focus.ts b/ui/artalk/src/plugins/list/goto-focus.ts
index 31ecf850..91eb234e 100644
--- a/ui/artalk/src/plugins/list/goto-focus.ts
+++ b/ui/artalk/src/plugins/list/goto-focus.ts
@@ -1,16 +1,15 @@
import type { ArtalkPlugin } from '@/types'
export const GotoFocus: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
+
ctx.on('list-goto', async (commentID) => {
// find the comment node
let comment = ctx.getCommentNodes().find((c) => c.getID() === commentID)
if (!comment) {
// fetch and insert the comment from the server
const data = (await ctx.getApi().comments.getComment(commentID)).data
- ctx
- .get('list')
- .getListLayout({ forceFlatMode: true })
- .insert(data.comment, data.reply_comment)
+ list.getLayout({ forceFlatMode: true }).insert(data.comment, data.reply_comment)
comment = ctx.getCommentNodes().find((c) => c.getID() === commentID)
}
if (!comment) return
diff --git a/ui/artalk/src/plugins/list/loading.ts b/ui/artalk/src/plugins/list/loading.ts
index 311114f4..ca3e1ee7 100644
--- a/ui/artalk/src/plugins/list/loading.ts
+++ b/ui/artalk/src/plugins/list/loading.ts
@@ -2,17 +2,16 @@ import type { ArtalkPlugin } from '@/types'
import * as Ui from '@/lib/ui'
export const Loading: ArtalkPlugin = (ctx) => {
- ctx.on('list-fetch', (p) => {
- const list = ctx.get('list')
+ const list = ctx.inject('list')
+ ctx.on('list-fetch', (p) => {
if (p.offset === 0)
// only show loading when fetch first page
- Ui.setLoading(true, list.$el)
+ Ui.setLoading(true, list.getEl())
// else if not first page, show loading in paginator (code not there)
})
ctx.on('list-fetched', () => {
- const list = ctx.get('list')
- Ui.setLoading(false, list.$el)
+ Ui.setLoading(false, list.getEl())
})
}
diff --git a/ui/artalk/src/plugins/list/no-comment.ts b/ui/artalk/src/plugins/list/no-comment.ts
index 34253b58..c0415c1b 100644
--- a/ui/artalk/src/plugins/list/no-comment.ts
+++ b/ui/artalk/src/plugins/list/no-comment.ts
@@ -3,9 +3,10 @@ import * as Utils from '@/lib/utils'
import { sanitize } from '@/lib/sanitizer'
export const NoComment: ArtalkPlugin = (ctx) => {
- ctx.on('list-loaded', (comments) => {
- const list = ctx.get('list')!
+ const list = ctx.inject('list')
+ const conf = ctx.inject('config')
+ ctx.on('list-loaded', (comments) => {
// 无评论
const isNoComment = comments.length <= 0
let $noComment = list.getCommentsWrapEl().querySelector('.atk-list-no-comment')
@@ -15,7 +16,7 @@ export const NoComment: ArtalkPlugin = (ctx) => {
$noComment = Utils.createElement('')
// sanitize before set innerHTML
- $noComment.innerHTML = sanitize(list.ctx.conf.noComment || list.ctx.$t('noComment'))
+ $noComment.innerHTML = sanitize(conf.get().noComment || ctx.$t('noComment'))
list.getCommentsWrapEl().appendChild($noComment)
}
} else {
diff --git a/ui/artalk/src/plugins/list/reach-bottom.ts b/ui/artalk/src/plugins/list/reach-bottom.ts
index d7f66f03..fcee7533 100644
--- a/ui/artalk/src/plugins/list/reach-bottom.ts
+++ b/ui/artalk/src/plugins/list/reach-bottom.ts
@@ -1,10 +1,12 @@
import type { ArtalkPlugin } from '@/types'
export const ReachBottom: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
+ const conf = ctx.inject('config')
let observer: IntersectionObserver | null = null
const setupObserver = ($target: HTMLElement) => {
- const scrollEvtAt = (ctx.conf.scrollRelativeTo && ctx.conf.scrollRelativeTo()) || null
+ const scrollEvtAt = conf.get().scrollRelativeTo?.() || null
observer = new IntersectionObserver(
([entries]) => {
@@ -35,8 +37,6 @@ export const ReachBottom: ArtalkPlugin = (ctx) => {
ctx.on('list-loaded', () => {
clearObserver()
- const list = ctx.get('list')
-
// get the second last child
const children = list.getCommentsWrapEl().childNodes
const $target = children.length > 2 ? (children[children.length - 2] as HTMLElement) : null
diff --git a/ui/artalk/src/plugins/list/sidebar-btn.ts b/ui/artalk/src/plugins/list/sidebar-btn.ts
index 2f9c82e5..a1c1f0d3 100644
--- a/ui/artalk/src/plugins/list/sidebar-btn.ts
+++ b/ui/artalk/src/plugins/list/sidebar-btn.ts
@@ -2,11 +2,12 @@ import type { ArtalkPlugin } from '@/types'
import $t from '@/i18n'
export const SidebarBtn: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
let $openSidebarBtn: HTMLElement | null = null
const syncByUser = () => {
if (!$openSidebarBtn) return
- const user = ctx.get('user').getData()
+ const user = ctx.inject('user').getData()
// 已输入个人信息
if (!!user.name && !!user.email) {
@@ -21,9 +22,7 @@ export const SidebarBtn: ArtalkPlugin = (ctx) => {
}
ctx.watchConf(['locale'], (conf) => {
- const list = ctx.get('list')
-
- $openSidebarBtn = list.$el.querySelector('[data-action="open-sidebar"]')
+ $openSidebarBtn = list.getEl().querySelector('[data-action="open-sidebar"]')
if (!$openSidebarBtn) return
$openSidebarBtn.onclick = () => {
diff --git a/ui/artalk/src/plugins/list/time-ticking.ts b/ui/artalk/src/plugins/list/time-ticking.ts
index e927568f..2c8ceb20 100644
--- a/ui/artalk/src/plugins/list/time-ticking.ts
+++ b/ui/artalk/src/plugins/list/time-ticking.ts
@@ -3,16 +3,19 @@ import * as Utils from '@/lib/utils'
/** 评论时间自动更新 */
export const TimeTicking: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
+ const conf = ctx.inject('config')
let timer: number | null = null
ctx.on('mounted', () => {
timer = window.setInterval(() => {
- const list = ctx.get('list')
-
- list.$el.querySelectorAll('[data-atk-comment-date]').forEach((el) => {
- const date = new Date(Number(el.getAttribute('data-atk-comment-date')))
- el.innerText = ctx.getConf().dateFormatter?.(date) || Utils.timeAgo(date, ctx.$t)
- })
+ list
+ .getEl()
+ .querySelectorAll('[data-atk-comment-date]')
+ .forEach((el) => {
+ const date = new Date(Number(el.getAttribute('data-atk-comment-date')))
+ el.innerText = conf.get().dateFormatter?.(date) || Utils.timeAgo(date, ctx.$t)
+ })
}, 30 * 1000) // 30s 更新一次
})
diff --git a/ui/artalk/src/plugins/list/unread-badge.ts b/ui/artalk/src/plugins/list/unread-badge.ts
index b79edf78..1d75f86a 100644
--- a/ui/artalk/src/plugins/list/unread-badge.ts
+++ b/ui/artalk/src/plugins/list/unread-badge.ts
@@ -1,6 +1,7 @@
import type { ArtalkPlugin } from '@/types'
export const UnreadBadge: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
let $unreadBadge: HTMLElement | null = null
const showUnreadBadge = (count: number) => {
@@ -15,9 +16,7 @@ export const UnreadBadge: ArtalkPlugin = (ctx) => {
}
ctx.on('mounted', () => {
- const list = ctx.get('list')
-
- $unreadBadge = list.$el.querySelector('.atk-unread-badge')
+ $unreadBadge = list.getEl().querySelector('.atk-unread-badge')
})
ctx.on('notifies-updated', (notifies) => {
diff --git a/ui/artalk/src/plugins/list/unread.ts b/ui/artalk/src/plugins/list/unread.ts
index 5eb59692..4f9eb8c3 100644
--- a/ui/artalk/src/plugins/list/unread.ts
+++ b/ui/artalk/src/plugins/list/unread.ts
@@ -2,9 +2,11 @@ import type { ArtalkPlugin } from '@/types'
import * as Utils from '@/lib/utils'
export const Unread: ArtalkPlugin = (ctx) => {
+ const conf = ctx.inject('config')
+
ctx.on('comment-rendered', (comment) => {
// comment unread highlight
- if (ctx.conf.listUnreadHighlight === true) {
+ if (conf.get().listUnreadHighlight) {
const notifies = ctx.getData().getNotifies()
const notify = notifies.find((o) => o.comment_id === comment.getID())
diff --git a/ui/artalk/src/plugins/list/with-editor.ts b/ui/artalk/src/plugins/list/with-editor.ts
index bd2bb603..44a1a109 100644
--- a/ui/artalk/src/plugins/list/with-editor.ts
+++ b/ui/artalk/src/plugins/list/with-editor.ts
@@ -1,15 +1,17 @@
-import type { ContextApi, ArtalkPlugin, PageData } from '@/types'
+import type { Context as IContext, ArtalkPlugin, PageData } from '@/types'
import $t from '@/i18n'
export const WithEditor: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
+ const editorPlugs = ctx.inject('editorPlugs')
let $closeCommentBtn: HTMLElement | undefined
// on Artalk mounted
// (after all components had mounted)
ctx.on('mounted', () => {
- const list = ctx.get('list')
-
- $closeCommentBtn = list.$el.querySelector('[data-action="admin-close-comment"]')!
+ $closeCommentBtn = list
+ .getEl()
+ .querySelector('[data-action="admin-close-comment"]')!
// bind editor close button click event
$closeCommentBtn.addEventListener('click', () => {
@@ -23,16 +25,14 @@ export const WithEditor: ArtalkPlugin = (ctx) => {
// on comment list loaded (it will include page data update)
ctx.on('page-loaded', (page) => {
- const editor = ctx.get('editor')
-
// if page comment is closed
if (page?.admin_only === true) {
// then close editor
- editor.getPlugs()?.getEvents().trigger('editor-close')
+ editorPlugs?.getEvents().trigger('editor-close')
$closeCommentBtn && ($closeCommentBtn.innerText = $t('openComment'))
} else {
// the open editor
- editor.getPlugs()?.getEvents().trigger('editor-open')
+ editorPlugs?.getEvents().trigger('editor-open')
$closeCommentBtn && ($closeCommentBtn.innerText = $t('closeComment'))
}
})
@@ -44,7 +44,7 @@ export const WithEditor: ArtalkPlugin = (ctx) => {
}
/** 管理员设置页面信息 */
-function adminPageEditSave(ctx: ContextApi, page: PageData) {
+function adminPageEditSave(ctx: IContext, page: PageData) {
ctx.editorShowLoading()
ctx
.getApi()
diff --git a/ui/artalk/src/plugins/markdown.ts b/ui/artalk/src/plugins/markdown.ts
index ea10f5a4..4be4636c 100644
--- a/ui/artalk/src/plugins/markdown.ts
+++ b/ui/artalk/src/plugins/markdown.ts
@@ -4,8 +4,8 @@ import * as marked from '@/lib/marked'
export const Markdown: ArtalkPlugin = (ctx) => {
ctx.watchConf(['imgLazyLoad', 'markedOptions'], (conf) => {
marked.initMarked({
- markedOptions: ctx.getConf().markedOptions,
- imgLazyLoad: ctx.getConf().imgLazyLoad,
+ markedOptions: conf.markedOptions,
+ imgLazyLoad: conf.imgLazyLoad,
})
})
diff --git a/ui/artalk/src/plugins/mount-error.ts b/ui/artalk/src/plugins/mount-error.ts
new file mode 100644
index 00000000..3828dc19
--- /dev/null
+++ b/ui/artalk/src/plugins/mount-error.ts
@@ -0,0 +1,40 @@
+import { showErrorDialog } from '@/components/error-dialog'
+import type { ArtalkPlugin } from '@/types'
+
+export interface MountErrorOptions {
+ err?: { data?: { err_no_site?: boolean }; msg: string }
+ onRetry?: () => void
+}
+
+export const MountError: ArtalkPlugin = (ctx, opts = {}) => {
+ const list = ctx.inject('list')
+ const user = ctx.inject('user')
+ const conf = ctx.inject('config')
+
+ const err = opts.err
+ if (!err) throw new Error('MountError: `err` is required')
+
+ let sidebarOpenView = ''
+
+ // if response err_no_site, modify the sidebar open view to create site
+ if (err.data?.err_no_site) {
+ const viewLoadParam = {
+ create_name: conf.get().site,
+ create_urls: `${window.location.protocol}//${window.location.host}`,
+ }
+ sidebarOpenView = `sites|${JSON.stringify(viewLoadParam)}`
+ }
+
+ showErrorDialog({
+ $err: list.getEl(),
+ errMsg: err.msg || String(err),
+ errData: err.data,
+ retryFn: () => opts.onRetry?.(),
+ onOpenSidebar: user.getData().is_admin
+ ? () =>
+ ctx.showSidebar({
+ view: sidebarOpenView as any,
+ })
+ : undefined, // only show open sidebar button when user is admin
+ })
+}
diff --git a/ui/artalk/src/plugins/page-vote.ts b/ui/artalk/src/plugins/page-vote.ts
index 564df93d..8858f82c 100644
--- a/ui/artalk/src/plugins/page-vote.ts
+++ b/ui/artalk/src/plugins/page-vote.ts
@@ -40,6 +40,7 @@ interface PageVoteState {
}
export const PageVoteWidget: ArtalkPlugin = (ctx) => {
+ const conf = ctx.inject('config')
let state: PageVoteState = initState()
let cleanup: (() => void) | undefined
@@ -63,7 +64,7 @@ export const PageVoteWidget: ArtalkPlugin = (ctx) => {
// List fetched handler
let currPageId = 0
function listFetchedHandler({ data }: ListFetchedArgs) {
- if (!ctx.getConf().pageVote || !checkEls(state) || !data) return
+ if (!conf.get().pageVote || !checkEls(state) || !data) return
if (currPageId !== data.page.id) {
// Initialize vote status in a new page
diff --git a/ui/artalk/src/plugins/stat.ts b/ui/artalk/src/plugins/stat.ts
index 89318d38..07abb8db 100644
--- a/ui/artalk/src/plugins/stat.ts
+++ b/ui/artalk/src/plugins/stat.ts
@@ -1,4 +1,4 @@
-import type { ContextApi, ArtalkPlugin, ArtalkConfig } from '@/types'
+import type { Context, ArtalkPlugin } from '@/types'
import { Api } from '@/api'
type CountCache = { [pageKey: string]: number }
@@ -17,19 +17,22 @@ export interface CountOptions {
pvAdd?: boolean
}
-export const PvCountWidget: ArtalkPlugin = (ctx: ContextApi) => {
- ctx.watchConf(['site', 'pageKey', 'pageTitle', 'countEl', 'pvEl', 'statPageKeyAttr'], (conf) => {
- initCountWidget({
- getApi: () => ctx.getApi(),
- siteName: conf.site,
- pageKey: conf.pageKey,
- pageTitle: conf.pageTitle,
- countEl: conf.countEl,
- pvEl: conf.pvEl,
- pageKeyAttr: conf.statPageKeyAttr,
- pvAdd: typeof ctx.conf.pvAdd === 'boolean' ? ctx.conf.pvAdd : true,
- })
- })
+export const PvCountWidget: ArtalkPlugin = (ctx: Context) => {
+ ctx.watchConf(
+ ['site', 'pageKey', 'pageTitle', 'countEl', 'pvEl', 'statPageKeyAttr', 'pvAdd'],
+ (conf) => {
+ initCountWidget({
+ getApi: () => ctx.getApi(),
+ siteName: conf.site,
+ pageKey: conf.pageKey,
+ pageTitle: conf.pageTitle,
+ countEl: conf.countEl,
+ pvEl: conf.pvEl,
+ pageKeyAttr: conf.statPageKeyAttr,
+ pvAdd: conf.pvAdd,
+ })
+ },
+ )
}
/** Initialize count widgets */
diff --git a/ui/artalk/src/plugins/version-check.ts b/ui/artalk/src/plugins/version-check.ts
index 2f68ba00..9e53f297 100644
--- a/ui/artalk/src/plugins/version-check.ts
+++ b/ui/artalk/src/plugins/version-check.ts
@@ -1,15 +1,13 @@
-import type { ArtalkPlugin } from '@/types'
+import type { ArtalkPlugin, List } from '@/types'
import { version as ARTALK_VERSION } from '~/package.json'
-import type List from '~/src/list/list'
-import * as Ui from '@/lib/ui'
import * as Utils from '@/lib/utils'
import $t from '@/i18n'
let IgnoreVersionCheck = false
export const VersionCheck: ArtalkPlugin = (ctx) => {
+ const list = ctx.inject('list')
ctx.watchConf(['apiVersion', 'versionCheck'], (conf) => {
- const list = ctx.get('list')
if (conf.apiVersion && conf.versionCheck && !IgnoreVersionCheck)
versionCheck(list, ARTALK_VERSION, conf.apiVersion)
})
@@ -33,5 +31,5 @@ function versionCheck(list: List, feVer: string, beVer: string) {
IgnoreVersionCheck = true
}
errEl.append(ignoreBtn)
- list.$el.parentElement!.prepend(errEl)
+ list.getEl().parentElement!.prepend(errEl)
}
diff --git a/ui/artalk/src/service.ts b/ui/artalk/src/service.ts
deleted file mode 100644
index 1fa9a32e..00000000
--- a/ui/artalk/src/service.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import CheckerLauncher from './components/checker'
-import Editor from './editor/editor'
-import SidebarLayer from './layer/sidebar-layer'
-
-import List from './list/list'
-
-import * as I18n from './i18n'
-import { PlugManager } from './plugins/editor-kit'
-import { LayerManager } from './layer/layer-manager'
-import User from './lib/user'
-import type { ContextApi } from '@/types'
-
-/**
- * Services
- *
- * @description Call these services by `ctx.get('serviceName')` or `ctx.serviceName`
- */
-const services = {
- // I18n
- i18n(ctx: ContextApi) {
- I18n.setLocale(ctx.conf.locale)
-
- ctx.watchConf(['locale'], (conf) => {
- I18n.setLocale(conf.locale)
- })
- },
-
- // User Store
- user(ctx: ContextApi) {
- const user = new User({
- onUserChanged: (data) => {
- ctx.trigger('user-changed', data)
- },
- })
- return user
- },
-
- // 弹出层
- layerManager(ctx: ContextApi) {
- return new LayerManager(ctx)
- },
-
- // CheckerLauncher
- checkerLauncher(ctx: ContextApi) {
- const checkerLauncher = new CheckerLauncher({
- getCtx: () => ctx,
- getApi: () => ctx.getApi(),
- onReload: () => ctx.reload(),
-
- // make sure suffix with a slash, because it will be used as a base url when call `fetch`
- getCaptchaIframeURL: () => `${ctx.conf.server}/api/v2/captcha/?t=${+new Date()}`,
- })
- return checkerLauncher
- },
-
- // 编辑器
- editor(ctx: ContextApi) {
- const editor = new Editor(ctx)
- ctx.$root.appendChild(editor.$el)
- return editor
- },
-
- // 评论列表
- list(ctx: ContextApi): List {
- const list = new List(ctx)
- ctx.$root.appendChild(list.$el)
- return list
- },
-
- // 侧边栏 Layer
- sidebarLayer(ctx: ContextApi) {
- const sidebarLayer = new SidebarLayer(ctx)
- return sidebarLayer
- },
-
- // Extra Service
- // -----------------------------------------
- // Only for type check
- // Not inject to ctx immediately,
- // but can be injected by other occasions
-
- editorPlugs(): PlugManager | undefined {
- return undefined
- },
-}
-
-export default services
-
-// type tricks for dependency injection
-type TServiceImps = typeof services
-type TObjectWithFuncs = { [k: string]: (...args: any) => any }
-type TKeysOnlyReturn = {
- [K in keyof T]: ReturnType extends V ? K : never
-}[keyof T]
-type TOmitConditions = TKeysOnlyReturn
-type TServiceInjectors = Omit
-export type TInjectedServices = {
- [K in keyof TServiceInjectors]: ReturnType
-}
diff --git a/ui/artalk/src/services/api.ts b/ui/artalk/src/services/api.ts
new file mode 100644
index 00000000..c891f467
--- /dev/null
+++ b/ui/artalk/src/services/api.ts
@@ -0,0 +1,15 @@
+import type { ArtalkPlugin } from '@/types'
+import { Api, createApiHandlers } from '@/api'
+import { convertApiOptions } from '@/config'
+
+export const ApiService: ArtalkPlugin = (ctx) => {
+ ctx.provide('apiHandlers', () => createApiHandlers(), [] as const)
+ ctx.provide(
+ 'api',
+ (user, config, apiHandlers) => new Api(convertApiOptions(config.get(), user, apiHandlers)),
+ ['user', 'config', 'apiHandlers'] as const,
+ {
+ lifecycle: 'transient',
+ },
+ )
+}
diff --git a/ui/artalk/src/services/checkers.ts b/ui/artalk/src/services/checkers.ts
new file mode 100644
index 00000000..2e7b6978
--- /dev/null
+++ b/ui/artalk/src/services/checkers.ts
@@ -0,0 +1,25 @@
+import type { ArtalkPlugin } from '@/types'
+import { CheckerLauncher } from '@/components/checker'
+
+export const CheckersService: ArtalkPlugin = (ctx) => {
+ ctx.provide(
+ 'checkers',
+ (api, apiHandlers, layers, user, config) => {
+ const checkers = new CheckerLauncher({
+ getApi: () => api,
+ getLayers: () => layers,
+ getUser: () => user,
+ onReload: () => ctx.reload(),
+
+ // make sure suffix with a slash, because it will be used as a base url when call `fetch`
+ getCaptchaIframeURL: () => `${config.get().server}/api/v2/captcha/?t=${+new Date()}`,
+ })
+
+ apiHandlers.add('need_captcha', (res) => checkers.checkCaptcha(res))
+ apiHandlers.add('need_login', () => checkers.checkAdmin({}))
+
+ return checkers
+ },
+ ['api', 'apiHandlers', 'layers', 'user', 'config'] as const,
+ )
+}
diff --git a/ui/artalk/src/services/config.ts b/ui/artalk/src/services/config.ts
new file mode 100644
index 00000000..06da8392
--- /dev/null
+++ b/ui/artalk/src/services/config.ts
@@ -0,0 +1,33 @@
+import type { Config, ArtalkPlugin, ConfigManager } from '@/types'
+import { mergeDeep } from '@/lib/merge-deep'
+import { handelCustomConf } from '@/config'
+import { watchConf } from '@/lib/watch-conf'
+
+export const ConfigService: ArtalkPlugin = (ctx) => {
+ let conf: Config = handelCustomConf({}, true)
+
+ ctx.provide(
+ 'config',
+ (events) => {
+ let mounted = false
+ events.on('mounted', () => (mounted = true))
+ const instance: ConfigManager = {
+ watchConf: (keys, effect) => {
+ watchConf({
+ keys,
+ effect,
+ getConf: () => conf,
+ getEvents: () => events,
+ })
+ },
+ get: () => conf,
+ update: (config) => {
+ conf = mergeDeep(conf, handelCustomConf(config, false))
+ mounted && events.trigger('updated', conf)
+ },
+ }
+ return instance
+ },
+ ['events'] as const,
+ )
+}
diff --git a/ui/artalk/src/services/data.ts b/ui/artalk/src/services/data.ts
new file mode 100644
index 00000000..bde11fb5
--- /dev/null
+++ b/ui/artalk/src/services/data.ts
@@ -0,0 +1,13 @@
+import { ArtalkPlugin } from '@/types'
+import { DataManager } from '@/data'
+
+export const DataService: ArtalkPlugin = (ctx) => {
+ ctx.provide('data', (events) => new DataManager(events), ['events'] as const)
+
+ ctx.on('mounted', () => {
+ // Load comment list immediately after mounting
+ if (ctx.getConf().fetchCommentsOnInit) {
+ ctx.getData().fetchComments({ offset: 0 })
+ }
+ })
+}
diff --git a/ui/artalk/src/services/editor.ts b/ui/artalk/src/services/editor.ts
new file mode 100644
index 00000000..6273ee41
--- /dev/null
+++ b/ui/artalk/src/services/editor.ts
@@ -0,0 +1,17 @@
+import type { ArtalkPlugin } from '@/types'
+import { Editor } from '@/editor/editor'
+
+export const EditorService: ArtalkPlugin = (ctx) => {
+ ctx.provide(
+ 'editor',
+ (events, config) => {
+ const editor = new Editor({
+ getEvents: () => events,
+ getConf: () => config,
+ })
+ ctx.getEl().appendChild(editor.getEl())
+ return editor
+ },
+ ['events', 'config'] as const,
+ )
+}
diff --git a/ui/artalk/src/services/events.ts b/ui/artalk/src/services/events.ts
new file mode 100644
index 00000000..88606315
--- /dev/null
+++ b/ui/artalk/src/services/events.ts
@@ -0,0 +1,6 @@
+import type { ArtalkPlugin, EventPayloadMap } from '@/types'
+import { EventManager } from '@/lib/event-manager'
+
+export const EventsService: ArtalkPlugin = (ctx) => {
+ ctx.provide('events', () => new EventManager())
+}
diff --git a/ui/artalk/src/services/i18n.ts b/ui/artalk/src/services/i18n.ts
new file mode 100644
index 00000000..ada92b9c
--- /dev/null
+++ b/ui/artalk/src/services/i18n.ts
@@ -0,0 +1,8 @@
+import * as I18n from '@/i18n'
+import type { ArtalkPlugin } from '@/types'
+
+export const I18nService: ArtalkPlugin = (ctx) => {
+ ctx.watchConf(['locale'], (conf) => {
+ I18n.setLocale(conf.locale)
+ })
+}
diff --git a/ui/artalk/src/services/index.ts b/ui/artalk/src/services/index.ts
new file mode 100644
index 00000000..b097e3c6
--- /dev/null
+++ b/ui/artalk/src/services/index.ts
@@ -0,0 +1,22 @@
+import { ApiService } from './api'
+import { UserService } from './user'
+import { CheckersService } from './checkers'
+import { EditorService } from './editor'
+import { I18nService } from './i18n'
+import { DataService } from './data'
+import { LayersService } from './layer'
+import { ListService } from './list'
+import { SidebarService } from './sidebar'
+import { ArtalkPlugin } from '@/types'
+
+export const Services: Set = new Set([
+ DataService,
+ ApiService,
+ I18nService,
+ UserService,
+ EditorService,
+ ListService,
+ CheckersService,
+ LayersService,
+ SidebarService,
+])
diff --git a/ui/artalk/src/services/layer.ts b/ui/artalk/src/services/layer.ts
new file mode 100644
index 00000000..b06eee30
--- /dev/null
+++ b/ui/artalk/src/services/layer.ts
@@ -0,0 +1,13 @@
+import type { ArtalkPlugin } from '@/types'
+import { LayerManager } from '@/layer'
+
+export const LayersService: ArtalkPlugin = (ctx) => {
+ ctx.provide('layers', () => {
+ const layerManager = new LayerManager()
+ document.body.appendChild(layerManager.getEl())
+ ctx.on('unmounted', () => {
+ layerManager?.destroy()
+ })
+ return layerManager
+ })
+}
diff --git a/ui/artalk/src/services/list.ts b/ui/artalk/src/services/list.ts
new file mode 100644
index 00000000..23fd0a4e
--- /dev/null
+++ b/ui/artalk/src/services/list.ts
@@ -0,0 +1,24 @@
+import type { ArtalkPlugin } from '@/types'
+import { List } from '@/list/list'
+
+export const ListService: ArtalkPlugin = (ctx) => {
+ ctx.provide(
+ 'list',
+ (api, events, config, data, editor) => {
+ const list = new List({
+ getApi: () => api,
+ getEvents: () => events,
+ getConf: () => config,
+ getData: () => data,
+
+ replyComment: (c, $el) => editor.setReplyComment(c, $el),
+ editComment: (c, $el) => editor.setEditComment(c, $el),
+ resetEditorState: () => editor.resetState(),
+ onListGotoFirst: () => ctx.listGotoFirst(),
+ })
+ ctx.getEl().appendChild(list.getEl())
+ return list
+ },
+ ['api', 'events', 'config', 'data', 'editor'] as const,
+ )
+}
diff --git a/ui/artalk/src/services/sidebar.ts b/ui/artalk/src/services/sidebar.ts
new file mode 100644
index 00000000..074ae89f
--- /dev/null
+++ b/ui/artalk/src/services/sidebar.ts
@@ -0,0 +1,37 @@
+import type { ArtalkPlugin } from '@/types'
+import { SidebarLayer } from '@/layer/sidebar-layer'
+
+export const SidebarService: ArtalkPlugin = (ctx) => {
+ ctx.provide(
+ 'sidebar',
+ (events, data, editor, checkers, api, config, user, layers) => {
+ const sidebarLayer = new SidebarLayer({
+ onShow: () => {
+ setTimeout(() => {
+ data.updateNotifies([])
+ }, 0)
+
+ events.trigger('sidebar-show')
+ },
+ onHide: () => {
+ // prevent comment box from being swallowed
+ editor.resetState()
+
+ events.trigger('sidebar-hide')
+ },
+ getCheckers: () => checkers,
+ getApi: () => api,
+ getConf: () => config,
+ getUser: () => user,
+ getLayers: () => layers,
+ })
+
+ ctx.on('user-changed', () => {
+ sidebarLayer?.onUserChanged()
+ })
+
+ return sidebarLayer
+ },
+ ['events', 'data', 'editor', 'checkers', 'api', 'config', 'user', 'layers'] as const,
+ )
+}
diff --git a/ui/artalk/src/services/user.ts b/ui/artalk/src/services/user.ts
new file mode 100644
index 00000000..dfa1e367
--- /dev/null
+++ b/ui/artalk/src/services/user.ts
@@ -0,0 +1,16 @@
+import type { ArtalkPlugin } from '@/types'
+import { UserManager } from '@/lib/user'
+
+export const UserService: ArtalkPlugin = (ctx) => {
+ ctx.provide(
+ 'user',
+ () => {
+ return new UserManager({
+ onUserChanged: (data) => {
+ ctx.trigger('user-changed', data)
+ },
+ })
+ },
+ [] as const,
+ )
+}
diff --git a/ui/artalk/src/types/checker.ts b/ui/artalk/src/types/checker.ts
new file mode 100644
index 00000000..d25517f4
--- /dev/null
+++ b/ui/artalk/src/types/checker.ts
@@ -0,0 +1,12 @@
+import type {
+ Checker,
+ CheckerCaptchaPayload,
+ CheckerCtx,
+ CheckerPayload,
+} from '@/components/checker'
+
+export interface CheckerManager {
+ checkCaptcha: (payload: CheckerCaptchaPayload) => Promise
+ checkAdmin: (payload: CheckerPayload) => Promise
+ check: (checker: Checker, payload: CheckerPayload, beforeCheck?: (c: CheckerCtx) => void) => void
+}
diff --git a/ui/artalk/src/types/config.ts b/ui/artalk/src/types/config.ts
index 5ef158fe..d2cd3644 100644
--- a/ui/artalk/src/types/config.ts
+++ b/ui/artalk/src/types/config.ts
@@ -1,9 +1,10 @@
import type { MarkedOptions } from 'marked'
import type { CommentData } from './data'
-import type { EditorApi } from './editor'
+import type { Editor } from './editor'
+import type { Context } from './context'
import type { I18n } from '@/i18n'
-export interface ArtalkConfig {
+export interface Config {
/** Element selector or Element to mount the Artalk */
el: string | HTMLElement
@@ -133,7 +134,7 @@ export interface ArtalkConfig {
imgUploader?: (file: File) => Promise
/** Image lazy load mode */
- imgLazyLoad?: 'native' | 'data-src'
+ imgLazyLoad: false | 'native' | 'data-src'
/** Enable version check */
versionCheck: boolean
@@ -145,7 +146,7 @@ export interface ArtalkConfig {
locale: I18n | string
/** Backend API version (system data, not allowed for user modification) */
- apiVersion?: string
+ apiVersion: string
/** URLs for plugin scripts */
pluginURLs?: string[]
@@ -166,47 +167,33 @@ export interface ArtalkConfig {
*/
dateFormatter?: (date: Date) => string
- /** Custom configuration modifier for remote configuration (referenced by artalk-sidebar) */
- // TODO: Consider merging list-related configuration into a single object, or flatten everything for simplicity and consistency
- remoteConfModifier?: (conf: DeepPartial) => void
-
/** List unread highlight (enable by default in artalk-sidebar) */
- listUnreadHighlight?: boolean
+ listUnreadHighlight: boolean
/** The relative element for scrolling (useful if artalk is in a scrollable container) */
scrollRelativeTo?: () => HTMLElement
/** Page view increment when comment list is loaded */
- pvAdd?: boolean
+ pvAdd: boolean
+
+ /** Immediately fetch comments when Artalk instance is initialized */
+ fetchCommentsOnInit: boolean
/** Callback before submitting a comment */
- beforeSubmit?: (editor: EditorApi, next: () => void) => void
+ beforeSubmit?: (editor: Editor, next: () => void) => void
}
type DeepPartial = {
[K in keyof T]?: T[K] extends object ? DeepPartial : T[K]
}
-export type ArtalkConfigPartial = DeepPartial
-
-/**
- * Local User Data (in localStorage)
- *
- * @note Keep flat for easy handling
- */
-export interface LocalUser {
- /** Username (aka. Nickname) */
- name: string
-
- /** Email */
- email: string
-
- /** Link (aka. Website) */
- link: string
+export type ConfigPartial = DeepPartial
- /** Token (for authorization) */
- token: string
+// Alias for backward compatibility
+export type ArtalkConfig = Config
- /** Admin flag */
- is_admin: boolean
+export interface ConfigManager {
+ watchConf: Context['watchConf']
+ get: () => Config
+ update: (conf: ConfigPartial) => void
}
diff --git a/ui/artalk/src/types/context.ts b/ui/artalk/src/types/context.ts
index a3417334..06e201d4 100644
--- a/ui/artalk/src/types/context.ts
+++ b/ui/artalk/src/types/context.ts
@@ -2,16 +2,18 @@ import type { Marked } from 'marked'
import type {
SidebarShowPayload,
EventPayloadMap,
- ArtalkConfigPartial,
- ArtalkConfig,
+ ConfigPartial,
+ Config,
CommentData,
- DataManagerApi,
+ DataManager,
ListFetchParams,
NotifyLevel,
+ Services,
+ EventManager,
+ UserManager,
} from '.'
-import type { TInjectedServices } from '@/service'
import type { CheckerCaptchaPayload, CheckerPayload } from '@/components/checker'
-import type { EventManagerFuncs } from '@/lib/event-manager'
+import type { DependencyContainer } from '@/lib/injection'
import type { I18n } from '@/i18n'
import type { Api, ApiHandlers } from '@/api'
import type { CommentNode } from '@/comment'
@@ -19,45 +21,74 @@ import type { CommentNode } from '@/comment'
/**
* Artalk Context
*/
-export interface ContextApi extends EventManagerFuncs {
- /** Artalk 根元素对象 */
+export interface Context extends EventManager, DependencyContainer {
+ /**
+ * The root element of Artalk
+ *
+ * @deprecated Use `getEl()` instead
+ */
$root: HTMLElement
- /** 依赖注入函数 */
- inject(depName: K, obj: TInjectedServices[K]): void
+ /** Get the root element */
+ getEl(): HTMLElement
+
+ /**
+ * Inject a dependency object
+ *
+ * @deprecated Use `inject()` instead
+ */
+ get(key: T): Services[T]
+
+ /**
+ * Get config object
+ *
+ * @deprecated Use `getConf()` and `updateConf()` instead
+ */
+ conf: Config
+
+ /** Get the config */
+ getConf(): Config
- /** 获取依赖对象 */
- get(depName: K): TInjectedServices[K]
+ /** Update the config */
+ updateConf(conf: ConfigPartial): void
- /** 配置对象 */
- // TODO: 修改为 getConf() 和 setConf() 并且返回拷贝而不是引用
- conf: ArtalkConfig
+ /** Watch the config */
+ watchConf(
+ keys: T,
+ effect: (val: Pick) => void,
+ ): void
- /** marked 依赖对象 */
+ /** Get the marked instance */
getMarked(): Marked | undefined
- /** 获取 API 以供 HTTP 请求 */
+ /** Set dark mode */
+ setDarkMode(darkMode: boolean | 'auto'): void
+
+ /** Translate i18n message */
+ $t(key: keyof I18n, args?: { [key: string]: string }): string
+
+ /** Get HTTP API client */
getApi(): Api
- /** Get API handlers */
+ /** Get HTTP API handlers */
getApiHandlers(): ApiHandlers
- /** 获取数据管理器对象 */
- getData(): DataManagerApi
+ /** Get Data Manager */
+ getData(): DataManager
- /** 评论回复 */
- replyComment(commentData: CommentData, $comment: HTMLElement): void
+ /** Get User Manager */
+ getUser(): UserManager
- /** 编辑评论 */
- editComment(commentData: CommentData, $comment: HTMLElement): void
-
- /** 获取评论数据 */
+ /** Fetch comments */
fetch(params: Partial): void
- /** 重载评论数据 */
+ /** Reload comments */
reload(): void
- /** 列表滚动到第一个评论的位置 */
+ /** Destroy */
+ destroy(): void
+
+ /** Goto the first comment of the list */
listGotoFirst(): void
/** Get the comment data list */
@@ -78,48 +109,33 @@ export interface ContextApi extends EventManagerFuncs {
*/
getCommentList(): CommentNode[]
- /** 显示侧边栏 */
- showSidebar(payload?: SidebarShowPayload): void
+ /** Reply to a comment */
+ replyComment(commentData: CommentData, $comment: HTMLElement): void
- /** 隐藏侧边栏 */
- hideSidebar(): void
+ /** Edit a comment */
+ editComment(commentData: CommentData, $comment: HTMLElement): void
- /** 编辑器 - 显示加载 */
+ /** Show loading of the editor */
editorShowLoading(): void
- /** 编辑器 - 隐藏加载 */
+ /** Hide loading of the editor */
editorHideLoading(): void
- /** 编辑器 - 显示提示消息 */
+ /** Show notify of the editor */
editorShowNotify(msg: string, type: NotifyLevel): void
- /** 评论框 - 复原状态 */
+ /** Reset the state of the editor */
editorResetState(): void
- /** 验证码检测 */
- checkCaptcha(payload: CheckerCaptchaPayload): Promise
-
- /** 管理员检测 */
- checkAdmin(payload: CheckerPayload): Promise
-
- /** i18n 翻译 */
- $t(key: keyof I18n, args?: { [key: string]: string }): string
-
- /** 设置夜间模式 */
- setDarkMode(darkMode: boolean | 'auto'): void
-
- /** 获取配置 */
- getConf(): ArtalkConfig
+ /** Show the sidebar */
+ showSidebar(payload?: SidebarShowPayload): void
- /** 获取挂载元素 */
- getEl(): HTMLElement
+ /** Hide the sidebar */
+ hideSidebar(): void
- /** 更新配置 */
- updateConf(conf: ArtalkConfigPartial): void
+ /** Check captcha */
+ checkCaptcha(payload: CheckerCaptchaPayload): Promise
- /** 监听配置更新 */
- watchConf(
- keys: T,
- effect: (val: Pick) => void,
- ): void
+ /** Check admin */
+ checkAdmin(payload: CheckerPayload): Promise
}
diff --git a/ui/artalk/src/types/data.ts b/ui/artalk/src/types/data.ts
index 28bd9c09..c7bb191d 100644
--- a/ui/artalk/src/types/data.ts
+++ b/ui/artalk/src/types/data.ts
@@ -219,7 +219,7 @@ export interface ListLastFetchData {
data?: ListData
}
-export interface DataManagerApi {
+export interface DataManager {
getLoading(): boolean
setLoading(val: boolean): void
diff --git a/ui/artalk/src/types/editor.ts b/ui/artalk/src/types/editor.ts
index 1572e351..e8760e40 100644
--- a/ui/artalk/src/types/editor.ts
+++ b/ui/artalk/src/types/editor.ts
@@ -1,10 +1,29 @@
-import type { CommentData, NotifyLevel } from '.'
-import type Component from '@/lib/component'
+import type { CommentData, NotifyLevel, EventManager } from '.'
+import type {
+ EditorEventPayloadMap,
+ PluginManager,
+ PluginManagerOptions,
+} from '@/plugins/editor-kit'
+import type { EditorPlugin } from '@/plugins/editor/_plug'
+import type { EditorOptions } from '@/editor/editor'
import type { EditorUI } from '@/editor/ui'
export type EditorState = 'reply' | 'edit' | 'normal'
-export interface EditorApi extends Component {
+export interface Editor {
+ /**
+ * Get editor options
+ */
+ getOptions(): EditorOptions
+
+ /**
+ * Get the editor element
+ */
+ getEl(): HTMLElement
+
+ /**
+ * Get the editor UI instance
+ */
getUI(): EditorUI
/**
@@ -84,12 +103,31 @@ export interface EditorApi extends Component {
/**
* Start replying a comment
*/
- setReply(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void
+ setReplyComment(commentData: CommentData, $comment: HTMLElement, scroll?: boolean): void
/**
* Start editing a comment
*/
setEditComment(commentData: CommentData, $comment: HTMLElement): void
+
+ /**
+ * Get plugin manager
+ */
+ getPlugins(): PluginManager | undefined
+
+ /**
+ * Set plugin manager
+ */
+ setPlugins(plugins: PluginManager): void
}
-export default EditorApi
+export interface EditorPluginManager {
+ getPlugins: () => EditorPlugin[]
+ getEvents: () => EventManager
+ getEditor: () => Editor
+ getOptions: () => PluginManagerOptions
+ get(plug: T): InstanceType | undefined
+ openPluginPanel: (plug: EditorPlugin) => void
+ closePluginPanel: () => void
+ getTransformedContent: (rawContent: string) => string
+}
diff --git a/ui/artalk/src/types/event.ts b/ui/artalk/src/types/event.ts
index 4669bd3c..c38791bb 100644
--- a/ui/artalk/src/types/event.ts
+++ b/ui/artalk/src/types/event.ts
@@ -1,5 +1,12 @@
-import { CommentData, ListData, ListFetchParams, NotifyData, PageData } from './data'
-import { ArtalkConfig, LocalUser } from './config'
+import type {
+ CommentData,
+ ListData,
+ ListFetchParams,
+ NotifyData,
+ PageData,
+ LocalUser,
+ Config,
+} from '.'
import type { CommentNode } from '@/comment'
export interface ListErrorData {
@@ -17,7 +24,7 @@ export interface EventPayloadMap {
// Basic lifecycle
created: undefined
mounted: undefined
- updated: ArtalkConfig
+ updated: Config
unmounted: undefined
'list-fetch': Partial // 评论列表请求时
@@ -43,3 +50,20 @@ export interface EventPayloadMap {
'sidebar-show': undefined // 侧边栏显示
'sidebar-hide': undefined // 侧边栏隐藏
}
+
+export type EventHandler = (payload: T) => void
+
+export interface Event extends EventOptions {
+ name: K
+ handler: EventHandler
+}
+
+export interface EventOptions {
+ once?: boolean
+}
+
+export interface EventManager {
+ on(name: K, handler: EventHandler, opts?: EventOptions): void
+ off(name: K, handler: EventHandler): void
+ trigger(name: K, payload?: T[K]): void
+}
diff --git a/ui/artalk/src/types/index.ts b/ui/artalk/src/types/index.ts
index e38be02b..4e6ba0a2 100644
--- a/ui/artalk/src/types/index.ts
+++ b/ui/artalk/src/types/index.ts
@@ -1,9 +1,14 @@
export type * from './config'
export type * from './data'
export type * from './context'
+export type * from './user'
+export type * from './list'
export type * from './editor'
export type * from './event'
export type * from './plugin'
export type * from './sidebar'
export type * from './layer'
+export type * from './checker'
+export type * from './service'
export type { I18n, I18nKeys } from '../i18n'
+export type { Api, ApiHandlers } from '../api'
diff --git a/ui/artalk/src/types/layer.ts b/ui/artalk/src/types/layer.ts
index 4310819c..60b64c73 100644
--- a/ui/artalk/src/types/layer.ts
+++ b/ui/artalk/src/types/layer.ts
@@ -7,3 +7,9 @@ export interface Layer {
getAllowMaskClose(): boolean
getEl: () => HTMLElement
}
+
+export interface LayerManager {
+ create(name: string, el?: HTMLElement): Layer
+ destroy(): void
+ getEl(): HTMLElement
+}
diff --git a/ui/artalk/src/types/list.ts b/ui/artalk/src/types/list.ts
new file mode 100644
index 00000000..fa9d5415
--- /dev/null
+++ b/ui/artalk/src/types/list.ts
@@ -0,0 +1,14 @@
+import type { CommentNode } from '../comment'
+import type { CommentData } from './data'
+
+export interface List {
+ getEl: () => HTMLElement
+ getCommentsWrapEl: () => HTMLElement
+ getLayout: (arg: { forceFlatMode?: boolean }) => ListLayout
+ getCommentNodes: () => CommentNode[]
+}
+
+export interface ListLayout {
+ import: (comments: CommentData[]) => void
+ insert: (comment: CommentData, replyComment?: CommentData) => void
+}
diff --git a/ui/artalk/src/types/plugin.ts b/ui/artalk/src/types/plugin.ts
index 2fae206d..f3614c7e 100644
--- a/ui/artalk/src/types/plugin.ts
+++ b/ui/artalk/src/types/plugin.ts
@@ -1,3 +1,3 @@
-import { ContextApi } from './context'
+import { Context } from './context'
-export type ArtalkPlugin = (ctx: ContextApi, options?: T) => void
+export type ArtalkPlugin = (ctx: Context, options?: T) => void
diff --git a/ui/artalk/src/types/service.ts b/ui/artalk/src/types/service.ts
new file mode 100644
index 00000000..bf4b8558
--- /dev/null
+++ b/ui/artalk/src/types/service.ts
@@ -0,0 +1,29 @@
+import type {
+ DataManager,
+ Editor,
+ EventManager,
+ Api,
+ ApiHandlers,
+ ConfigManager,
+ UserManager,
+ LayerManager,
+ List,
+ SidebarLayer,
+ EditorPluginManager,
+ CheckerManager,
+} from '@/types'
+
+export interface Services {
+ config: ConfigManager
+ events: EventManager
+ data: DataManager
+ api: Api
+ apiHandlers: ApiHandlers
+ editor: Editor
+ editorPlugs: EditorPluginManager | undefined
+ list: List
+ sidebar: SidebarLayer
+ checkers: CheckerManager
+ layers: LayerManager
+ user: UserManager
+}
diff --git a/ui/artalk/src/types/sidebar.ts b/ui/artalk/src/types/sidebar.ts
index d3c805cd..c79d41c2 100644
--- a/ui/artalk/src/types/sidebar.ts
+++ b/ui/artalk/src/types/sidebar.ts
@@ -1,3 +1,9 @@
+export interface SidebarLayer {
+ onUserChanged: () => void
+ show: (conf?: SidebarShowPayload) => void
+ hide: () => void
+}
+
export interface SidebarShowPayload {
view?: 'comments' | 'sites' | 'pages' | 'transfer'
}
diff --git a/ui/artalk/src/types/user.ts b/ui/artalk/src/types/user.ts
new file mode 100644
index 00000000..c9f6f869
--- /dev/null
+++ b/ui/artalk/src/types/user.ts
@@ -0,0 +1,28 @@
+/**
+ * Local User Data (in localStorage)
+ *
+ * @note Keep flat for easy handling
+ */
+export interface LocalUser {
+ /** Username (aka. Nickname) */
+ name: string
+
+ /** Email */
+ email: string
+
+ /** Link (aka. Website) */
+ link: string
+
+ /** Token (for authorization) */
+ token: string
+
+ /** Admin flag */
+ is_admin: boolean
+}
+
+export interface UserManager {
+ getData: () => LocalUser
+ update: (dta: Partial) => void
+ logout: () => void
+ checkHasBasicUserInfo: () => boolean
+}
diff --git a/ui/artalk/tests/ui-api.test.ts b/ui/artalk/tests/ui-api.test.ts
index 5cd3c230..29ddc2d2 100644
--- a/ui/artalk/tests/ui-api.test.ts
+++ b/ui/artalk/tests/ui-api.test.ts
@@ -97,7 +97,7 @@ describe('Artalk instance', () => {
it(
'should can listen to events and the conf-remoter works (artalk.trigger, artalk.on, conf-remoter)',
async () => {
- global.devLoadArtalk()
+ global.devMountArtalk()
const fn = vi.fn()