${this.$t('readMore')}`)
- $hideMoreOpenBtn.onclick = (e) => {
- e.stopPropagation()
- this.heightLimitRemove($el)
-
- // 子评论数等于 1,直接取消限高
- const children = this.getChildren()
- if (children.length === 1) children[0].heightLimitRemove(children[0].$content)
- }
- $el.append($hideMoreOpenBtn)
- }
-}
diff --git a/packages/artalk/src/components/dialog.ts b/packages/artalk/src/components/dialog.ts
index d748ea3fd..a54544d8e 100644
--- a/packages/artalk/src/components/dialog.ts
+++ b/packages/artalk/src/components/dialog.ts
@@ -1,4 +1,4 @@
-import Context from '../context'
+import Context from '~/types/context'
import * as Utils from '../lib/utils'
type BtnClickHandler = (btnEl: HTMLElement, dialog: Dialog) => boolean|void
diff --git a/packages/artalk/src/components/editor-plugs/_example-plug.ts.txt b/packages/artalk/src/components/editor-plugs/_example-plug.ts.txt
deleted file mode 100644
index e6dc88456..000000000
--- a/packages/artalk/src/components/editor-plugs/_example-plug.ts.txt
+++ /dev/null
@@ -1,29 +0,0 @@
-import './_PreviewPlug/PreviewPlug.less'
-import Editor from '../Editor'
-import ArtalkContext from '~/src/ArtalkContext'
-import Utils from '~/src/utils'
-
-export default class PreviewPlug extends ArtalkContext {
- public elem: HTMLElement
-
- constructor (public editor: Editor) {
- super(editor.artalk)
-
- this.initElem()
- }
-
- initElem () {
- this.elem = Utils.createElement('
')
- }
-
- static Name = 'example'
- static BtnHTML = '栗子'
-
- getEl () {
- return this.elem
- }
-
- onShow () {}
-
- onHide () {}
-}
diff --git a/packages/artalk/src/components/editor-plugs/editor-plug.ts b/packages/artalk/src/components/editor-plugs/editor-plug.ts
deleted file mode 100644
index 3315f9f10..000000000
--- a/packages/artalk/src/components/editor-plugs/editor-plug.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import Context from "~/src/context"
-import Editor from "../editor"
-
-export default abstract class EditorPlug {
- protected editor: Editor
- protected ctx: Context
- public abstract $el: HTMLElement
-
- constructor (editor: Editor) {
- this.editor = editor
- this.ctx = editor.ctx
- }
-
- public static Name: string
- public static BtnHTML: string
-
- public abstract getEl(): HTMLElement
- public abstract onShow(): void
- public abstract onHide(): void
-}
diff --git a/packages/artalk/src/components/editor-plugs/preview-plug.ts b/packages/artalk/src/components/editor-plugs/preview-plug.ts
deleted file mode 100644
index 91ea36cd4..000000000
--- a/packages/artalk/src/components/editor-plugs/preview-plug.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import './preview-plug.less'
-
-import * as Utils from '~/src/lib/utils'
-import Editor from '../editor'
-import EditorPlug from './editor-plug'
-
-export default class PreviewPlug extends EditorPlug {
- $el!: HTMLElement
- binded: boolean = false
-
- constructor (editor: Editor) {
- super(editor)
-
- this.initEl()
- }
-
- initEl () {
- this.$el = Utils.createElement('
')
- this.binded = false
- }
-
- static Name = 'preview'
- static BtnHTML = '预览
'
-
- getEl () {
- return this.$el
- }
-
- onShow () {
- this.updateContent()
- if (!this.binded) {
- const event = () => {
- this.updateContent()
- }
- this.editor.$textarea.addEventListener('input', event)
- this.editor.$textarea.addEventListener('change', event)
- this.binded = true
- }
- }
-
- onHide () {}
-
- updateContent () {
- if (this.$el.style.display !== 'none') {
- this.$el.innerHTML = this.editor.getContentMarked()
- }
- }
-}
diff --git a/packages/artalk/src/components/editor.ts b/packages/artalk/src/components/editor.ts
deleted file mode 100644
index 4df933448..000000000
--- a/packages/artalk/src/components/editor.ts
+++ /dev/null
@@ -1,615 +0,0 @@
-import '../style/editor.less'
-
-import { CommentData } from '~/types/artalk-data'
-import Context from '../context'
-import Component from '../lib/component'
-import * as Utils from '../lib/utils'
-import * as Ui from '../lib/ui'
-import EditorHTML from './html/editor.html?raw'
-
-import EmoticonsPlug from './editor-plugs/emoticons-plug'
-import PreviewPlug from './editor-plugs/preview-plug'
-import Api from '../api'
-
-export default class Editor extends Component {
- private readonly LOADABLE_PLUG_LIST = [EmoticonsPlug, PreviewPlug]
- public plugList: { [name: string]: any } = {}
-
- public $header: HTMLElement
- public $textareaWrap: HTMLElement
- public $textarea: HTMLTextAreaElement
- public $plugWrap: HTMLElement
- public $bottom: HTMLElement
- public $plugBtnWrap: HTMLElement
- public $imgUploadBtn?: HTMLElement
- public $imgUploadInput?: HTMLInputElement
- public $submitBtn: HTMLButtonElement
- public $notifyWrap: HTMLElement
-
- private replyComment: CommentData|null = null
- private $sendReply: HTMLElement|null = null
-
- private isTraveling = false
-
- private get user () {
- return this.ctx.user
- }
-
- constructor (ctx: Context) {
- super(ctx)
-
- this.$el = Utils.createElement(EditorHTML)
-
- this.$header = this.$el.querySelector('.atk-header')!
- this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
- this.$textarea = this.$el.querySelector('.atk-textarea')!
- this.$plugWrap = this.$el.querySelector('.atk-plug-wrap')!
- this.$bottom = this.$el.querySelector('.atk-bottom')!
- this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
- this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
- this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
-
- this.initLocalStorage()
- this.initHeader()
- this.initTextarea()
- this.initEditorPlug()
- this.initBottomPart()
-
- // 监听事件
- this.ctx.on('editor-open', () => (this.open()))
- this.ctx.on('editor-close', () => (this.close()))
- this.ctx.on('editor-reply', (p) => (this.setReply(p.data, p.$el, p.scroll)))
- this.ctx.on('editor-reply-cancel', () => (this.cancelReply()))
- this.ctx.on('editor-show-loading', () => (Ui.showLoading(this.$el)))
- this.ctx.on('editor-hide-loading', () => (Ui.hideLoading(this.$el)))
- this.ctx.on('editor-notify', (f) => (this.showNotify(f.msg, f.type)))
- this.ctx.on('editor-travel', ($el) => (this.travel($el)))
- this.ctx.on('editor-travel-back', () => (this.travelBack()))
- this.ctx.on('conf-updated', () => (this.refreshUploadBtn()))
- }
-
- initLocalStorage () {
- const localContent = window.localStorage.getItem('ArtalkContent') || ''
- if (localContent.trim() !== '') {
- this.showNotify(this.$t('restoredMsg'), 'i')
- this.setContent(localContent)
- }
- this.$textarea.addEventListener('input', () => {
- this.saveContent()
- })
- }
-
- initHeader () {
- Object.keys(this.user.data).forEach((field) => {
- const inputEl = this.getInputEl(field)
- if (inputEl && inputEl instanceof HTMLInputElement) {
- inputEl.value = this.user.data[field] || ''
- // 绑定事件
- inputEl.addEventListener('input', () => this.onHeaderInput(field, inputEl))
- }
- })
-
- // Link URL 自动补全协议
- const $linkInput = this.getInputEl('link')
- if ($linkInput) {
- $linkInput.addEventListener('change', () => {
- const link = $linkInput.value.trim()
- if (!!link && !/^(http|https):\/\//.test(link)) {
- $linkInput.value = `https://${link}`
- this.user.data.link = $linkInput.value
- this.saveUser()
- }
- })
- }
-
- // i18n patch
- [['nick', '昵称'], ['email', '邮箱'], ['link', '网址']].forEach((entry) => {
- const $input = this.getInputEl(entry[0])!
- $input.placeholder = $input.placeholder.replace(entry[1], this.$t(entry[0] as any))
- })
- }
-
- getInputEl (field: 'nick'|'email'|'link'|string) {
- const inputEl = this.$header.querySelector
(`[name="${field}"]`)
- return inputEl
- }
-
- queryUserInfo = {
- timeout: null,
- abortFunc: <(() => void)|null>null
- }
-
- /** header 输入框内容变化事件 */
- onHeaderInput(field: string, inputEl: HTMLInputElement) {
- this.user.data[field] = inputEl.value.trim()
-
- // 若修改的是 nick or email
- if (field === 'nick' || field === 'email') {
- this.user.data.token = '' // 清除 token 登陆状态
- this.user.data.isAdmin = false
-
- // 获取用户信息
- if (this.queryUserInfo.timeout !== null) window.clearTimeout(this.queryUserInfo.timeout) // 清除待发出的请求
- if (this.queryUserInfo.abortFunc !== null) this.queryUserInfo.abortFunc() // 之前发出未完成的请求立刻中止
-
- this.queryUserInfo.timeout = window.setTimeout(() => {
- this.queryUserInfo.timeout = null // 清理
-
- const {req, abort} = new Api(this.ctx).userGet(
- this.user.data.nick, this.user.data.email
- )
- this.queryUserInfo.abortFunc = abort
- req.then(data => {
- if (!data.is_login) {
- this.user.data.token = ''
- this.user.data.isAdmin = false
- }
-
- // 未读消息更新
- this.ctx.trigger('unread-update', { notifies: data.unread, })
-
- // 若用户为管理员,执行登陆操作
- if (this.user.checkHasBasicUserInfo() && !data.is_login && data.user && data.user.is_admin) {
- this.showLoginDialog()
- }
-
- // 自动填入 link
- if (data.user && data.user.link) {
- this.user.data.link = data.user.link
- this.getInputEl('link')!.value = data.user.link
- }
- })
- .catch(() => {})
- .finally(() => {
- this.queryUserInfo.abortFunc = null // 清理
- })
- }, 400) // 延迟执行,减少请求次数
- }
-
- this.saveUser()
- }
-
- showLoginDialog () {
- this.ctx.trigger('checker-admin', {
- onSuccess: () => {
- }
- })
- }
-
- saveUser () {
- this.user.save()
- this.ctx.trigger('user-changed', this.ctx.user.data)
- }
-
- saveContent () {
- window.localStorage.setItem('ArtalkContent', this.getContentOriginal().trim())
- }
-
- initTextarea () {
- // 占位符
- this.$textarea.placeholder = this.ctx.conf.placeholder || this.$t('placeholder')
-
- // 修复按下 Tab 输入的内容
- this.$textarea.addEventListener('keydown', (e) => {
- const keyCode = e.keyCode || e.which
-
- if (keyCode === 9) {
- e.preventDefault()
- this.insertContent('\t')
- }
- })
-
- // 输入框高度随内容而变化
- this.$textarea.addEventListener('input', (evt) => {
- this.adjustTextareaHeight()
- })
- }
-
- adjustTextareaHeight () {
- const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
- this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
- this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
- }
-
- openedPlugName: string|null = null
-
- initEditorPlug () {
- this.plugList = {}
- this.$plugWrap.innerHTML = ''
- this.$plugWrap.style.display = 'none'
- this.openedPlugName = null
- this.$plugBtnWrap.innerHTML = ''
-
- // 初始化 plug 按钮
- this.LOADABLE_PLUG_LIST.forEach((PlugObj) => {
- if (PlugObj.Name === 'emoticons' && !this.conf.emoticons) return
-
- // 切换按钮
- let btnHtml = PlugObj.BtnHTML
- btnHtml = btnHtml.replace('表情', this.$t('emoticon')).replace('预览', this.$t('preview'))
- const btnElem = Utils.createElement(`${btnHtml}`)
- this.$plugBtnWrap.appendChild(btnElem)
-
- btnElem.addEventListener('click', () => {
- let plug = this.plugList[PlugObj.Name]
- if (!plug) {
- plug = new PlugObj(this)
- this.plugList[PlugObj.Name] = plug
- }
-
- this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
-
- // 若点击已打开的,则收起
- if (PlugObj.Name === this.openedPlugName) {
- plug.onHide()
- this.$plugWrap.style.display = 'none'
- this.openedPlugName = null
- return
- }
-
- if (this.$plugWrap.querySelector(`[data-plug-name="${PlugObj.Name}"]`) === null) {
- // 需要初始化
- const plugEl = plug.getEl()
- plugEl.setAttribute('data-plug-name', PlugObj.Name)
- plugEl.style.display = 'none'
- this.$plugWrap.appendChild(plugEl)
- }
-
- (Array.from(this.$plugWrap.children) as HTMLElement[]).forEach((plugItemEl: HTMLElement) => {
- const plugItemName = plugItemEl.getAttribute('data-plug-name')!
- if (plugItemName === PlugObj.Name) {
- plugItemEl.style.display = ''
- this.plugList[plugItemName].onShow()
- } else {
- plugItemEl.style.display = 'none'
- this.plugList[plugItemName].onHide()
- }
- })
-
- this.$plugWrap.style.display = ''
- this.openedPlugName = PlugObj.Name
-
- btnElem.classList.add('active')
- })
- })
-
- // 表情包插件预加载
- if (this.conf.emoticons) {
- const emoPlug = new EmoticonsPlug(this)
- this.plugList[EmoticonsPlug.Name] = emoPlug
-
- window.setTimeout(() => {
- emoPlug.loadEmoticonsData()
- }, 1000) // 延迟 1s 加载
- }
-
- this.initImgUpload()
- }
-
- /** 关闭编辑器插件 */
- closePlug () {
- this.$plugWrap.innerHTML = ''
- this.$plugWrap.style.display = 'none'
- this.openedPlugName = null
- }
-
- /** 允许的图片格式 */
- allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
-
- /** 初始化图片上传功能 */
- initImgUpload() {
- this.$imgUploadBtn = Utils.createElement(`${this.$t('image')}`)
- this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
-
- this.$imgUploadInput = document.createElement('input')
- this.$imgUploadInput.type = 'file'
- this.$imgUploadInput.style.display = 'none'
- this.$imgUploadInput.accept = this.allowImgExts.map(o => `.${o}`).join(',')
- this.$imgUploadBtn.after(this.$imgUploadInput)
-
- // 按钮点击
- this.$imgUploadBtn.onclick = () => {
- // 选择图片
- const $input = this.$imgUploadInput!
- $input.onchange = () => {
- (async () => { // 解决阻塞 UI 问题
- if (!$input.files || $input.files.length === 0) return
- const file = $input.files[0]
- this.uploadImg(file)
- })()
- }
- $input.click() // 显示选择图片对话框
- }
-
- // 统一从 FileList 获取文件并上传图片方法
- const uploadFromFileList = (files?: FileList) => {
- if (!files) return
-
- for (let i = 0; i < files.length; i++) {
- const file = files[i]
- this.uploadImg(file)
- }
- }
-
- // 拖拽图片
- // @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
- // 阻止浏览器的默认释放行为
- this.$textarea.addEventListener('dragover', (evt) => {
- evt.stopPropagation()
- evt.preventDefault()
- })
-
- this.$textarea.addEventListener('drop', (evt) => {
- const files = evt.dataTransfer?.files
- if (files?.length) {
- evt.preventDefault()
- uploadFromFileList(files)
- }
- })
-
- // 粘贴图片
- this.$textarea.addEventListener('paste', (evt) => {
- const files = evt.clipboardData?.files
- if (files?.length) {
- evt.preventDefault()
- uploadFromFileList(files)
- }
- })
- }
-
- /** 刷新图片上传按钮 */
- refreshUploadBtn() {
- if (!this.$imgUploadBtn) return
-
- if (!this.ctx.conf.imgUpload) {
- this.$imgUploadBtn.setAttribute('atk-only-admin-show', '')
- this.ctx.trigger('check-admin-show-el')
- }
- }
-
- async uploadImg(file: File) {
- const fileExt = /[^.]+$/.exec(file.name)
- if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
-
- // 未登录提示
- if (!this.ctx.user.checkHasBasicUserInfo()) {
- this.showNotify('填入你的名字邮箱才能上传哦', 'w')
- return
- }
-
- // 插入图片前换一行
- let insertPrefix = '\n'
- if (this.$textarea.value.trim() === '') insertPrefix = ''
-
- // 插入占位加载文字
- const uploadPlaceholderTxt = `${insertPrefix}![](Uploading ${file.name}...)`
- this.insertContent(uploadPlaceholderTxt)
-
- // 上传图片
- let resp: any
- try {
- resp = await new Api(this.ctx).imgUpload(file)
- } catch (err: any) {
- console.error(err)
- this.showNotify(`${this.$t('uploadFail')},${err.msg}`, 'e')
- }
- if (!!resp && resp.img_url) {
- let imgURL = resp.img_url as string
-
- // 若为相对路径,加上 artalk server
- if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
-
- // 上传成功插入图片
- this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}![](${imgURL})`))
- } else {
- // 上传失败删除加载文字
- this.setContent(this.$textarea.value.replace(uploadPlaceholderTxt, ''))
- }
- }
-
- insertContent (val: string) {
- if ((document as any).selection) {
- this.$textarea.focus();
- (document as any).selection.createRange().text = val
- this.$textarea.focus()
- } else if (this.$textarea.selectionStart || this.$textarea.selectionStart === 0) {
- const sStart = this.$textarea.selectionStart
- const sEnd = this.$textarea.selectionEnd
- const sT = this.$textarea.scrollTop
- this.setContent(this.$textarea.value.substring(0, sStart) + val + this.$textarea.value.substring(sEnd, this.$textarea.value.length))
- this.$textarea.focus()
- this.$textarea.selectionStart = sStart + val.length
- this.$textarea.selectionEnd = sStart + val.length
- this.$textarea.scrollTop = sT
- } else {
- this.$textarea.focus()
- this.$textarea.value += val
- }
- }
-
- setContent (val: string) {
- this.$textarea.value = val
- this.saveContent()
- if (!!this.plugList && !!this.plugList.preview) {
- this.plugList.preview.updateContent()
- }
-
- // 延迟执行防止无效
- window.setTimeout(() => {
- this.adjustTextareaHeight()
- }, 80)
- }
-
- clearEditor () {
- this.setContent('')
- this.cancelReply()
- }
-
- /** 获取最终用于 submit 的数据 */
- getFinalContent () {
- let content = this.getContentOriginal()
-
- // 表情包处理
- if (this.plugList && this.plugList.emoticons) {
- const emoticonsPlug = this.plugList.emoticons as EmoticonsPlug
- content = emoticonsPlug.transEmoticonImageText(content)
- }
-
- return content
- }
-
- getContentOriginal () {
- return this.$textarea.value || '' // Tip: !!"0" === true
- }
-
- getContentMarked () {
- return Utils.marked(this.ctx, this.getFinalContent())
- }
-
- initBottomPart () {
- this.initReply()
- this.initSubmit()
- }
-
- initReply () {
- this.replyComment = null
- this.$sendReply = null
- }
-
- setReply (commentData: CommentData, $comment: HTMLElement, scroll = true) {
- if (this.replyComment !== null) {
- this.cancelReply()
- }
-
- if (this.$sendReply === null) {
- this.$sendReply = Utils.createElement(`${this.$t('reply')} ×
`);
- this.$sendReply.querySelector('.atk-text')!.innerText = `@${commentData.nick}`
- this.$sendReply.addEventListener('click', () => {
- this.cancelReply()
- })
- this.$textareaWrap.append(this.$sendReply)
- }
- this.replyComment = commentData
-
- if (this.ctx.conf.editorTravel === true) {
- this.travel($comment)
- }
-
- if (scroll) Ui.scrollIntoView(this.$el)
-
- this.$textarea.focus()
- }
-
- cancelReply () {
- if (this.$sendReply !== null) {
- this.$sendReply.remove()
- this.$sendReply = null
- }
- this.replyComment = null
-
- if (this.ctx.conf.editorTravel === true) {
- this.travelBack()
- }
- }
-
- initSubmit () {
- this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
-
- this.$submitBtn.addEventListener('click', (evt) => {
- const btnEl = evt.currentTarget
- this.submit()
- })
- }
-
- async submit () {
- if (this.getFinalContent().trim() === '') {
- this.$textarea.focus()
- return
- }
-
- this.ctx.trigger('editor-submit')
-
- Ui.showLoading(this.$el)
-
- try {
- const nComment = await new Api(this.ctx).add({
- content: this.getFinalContent(),
- nick: this.user.data.nick,
- email: this.user.data.email,
- link: this.user.data.link,
- rid: (this.replyComment === null) ? 0 : this.replyComment.id,
- page_key: (this.replyComment === null) ? this.ctx.conf.pageKey : this.replyComment.page_key,
- page_title: (this.replyComment === null) ? this.ctx.conf.pageTitle : undefined,
- site_name: (this.replyComment === null) ? this.ctx.conf.site : this.replyComment.site_name
- })
-
- // 回复不同页面的评论
- if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
- window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
- }
-
- this.ctx.trigger('list-insert', nComment)
- this.clearEditor() // 清空编辑器
- this.ctx.trigger('editor-submitted')
- } catch (err: any) {
- console.error(err)
- this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
- return
- } finally {
- Ui.hideLoading(this.$el)
- }
- }
-
- showNotify (msg: string, type) {
- Ui.showNotify(this.$notifyWrap, msg, type)
- }
-
- /** 关闭评论 */
- close () {
- if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
- this.$textareaWrap.prepend(Utils.createElement(''))
-
- if (!this.user.data.isAdmin) {
- this.$textarea.style.display = 'none'
- this.closePlug()
- this.$bottom.style.display = 'none'
- } else {
- // 管理员一直打开评论
- this.$textarea.style.display = ''
- this.$bottom.style.display = ''
- }
- }
-
- /** 打开评论 */
- open () {
- this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
- this.$textarea.style.display = ''
- this.$bottom.style.display = ''
- }
-
- travel ($afterEl: HTMLElement) {
- if (this.isTraveling) return
- this.isTraveling = true
- this.$el.after(Utils.createElement(''))
-
- const $travelPlace = Utils.createElement('')
- $afterEl.after($travelPlace)
- $travelPlace.replaceWith(this.$el)
-
- this.$el.classList.add('atk-fade-in') // 添加渐入动画
- }
-
- travelBack () {
- if (!this.isTraveling) return
- this.isTraveling = false
- this.ctx.$root.querySelector('.atk-editor-travel-placeholder')?.replaceWith(this.$el)
-
- // 取消回复
- if (this.replyComment !== null) this.cancelReply()
- }
-
- initRemoteEmoticons () {
-
- }
-}
-
diff --git a/packages/artalk/src/components/html/editor.html b/packages/artalk/src/components/html/editor.html
deleted file mode 100644
index 2120103e2..000000000
--- a/packages/artalk/src/components/html/editor.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
diff --git a/packages/artalk/src/context.ts b/packages/artalk/src/context.ts
index 96aa4aa39..79da0fcfd 100644
--- a/packages/artalk/src/context.ts
+++ b/packages/artalk/src/context.ts
@@ -1,45 +1,46 @@
import { marked as libMarked } from 'marked'
import ArtalkConfig from '~/types/artalk-config'
-import { EventPayloadMap, Event, EventScopeType, Handler } from '~/types/event'
+import { Event } from '~/types/event'
import { internal as internalLocales, I18n } from './i18n'
import User from './lib/user'
+import ContextApi from '../types/context'
/**
* Artalk Context
*/
-export default class Context {
+export default class Context implements ContextApi {
public cid: number // Context 唯一标识
- public $root: HTMLElement
public conf: ArtalkConfig
public user: User
+ public $root: HTMLElement
private eventList: Event[] = []
- public constructor (rootEl: HTMLElement, conf: ArtalkConfig) {
+ public constructor($root: HTMLElement, conf: ArtalkConfig) {
this.cid = +new Date()
- this.$root = rootEl
this.conf = conf
- this.user = new User(this.conf)
+ this.user = new User(this)
+ this.$root = $root
this.$root.setAttribute('atk-run-id', this.cid.toString())
}
- public on(name: K, handler: Handler, scope: EventScopeType = 'internal') {
- this.eventList.push({ name, handler: handler as any, scope })
+ public on(name: any, handler: any, scope: any = 'internal') {
+ this.eventList.push({ name, handler, scope })
}
- public off(name: K, handler?: Handler, scope: EventScopeType = 'internal') {
+ public off(name: any, handler: any, scope: any = 'internal') {
this.eventList = this.eventList.filter((evt) => {
if (handler) return !(evt.name === name && evt.handler === handler && evt.scope === scope)
return !(evt.name === name && evt.scope === scope) // 删除全部相同 name event
})
}
- public trigger(name: K, payload?: EventPayloadMap[K], scope?: EventScopeType) {
+ public trigger(name: any, payload?: any, scope?: any) {
this.eventList
.filter((evt) => evt.name === name && (scope ? (evt.scope === scope) : true))
.map((evt) => evt.handler)
- .forEach((handler) => handler(payload as any))
+ .forEach((handler) => handler(payload))
}
public markedInstance!: typeof libMarked
diff --git a/packages/artalk/src/defaults.ts b/packages/artalk/src/defaults.ts
index cc2b2fd95..c2402da97 100644
--- a/packages/artalk/src/defaults.ts
+++ b/packages/artalk/src/defaults.ts
@@ -3,6 +3,7 @@ import ArtalkConfig from "~/types/artalk-config"
const defaults: ArtalkConfig = {
el: '',
pageKey: '',
+ pageTitle: '',
server: '',
site: '',
diff --git a/packages/artalk/src/editor/editor.html b/packages/artalk/src/editor/editor.html
new file mode 100644
index 000000000..0a64ef309
--- /dev/null
+++ b/packages/artalk/src/editor/editor.html
@@ -0,0 +1,18 @@
+
diff --git a/packages/artalk/src/editor/editor.ts b/packages/artalk/src/editor/editor.ts
new file mode 100644
index 000000000..092387db1
--- /dev/null
+++ b/packages/artalk/src/editor/editor.ts
@@ -0,0 +1,420 @@
+import '@/style/editor.less'
+
+import { CommentData } from '~/types/artalk-data'
+import Context from '~/types/context'
+import Component from '../lib/component'
+import * as Utils from '../lib/utils'
+import * as Ui from '../lib/ui'
+import Api from '../api'
+
+import EditorHTML from './editor.html?raw'
+
+import EmoticonsPlug from './plugs/emoticons-plug'
+import UploadPlug from './plugs/upload-plug'
+import PreviewPlug from './plugs/preview-plug'
+import EditorPlug from './plugs/editor-plug'
+import HeaderInputPlug from './plugs/header-input-plug'
+
+export default class Editor extends Component {
+ private get user() { return this.ctx.user }
+
+ public $header: HTMLElement
+ public $nick: HTMLInputElement
+ public $email: HTMLInputElement
+ public $link: HTMLInputElement
+ public get $inputs() {
+ return { nick: this.$nick, email: this.$email, link: this.$link }
+ }
+
+ public $textareaWrap: HTMLElement
+ public $textarea: HTMLTextAreaElement
+ public $bottom: HTMLElement
+ public $submitBtn: HTMLButtonElement
+ public $notifyWrap: HTMLElement
+
+ private replyComment: CommentData|null = null
+ private $sendReply: HTMLElement|null = null
+
+ private isTraveling = false
+
+ /** 启用的插件 */
+ private readonly ENABLED_PLUGS = [ EmoticonsPlug, UploadPlug, PreviewPlug, HeaderInputPlug ]
+ public plugList: { [name: string]: EditorPlug } = {}
+ private openedPlugName: string|null = null
+ public $plugPanelWrap: HTMLElement
+ public $plugBtnWrap: HTMLElement
+
+ public constructor(ctx: Context) {
+ super(ctx)
+
+ this.$el = Utils.createElement(EditorHTML)
+
+ this.$header = this.$el.querySelector('.atk-header')!
+ this.$nick = this.$header.querySelector('[name="nick"]')!
+ this.$email = this.$header.querySelector('[name="email"]')!
+ this.$link = this.$header.querySelector('[name="link"]')!
+
+ this.$textareaWrap = this.$el.querySelector('.atk-textarea-wrap')!
+ this.$textarea = this.$el.querySelector('.atk-textarea')!
+ this.$bottom = this.$el.querySelector('.atk-bottom')!
+ this.$submitBtn = this.$el.querySelector('.atk-send-btn')!
+ this.$notifyWrap = this.$el.querySelector('.atk-notify-wrap')!
+
+ this.$plugBtnWrap = this.$el.querySelector('.atk-plug-btn-wrap')!
+ this.$plugPanelWrap = this.$el.querySelector('.atk-plug-panel-wrap')!
+
+ // 执行初始化
+ this.initLocalStorage()
+ this.initHeader()
+ this.initTextarea()
+ this.initPlugs()
+ this.initSubmitBtn()
+
+ // 监听事件
+ this.ctx.on('editor-open', () => (this.open()))
+ this.ctx.on('editor-close', () => (this.close()))
+ this.ctx.on('editor-reply', (p) => (this.setReply(p.data, p.$el, p.scroll)))
+ this.ctx.on('editor-reply-cancel', () => (this.cancelReply()))
+ this.ctx.on('editor-show-loading', () => (Ui.showLoading(this.$el)))
+ this.ctx.on('editor-hide-loading', () => (Ui.hideLoading(this.$el)))
+ this.ctx.on('editor-notify', (f) => (this.showNotify(f.msg, f.type)))
+ this.ctx.on('editor-travel', ($el) => (this.travel($el)))
+ this.ctx.on('editor-travel-back', () => (this.travelBack()))
+ this.ctx.on('conf-updated', () => {})
+ }
+
+ private initLocalStorage() {
+ const localContent = window.localStorage.getItem('ArtalkContent') || ''
+ if (localContent.trim() !== '') {
+ this.showNotify(this.$t('restoredMsg'), 'i')
+ this.setContent(localContent)
+ }
+
+ this.$textarea.addEventListener('input', () => (this.saveToLocalStorage()))
+ }
+
+ private saveToLocalStorage() {
+ window.localStorage.setItem('ArtalkContent', this.getContentOriginal().trim())
+ }
+
+ private initHeader() {
+ Object.entries(this.$inputs).forEach(([key, $input]) => {
+ $input.value = this.user.data[key] || ''
+ $input.addEventListener('input', () => this.onHeaderInput(key, $input))
+
+ // 设置 Placeholder
+ $input.placeholder = `${this.$t(key as any)}`
+ })
+ }
+
+ private onHeaderInput(key: string, $input: HTMLInputElement) {
+ this.user.update({
+ [key]: $input.value.trim()
+ })
+
+ // 插件监听事件响应
+ Object.entries(this.plugList).forEach(([plugName, plug]) => {
+ if (plug.onHeaderInput) plug.onHeaderInput(key, $input)
+ })
+ }
+
+ private initTextarea() {
+ // 占位符
+ this.$textarea.placeholder = this.ctx.conf.placeholder || this.$t('placeholder')
+
+ // 修复按下 Tab 输入的内容
+ this.$textarea.addEventListener('keydown', (e) => {
+ const keyCode = e.keyCode || e.which
+
+ if (keyCode === 9) {
+ e.preventDefault()
+ this.insertContent('\t')
+ }
+ })
+
+ // 输入框高度随内容而变化
+ this.$textarea.addEventListener('input', () => {
+ this.adjustTextareaHeight()
+ })
+ }
+
+ private initSubmitBtn() {
+ this.$submitBtn.innerText = this.ctx.conf.sendBtn || this.$t('send')
+ this.$submitBtn.addEventListener('click', () => (this.submit()))
+ }
+
+ /** 最终用于 submit 的数据 */
+ public getFinalContent() {
+ let content = this.getContentOriginal()
+
+ // 表情包处理
+ if (this.plugList.emoticons) {
+ content = (this.plugList.emoticons as EmoticonsPlug).transEmoticonImageText(content)
+ }
+
+ return content
+ }
+
+ public getContentOriginal() {
+ return this.$textarea.value || '' // Tip: !!"0" === true
+ }
+
+ public getContentMarked() {
+ return Utils.marked(this.ctx, this.getFinalContent())
+ }
+
+ public setContent(val: string) {
+ this.$textarea.value = val
+ this.saveToLocalStorage()
+ if (this.plugList.preview) {
+ ;(this.plugList.preview as PreviewPlug).updateContent()
+ }
+
+ // 延迟执行防止无效
+ window.setTimeout(() => {
+ this.adjustTextareaHeight()
+ }, 80)
+ }
+
+ public insertContent(val: string) {
+ if ((document as any).selection) {
+ this.$textarea.focus();
+ (document as any).selection.createRange().text = val
+ this.$textarea.focus()
+ } else if (this.$textarea.selectionStart || this.$textarea.selectionStart === 0) {
+ const sStart = this.$textarea.selectionStart
+ const sEnd = this.$textarea.selectionEnd
+ const sT = this.$textarea.scrollTop
+ this.setContent(this.$textarea.value.substring(0, sStart) + val + this.$textarea.value.substring(sEnd, this.$textarea.value.length))
+ this.$textarea.focus()
+ this.$textarea.selectionStart = sStart + val.length
+ this.$textarea.selectionEnd = sStart + val.length
+ this.$textarea.scrollTop = sT
+ } else {
+ this.$textarea.focus()
+ this.$textarea.value += val
+ }
+ }
+
+ public clearEditor() {
+ this.setContent('')
+ this.cancelReply()
+ }
+
+ private adjustTextareaHeight() {
+ const diff = this.$textarea.offsetHeight - this.$textarea.clientHeight
+ this.$textarea.style.height = '0px' // it's a magic. 若不加此行,内容减少,高度回不去
+ this.$textarea.style.height = `${this.$textarea.scrollHeight + diff}px`
+ }
+
+ public setReply(commentData: CommentData, $comment: HTMLElement, scroll = true) {
+ if (this.replyComment !== null) {
+ this.cancelReply()
+ }
+
+ if (this.$sendReply === null) {
+ this.$sendReply = Utils.createElement(`${this.$t('reply')} ×
`);
+ this.$sendReply.querySelector('.atk-text')!.innerText = `@${commentData.nick}`
+ this.$sendReply.addEventListener('click', () => {
+ this.cancelReply()
+ })
+ this.$textareaWrap.append(this.$sendReply)
+ }
+ this.replyComment = commentData
+
+ if (this.ctx.conf.editorTravel === true) {
+ this.travel($comment)
+ }
+
+ if (scroll) Ui.scrollIntoView(this.$el)
+
+ this.$textarea.focus()
+ }
+
+ public cancelReply() {
+ if (this.$sendReply !== null) {
+ this.$sendReply.remove()
+ this.$sendReply = null
+ }
+ this.replyComment = null
+
+ if (this.ctx.conf.editorTravel === true) {
+ this.travelBack()
+ }
+ }
+
+ public showNotify(msg: string, type: "i"|"s"|"w"|"e") {
+ Ui.showNotify(this.$notifyWrap, msg, type)
+ }
+
+ /** 提交评论 */
+ public async submit() {
+ if (this.getFinalContent().trim() === '') {
+ this.$textarea.focus()
+ return
+ }
+
+ this.ctx.trigger('editor-submit')
+
+ Ui.showLoading(this.$el)
+
+ try {
+ const nComment = await new Api(this.ctx).add({
+ content: this.getFinalContent(),
+ nick: this.user.data.nick,
+ email: this.user.data.email,
+ link: this.user.data.link,
+ rid: (this.replyComment === null) ? 0 : this.replyComment.id,
+ page_key: (this.replyComment === null) ? this.ctx.conf.pageKey : this.replyComment.page_key,
+ page_title: (this.replyComment === null) ? this.ctx.conf.pageTitle : undefined,
+ site_name: (this.replyComment === null) ? this.ctx.conf.site : this.replyComment.site_name
+ })
+
+ // 回复不同页面的评论
+ if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
+ window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
+ }
+
+ this.ctx.trigger('list-insert', nComment)
+ this.clearEditor() // 清空编辑器
+ this.ctx.trigger('editor-submitted')
+ } catch (err: any) {
+ console.error(err)
+ this.showNotify(`${this.$t('commentFail')},${err.msg || String(err)}`, 'e')
+ return
+ } finally {
+ Ui.hideLoading(this.$el)
+ }
+ }
+
+ /** 关闭评论 */
+ public close() {
+ if (!this.$textareaWrap.querySelector('.atk-comment-closed'))
+ this.$textareaWrap.prepend(Utils.createElement(''))
+
+ if (!this.user.data.isAdmin) {
+ this.$textarea.style.display = 'none'
+ this.closePlugPanel()
+ this.$bottom.style.display = 'none'
+ } else {
+ // 管理员一直打开评论
+ this.$textarea.style.display = ''
+ this.$bottom.style.display = ''
+ }
+ }
+
+ /** 打开评论 */
+ public open() {
+ this.$textareaWrap.querySelector('.atk-comment-closed')?.remove()
+ this.$textarea.style.display = ''
+ this.$bottom.style.display = ''
+ }
+
+ /** 移动评论框到置顶元素之后 */
+ public travel($afterEl: HTMLElement) {
+ if (this.isTraveling) return
+ this.isTraveling = true
+ this.$el.after(Utils.createElement(''))
+
+ const $travelPlace = Utils.createElement('')
+ $afterEl.after($travelPlace)
+ $travelPlace.replaceWith(this.$el)
+
+ this.$el.classList.add('atk-fade-in') // 添加渐入动画
+ }
+
+ /** 评论框归位 */
+ public travelBack() {
+ if (!this.isTraveling) return
+ this.isTraveling = false
+ this.ctx.$root.querySelector('.atk-editor-travel-placeholder')?.replaceWith(this.$el)
+
+ // 取消回复
+ if (this.replyComment !== null) this.cancelReply()
+ }
+
+ /** 插件初始化 */
+ private initPlugs() {
+ this.plugList = {}
+ this.$plugPanelWrap.innerHTML = ''
+ this.$plugPanelWrap.style.display = 'none'
+ this.openedPlugName = null
+ this.$plugBtnWrap.innerHTML = ''
+
+ const disabledPlugs: string[] = []
+ if (!this.conf.emoticons) disabledPlugs.push('emoticons')
+
+ // 初始化 Editor 插件
+ this.ENABLED_PLUGS.forEach((Plug) => {
+ const plugName = Plug.Name
+
+ // 禁用的插件
+ if (disabledPlugs.includes(plugName)) return
+
+ // 插件对象实例化
+ const plug = new Plug(this)
+ this.plugList[plugName] = plug
+
+ // 插件按钮
+ const $btn = plug.getBtn()
+ if ($btn) {
+ this.$plugBtnWrap.appendChild($btn)
+ $btn.onclick = $btn.onclick || (() => {
+ // 其他按钮去除 Active
+ this.$plugBtnWrap.querySelectorAll('.active').forEach(item => item.classList.remove('active'))
+
+ // 若点击已打开的,则关闭打开的面板
+ if (plugName === this.openedPlugName) {
+ this.closePlugPanel()
+ return
+ }
+
+ this.openPlugPanel(plugName)
+
+ // 当前按钮添加 Active
+ $btn.classList.add('active')
+ })
+
+ // 插件面板初始化
+ const $panel = plug.getPanel()
+ if ($panel) {
+ $panel.setAttribute('data-plug-name', plugName)
+ $panel.style.display = 'none'
+ this.$plugPanelWrap.appendChild($panel)
+ }
+ }
+ })
+ }
+
+ /** 展开插件面板 */
+ public openPlugPanel(plugName: string) {
+ Object.entries(this.plugList).forEach(([aPlugName, plug]) => {
+ const plugPanel = plug.getPanel()
+ if (!plugPanel) return
+
+ if (aPlugName === plugName) {
+ plugPanel.style.display = ''
+ if (plug.onPanelShow) plug.onPanelShow()
+ } else {
+ plugPanel.style.display = 'none'
+ if (plug.onPanelHide) plug.onPanelHide()
+ }
+ })
+
+ this.$plugPanelWrap.style.display = ''
+ this.openedPlugName = plugName
+ }
+
+ /** 收起插件面板 */
+ public closePlugPanel() {
+ if (!this.openedPlugName) return
+
+ const plug = this.plugList[this.openedPlugName]
+ if (!plug) return
+
+ if (plug.onPanelHide) plug.onPanelHide()
+
+ this.$plugPanelWrap.style.display = 'none'
+ this.openedPlugName = null
+ }
+}
diff --git a/packages/artalk/src/editor/index.ts b/packages/artalk/src/editor/index.ts
new file mode 100644
index 000000000..2916278ad
--- /dev/null
+++ b/packages/artalk/src/editor/index.ts
@@ -0,0 +1,3 @@
+import Editor from './editor'
+
+export default Editor
diff --git a/packages/artalk/src/editor/plugs/editor-plug.ts b/packages/artalk/src/editor/plugs/editor-plug.ts
new file mode 100644
index 000000000..4b1823fa8
--- /dev/null
+++ b/packages/artalk/src/editor/plugs/editor-plug.ts
@@ -0,0 +1,51 @@
+import * as Utils from '@/lib/utils'
+import Editor from '../editor'
+
+/**
+ * Editor 插件
+ *
+ * @desc 使用 Interface x Abstract 合并声明
+ * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
+ */
+interface EditorPlug {
+ onPanelShow?(): void
+ onPanelHide?(): void
+}
+
+abstract class EditorPlug {
+ protected editor: Editor
+ protected get ctx() { return this.editor.ctx }
+ protected $panel?: HTMLElement
+ protected $btn?: HTMLElement
+ public onHeaderInput?: (key: string, $input: HTMLElement) => void
+
+ public constructor(editor: Editor) {
+ this.editor = editor
+ }
+
+ public static Name: string
+
+ protected registerPanel(html: string = '') {
+ this.$panel = Utils.createElement(html)
+ return this.$panel
+ }
+
+ protected registerBtn(html: string) {
+ this.$btn = Utils.createElement(`${html}`)
+ return this.$btn
+ }
+
+ protected registerHeaderInputEvt(action: (key: string, $input: HTMLElement) => void) {
+ this.onHeaderInput = action
+ }
+
+ public getPanel() {
+ return this.$panel
+ }
+
+ public getBtn() {
+ return this.$btn
+ }
+}
+
+export default EditorPlug
diff --git a/packages/artalk/src/components/editor-plugs/emoticons-plug.less b/packages/artalk/src/editor/plugs/emoticons-plug.less
similarity index 100%
rename from packages/artalk/src/components/editor-plugs/emoticons-plug.less
rename to packages/artalk/src/editor/plugs/emoticons-plug.less
diff --git a/packages/artalk/src/components/editor-plugs/emoticons-plug.ts b/packages/artalk/src/editor/plugs/emoticons-plug.ts
similarity index 79%
rename from packages/artalk/src/components/editor-plugs/emoticons-plug.ts
rename to packages/artalk/src/editor/plugs/emoticons-plug.ts
index a132b815f..2f92933c6 100644
--- a/packages/artalk/src/components/editor-plugs/emoticons-plug.ts
+++ b/packages/artalk/src/editor/plugs/emoticons-plug.ts
@@ -14,24 +14,53 @@ type OwOFormatType = {
}
export default class EmoticonsPlug extends EditorPlug {
- public $el!: HTMLElement
+ public static Name = 'emoticons'
+
+ declare protected $panel: HTMLElement
+
public emoticons: EmoticonListData = []
+ public loadingTask: Promise|null = null
public $grpWrap!: HTMLElement
public $grpSwitcher!: HTMLElement
- public loadingTask: Promise|null = null
-
- constructor (public editor: Editor) {
+ public constructor(public editor: Editor) {
super(editor)
- this.$el = Utils.createElement(``)
+ this.registerPanel(``)
+ this.registerBtn(this.ctx.$t('emoticon'))
+
+ // 表情包预加载
+ window.setTimeout(() => {
+ this.loadEmoticonsData()
+ }, 1000) // 延迟 1s 加载
+ }
+
+ public onPanelShow() {
+ ;(async () => {
+ await this.loadEmoticonsData()
+
+ // 初始化元素
+ if (!this.isImgLoaded) {
+ this.initEmoticonsList()
+ this.isImgLoaded = true
+ }
+
+ // 延迟执行,防止无法读取高度
+ setTimeout(() => {
+ this.changeListHeight()
+ }, 30)
+ })()
+ }
+
+ public onPanelHide() {
+ this.$panel.parentElement!.style.height = ''
}
isListLoaded = false
isImgLoaded = false
- async loadEmoticonsData() {
+ public async loadEmoticonsData() {
if (this.isListLoaded) return
if (this.loadingTask !== null) {
await this.loadingTask
@@ -40,23 +69,23 @@ export default class EmoticonsPlug extends EditorPlug {
// 数据处理
this.loadingTask = (async () => {
- Ui.showLoading(this.$el)
+ Ui.showLoading(this.$panel)
this.emoticons = await this.handleData(this.ctx.conf.emoticons)
- Ui.hideLoading(this.$el)
+ Ui.hideLoading(this.$panel)
this.loadingTask = null
this.isListLoaded = true
})()
await this.loadingTask
}
- async handleData(data: any): Promise {
+ private async handleData(data: any): Promise {
if (!Array.isArray(data) && ['object', 'string'].includes(typeof data)) {
data = [data]
}
if (!Array.isArray(data)) {
- Ui.setError(this.$el, "表情包数据必须为 Array/Object/String 类型")
- Ui.hideLoading(this.$el)
+ Ui.setError(this.$panel, "表情包数据必须为 Array/Object/String 类型")
+ Ui.hideLoading(this.$panel)
return []
}
@@ -105,7 +134,7 @@ export default class EmoticonsPlug extends EditorPlug {
}
/** 远程加载 */
- async remoteLoad(url: string): Promise {
+ private async remoteLoad(url: string): Promise {
if (!url) return []
try {
@@ -113,14 +142,14 @@ export default class EmoticonsPlug extends EditorPlug {
const json = await resp.json()
return json
} catch (err) {
- Ui.hideLoading(this.$el)
- Ui.setError(this.$el, `表情加载失败 ${String(err)}`)
+ Ui.hideLoading(this.$panel)
+ Ui.setError(this.$panel, `表情加载失败 ${String(err)}`)
return []
}
}
/** 避免表情 item.key 为 null 的情况 */
- solveNullKey(data: EmoticonGrpData[]) {
+ private solveNullKey(data: EmoticonGrpData[]) {
data.forEach((grp) => {
grp.items.forEach((item, index) => {
if (!item.key) item.key = `${grp.name} ${index+1}`
@@ -129,7 +158,7 @@ export default class EmoticonsPlug extends EditorPlug {
}
/** 避免相同 item.key */
- solveSameKey(data: EmoticonGrpData[]) {
+ private solveSameKey(data: EmoticonGrpData[]) {
const tmp: {[key: string]: number} = {}
data.forEach((grp) => {
grp.items.forEach(item => {
@@ -143,7 +172,7 @@ export default class EmoticonsPlug extends EditorPlug {
}
/** 判断是否为 OwO 格式 */
- isOwOFormat(data: any) {
+ private isOwOFormat(data: any) {
try {
return (typeof data === 'object') && !!Object.values(data).length
&& Array.isArray(Object.keys(Object.values(data)[0].container))
@@ -152,7 +181,7 @@ export default class EmoticonsPlug extends EditorPlug {
}
/** 转换 OwO 格式数据 */
- convertOwO(owoData: OwOFormatType): EmoticonListData {
+ private convertOwO(owoData: OwOFormatType): EmoticonListData {
const dest: EmoticonListData = []
Object.entries(owoData).forEach(([grpName, grp]) => {
const nGrp: EmoticonGrpData = { name: grpName, type: grp.type, items: [] }
@@ -171,10 +200,10 @@ export default class EmoticonsPlug extends EditorPlug {
}
/** 初始化表情列表界面 */
- initEmoticonsList () {
+ private initEmoticonsList() {
// 表情列表
this.$grpWrap = Utils.createElement(``)
- this.$el.append(this.$grpWrap)
+ this.$panel.append(this.$grpWrap)
this.emoticons.forEach((grp, index) => {
const $grp = Utils.createElement(``)
@@ -208,24 +237,25 @@ export default class EmoticonsPlug extends EditorPlug {
})
})
- // 表情分类
- this.$grpSwitcher = Utils.createElement(``)
- this.$el.append(this.$grpSwitcher)
- this.emoticons.forEach((grp, index) => {
- const $item = Utils.createElement('')
- $item.innerText = grp.name
- $item.setAttribute('data-index', String(index))
- $item.onclick = () => (this.openGrp(index))
- this.$grpSwitcher.append($item)
- })
+ // 表情分类切换 bar
+ if (this.emoticons.length > 1) {
+ this.$grpSwitcher = Utils.createElement(``)
+ this.$panel.append(this.$grpSwitcher)
+ this.emoticons.forEach((grp, index) => {
+ const $item = Utils.createElement('')
+ $item.innerText = grp.name
+ $item.setAttribute('data-index', String(index))
+ $item.onclick = () => (this.openGrp(index))
+ this.$grpSwitcher.append($item)
+ })
+ }
// 默认打开第一个分类
- if (this.emoticons.length > 0)
- this.openGrp(0)
+ if (this.emoticons.length > 0) this.openGrp(0)
}
/** 打开一个表情组 */
- openGrp (index: number) {
+ public openGrp(index: number) {
Array.from(this.$grpWrap.children).forEach((item) => {
const el = item as HTMLElement
if (el.getAttribute('data-index') !== String(index)) {
@@ -241,41 +271,13 @@ export default class EmoticonsPlug extends EditorPlug {
this.changeListHeight()
}
- static Name = 'emoticons'
- static BtnHTML = '表情'
-
- getEl () {
- return this.$el
- }
-
- changeListHeight () {
+ private changeListHeight() {
/* const listWrapHeight = Utils.getHeight(this.$grpWrapem)
this.editor.plugWrapEl.style.height = `${listWrapHeight > 150 ? listWrapHeight : 150}px` */
}
- onShow () {
- ;(async () => {
- await this.loadEmoticonsData()
-
- // 初始化元素
- if (!this.isImgLoaded) {
- this.initEmoticonsList()
- this.isImgLoaded = true
- }
-
- // 延迟执行,防止无法读取高度
- setTimeout(() => {
- this.changeListHeight()
- }, 30)
- })()
- }
-
- onHide () {
- this.$el.parentElement!.style.height = ''
- }
-
/** 处理评论 content 中的表情内容 */
- public transEmoticonImageText (text: string) {
+ public transEmoticonImageText(text: string) {
if (!this.emoticons || !Array.isArray(this.emoticons))
return text
diff --git a/packages/artalk/src/editor/plugs/header-input-plug.ts b/packages/artalk/src/editor/plugs/header-input-plug.ts
new file mode 100644
index 000000000..9173ebc89
--- /dev/null
+++ b/packages/artalk/src/editor/plugs/header-input-plug.ts
@@ -0,0 +1,79 @@
+import Api from '@/api'
+import Editor from '../editor'
+import EditorPlug from './editor-plug'
+
+export default class HeaderInputPlug extends EditorPlug {
+ public static Name = 'headerInput'
+
+ public constructor(editor: Editor) {
+ super(editor)
+
+ this.registerHeaderInputEvt((key, $input) => {
+ if (key === 'nick' || key === 'email') {
+ this.fetchUserInfo()
+ }
+ })
+
+ // Link URL 自动补全协议
+ this.editor.$link.addEventListener('change', () => {
+ const link = this.editor.$link.value.trim()
+ if (!!link && !/^(http|https):\/\//.test(link)) {
+ this.editor.$link.value = `https://${link}`
+ this.ctx.user.update({ link: this.editor.$link.value })
+ }
+ })
+ }
+
+ queryUserInfo = {
+ timeout: null,
+ abortFunc: <(() => void)|null>null
+ }
+
+ /** 远程获取用户数据 */
+ fetchUserInfo() {
+ this.ctx.user.logout()
+
+ // 获取用户信息
+ if (this.queryUserInfo.timeout) window.clearTimeout(this.queryUserInfo.timeout) // 清除待发出的请求
+ if (this.queryUserInfo.abortFunc) this.queryUserInfo.abortFunc() // 之前发出未完成的请求立刻中止
+
+ this.queryUserInfo.timeout = window.setTimeout(() => {
+ this.queryUserInfo.timeout = null // 清理
+
+ const {req, abort} = new Api(this.ctx).userGet(
+ this.ctx.user.data.nick, this.ctx.user.data.email
+ )
+ this.queryUserInfo.abortFunc = abort
+ req.then(data => {
+ if (!data.is_login) {
+ this.ctx.user.logout()
+ }
+
+ // 未读消息更新
+ this.ctx.trigger('unread-update', { notifies: data.unread, })
+
+ // 若用户为管理员,执行登陆操作
+ if (this.ctx.user.checkHasBasicUserInfo() && !data.is_login && data.user?.is_admin) {
+ this.showLoginDialog()
+ }
+
+ // 自动填入 link
+ if (data.user && data.user.link) {
+ this.editor.$link.value = data.user.link
+ this.ctx.user.update({ link: data.user.link })
+ }
+ })
+ .catch(() => {})
+ .finally(() => {
+ this.queryUserInfo.abortFunc = null // 清理
+ })
+ }, 400) // 延迟执行,减少请求次数
+ }
+
+ showLoginDialog() {
+ this.ctx.trigger('checker-admin', {
+ onSuccess: () => {
+ }
+ })
+ }
+}
diff --git a/packages/artalk/src/components/editor-plugs/preview-plug.less b/packages/artalk/src/editor/plugs/preview-plug.less
similarity index 100%
rename from packages/artalk/src/components/editor-plugs/preview-plug.less
rename to packages/artalk/src/editor/plugs/preview-plug.less
diff --git a/packages/artalk/src/editor/plugs/preview-plug.ts b/packages/artalk/src/editor/plugs/preview-plug.ts
new file mode 100644
index 000000000..108d6926a
--- /dev/null
+++ b/packages/artalk/src/editor/plugs/preview-plug.ts
@@ -0,0 +1,37 @@
+import './preview-plug.less'
+
+import Editor from '../editor'
+import EditorPlug from './editor-plug'
+
+export default class PreviewPlug extends EditorPlug {
+ public static Name = 'preview'
+ declare protected $panel: HTMLElement
+
+ private isBind = false
+
+ public constructor(editor: Editor) {
+ super(editor)
+
+ this.registerPanel(``)
+ this.registerBtn(`${this.editor.$t('preview')} `)
+ }
+
+ public onPanelShow() {
+ this.updateContent()
+
+ if (!this.isBind) {
+ const event = () => {
+ this.updateContent()
+ }
+ this.editor.$textarea.addEventListener('input', event)
+ this.editor.$textarea.addEventListener('change', event)
+ this.isBind = true
+ }
+ }
+
+ public updateContent() {
+ if (this.$panel.style.display !== 'none') {
+ this.$panel.innerHTML = this.editor.getContentMarked()
+ }
+ }
+}
diff --git a/packages/artalk/src/editor/plugs/upload-plug.ts b/packages/artalk/src/editor/plugs/upload-plug.ts
new file mode 100644
index 000000000..8f0e648ab
--- /dev/null
+++ b/packages/artalk/src/editor/plugs/upload-plug.ts
@@ -0,0 +1,119 @@
+import * as Utils from '@/lib/utils'
+import Api from '@/api'
+import Editor from '../editor'
+import EditorPlug from './editor-plug'
+
+export default class UploadPlug extends EditorPlug {
+ public static Name = 'upload'
+
+ public $imgUploadInput?: HTMLInputElement
+
+ /** 允许的图片格式 */
+ allowImgExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']
+
+ public constructor(editor: Editor) {
+ super(editor)
+
+ this.$imgUploadInput = document.createElement('input')
+ this.$imgUploadInput.type = 'file'
+ this.$imgUploadInput.style.display = 'none'
+ this.$imgUploadInput.accept = this.allowImgExts.map(o => `.${o}`).join(',')
+
+ const $btn = this.registerBtn(`${this.ctx.$t('image')}`)
+ $btn.after(this.$imgUploadInput)
+ $btn.onclick = () => {
+ // 选择图片
+ const $input = this.$imgUploadInput!
+ $input.onchange = () => {
+ (async () => { // 解决阻塞 UI 问题
+ if (!$input.files || $input.files.length === 0) return
+ const file = $input.files[0]
+ this.uploadImg(file)
+ })()
+ }
+ $input.click() // 显示选择图片对话框
+ }
+
+ this.ctx.on('conf-updated', () => {
+ if (!this.ctx.conf.imgUpload) {
+ this.getBtn()!.setAttribute('atk-only-admin-show', '')
+ this.ctx.trigger('check-admin-show-el')
+ }
+ })
+
+ // 统一从 FileList 获取文件并上传图片方法
+ const uploadFromFileList = (files?: FileList) => {
+ if (!files) return
+
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i]
+ this.uploadImg(file)
+ }
+ }
+
+ // 拖拽图片
+ // @link https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
+ // 阻止浏览器的默认释放行为
+ this.editor.$textarea.addEventListener('dragover', (evt) => {
+ evt.stopPropagation()
+ evt.preventDefault()
+ })
+
+ this.editor.$textarea.addEventListener('drop', (evt) => {
+ const files = evt.dataTransfer?.files
+ if (files?.length) {
+ evt.preventDefault()
+ uploadFromFileList(files)
+ }
+ })
+
+ // 粘贴图片
+ this.editor.$textarea.addEventListener('paste', (evt) => {
+ const files = evt.clipboardData?.files
+ if (files?.length) {
+ evt.preventDefault()
+ uploadFromFileList(files)
+ }
+ })
+ }
+
+ public async uploadImg(file: File) {
+ const fileExt = /[^.]+$/.exec(file.name)
+ if (!fileExt || !this.allowImgExts.includes(fileExt[0])) return
+
+ // 未登录提示
+ if (!this.ctx.user.checkHasBasicUserInfo()) {
+ this.editor.showNotify('填入你的名字邮箱才能上传哦', 'w')
+ return
+ }
+
+ // 插入图片前换一行
+ let insertPrefix = '\n'
+ if (this.editor.$textarea.value.trim() === '') insertPrefix = ''
+
+ // 插入占位加载文字
+ const uploadPlaceholderTxt = `${insertPrefix}![](Uploading ${file.name}...)`
+ this.editor.insertContent(uploadPlaceholderTxt)
+
+ // 上传图片
+ let resp: any
+ try {
+ resp = await new Api(this.ctx).imgUpload(file)
+ } catch (err: any) {
+ console.error(err)
+ this.editor.showNotify(`${this.ctx.$t('uploadFail')},${err.msg}`, 'e')
+ }
+ if (!!resp && resp.img_url) {
+ let imgURL = resp.img_url as string
+
+ // 若为相对路径,加上 artalk server
+ if (!Utils.isValidURL(imgURL)) imgURL = Utils.getURLBasedOnApi(this.ctx, imgURL)
+
+ // 上传成功插入图片
+ this.editor.setContent(this.editor.$textarea.value.replace(uploadPlaceholderTxt, `${insertPrefix}![](${imgURL})`))
+ } else {
+ // 上传失败删除加载文字
+ this.editor.setContent(this.editor.$textarea.value.replace(uploadPlaceholderTxt, ''))
+ }
+ }
+}
diff --git a/packages/artalk/src/layer/index.ts b/packages/artalk/src/layer/index.ts
new file mode 100644
index 000000000..ce9f6e3ae
--- /dev/null
+++ b/packages/artalk/src/layer/index.ts
@@ -0,0 +1,4 @@
+import Layer, { GetLayerWrap } from './layer'
+
+export default Layer
+export { GetLayerWrap }
diff --git a/packages/artalk/src/components/layer.ts b/packages/artalk/src/layer/layer.ts
similarity index 94%
rename from packages/artalk/src/components/layer.ts
rename to packages/artalk/src/layer/layer.ts
index 56288e7b0..334eac399 100644
--- a/packages/artalk/src/components/layer.ts
+++ b/packages/artalk/src/layer/layer.ts
@@ -1,4 +1,4 @@
-import Context from '../context'
+import Context from '~/types/context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
@@ -40,19 +40,19 @@ export default class Layer extends Component {
this.$wrap.append(this.$el)
}
- getName () {
+ getName() {
return this.name
}
- getWrapEl () {
+ getWrapEl() {
return this.$wrap
}
- getEl () {
+ getEl() {
return this.$el
}
- show () {
+ show() {
this.fireAllActionTimer()
this.$wrap.style.display = 'block'
@@ -126,7 +126,7 @@ export default class Layer extends Component {
}
/** 销毁 - 无动画 */
- disposeNow () {
+ disposeNow() {
this.$el.remove()
this.pageBodyScrollBarShow()
// this.$el dispose
@@ -134,21 +134,21 @@ export default class Layer extends Component {
}
/** 销毁 */
- dispose () {
+ dispose() {
this.hide()
this.$el.remove()
// this.$el dispose
this.checkCleanLayer()
}
- checkCleanLayer () {
+ checkCleanLayer() {
if (this.getWrapEl().querySelectorAll('.atk-layer-item').length === 0) {
this.$wrap.style.display = 'none'
}
}
}
-export function GetLayerWrap (ctx: Context): { $wrap: HTMLElement, $mask: HTMLElement } {
+export function GetLayerWrap(ctx: Context): { $wrap: HTMLElement, $mask: HTMLElement } {
let $wrap = document.querySelector(`.atk-layer-wrap#ctx-${ctx.cid}`)
if (!$wrap) {
$wrap = Utils.createElement(
diff --git a/packages/artalk/src/components/html/sidebar-layer.html b/packages/artalk/src/layer/sidebar-layer.html
similarity index 100%
rename from packages/artalk/src/components/html/sidebar-layer.html
rename to packages/artalk/src/layer/sidebar-layer.html
diff --git a/packages/artalk/src/components/sidebar-layer.ts b/packages/artalk/src/layer/sidebar-layer.ts
similarity index 73%
rename from packages/artalk/src/components/sidebar-layer.ts
rename to packages/artalk/src/layer/sidebar-layer.ts
index f532b30da..39fe46e9b 100644
--- a/packages/artalk/src/components/sidebar-layer.ts
+++ b/packages/artalk/src/layer/sidebar-layer.ts
@@ -1,11 +1,11 @@
import '../style/sidebar-layer.less'
-import Context from '@/context'
+import Context from '~/types/context'
import Component from '@/lib/component'
import * as Utils from '@/lib/utils'
import * as Ui from '@/lib/ui'
import { SidebarShowPayload } from '~/types/event'
-import SidebarHTML from './html/sidebar-layer.html?raw'
+import SidebarHTML from './sidebar-layer.html?raw'
import Layer from './layer'
import Api from '../api'
@@ -125,7 +125,7 @@ export default class SidebarLayer extends Component {
this.layer?.hide()
}
- public iframeLoad(src: string) {
+ private iframeLoad(src: string) {
if (!this.$iframe) return
this.$iframe.src = src
@@ -135,39 +135,5 @@ export default class SidebarLayer extends Component {
this.$iframe.onload = () => {
Ui.hideLoading(this.$iframeWrap)
}
-
- // this.checkReqStatus(src) // 判不准,删了,没啥用
- }
-
- loadingTimer: number|null = null
-
- // 测试可访问性 (由于 iframe 测不准,需要额外请求)
- public async checkReqStatus(url: string) {
- if (this.loadingTimer !== null) window.clearTimeout(this.loadingTimer)
-
- this.loadingTimer = window.setTimeout(async () => {
- try {
- await fetch(url)
- } catch (err) {
- console.log(err)
- // 请求失败,显示错误提示
- const $errAlert = Utils.createElement(
- `` +
- `
侧边栏似乎打开失败
` +
- `
重新加载 / 取消
` +
- `
`
- )
- const $reloadBtn = $errAlert.querySelector('#AtkReload')!
- const $cancelBtn = $errAlert.querySelector('#AtkCancel')!
- $reloadBtn.onclick = () => {
- this.iframeLoad(url)
- $errAlert.remove()
- }
- $cancelBtn.onclick = () => { // 提供取消按钮,防止误判
- $errAlert.remove()
- }
- this.$iframeWrap.append($errAlert)
- }
- }, 2000) // 2s 后开始检测
}
}
diff --git a/packages/artalk/src/lib/checker/admin-checker.ts b/packages/artalk/src/lib/checker/admin-checker.ts
index 2011ffbcc..0077b9bab 100644
--- a/packages/artalk/src/lib/checker/admin-checker.ts
+++ b/packages/artalk/src/lib/checker/admin-checker.ts
@@ -20,9 +20,10 @@ const AdminChecker: Checker = {
},
onSuccess(that, ctx, userToken, inputVal, formEl) {
- that.ctx.user.data.isAdmin = true
- that.ctx.user.data.token = userToken
- that.ctx.user.save()
+ that.ctx.user.update({
+ isAdmin: true,
+ token: userToken
+ })
that.ctx.trigger('user-changed', that.ctx.user.data)
that.ctx.trigger('list-reload')
},
diff --git a/packages/artalk/src/lib/checker/index.ts b/packages/artalk/src/lib/checker/index.ts
index 31b063c5e..bd1eab021 100644
--- a/packages/artalk/src/lib/checker/index.ts
+++ b/packages/artalk/src/lib/checker/index.ts
@@ -1,7 +1,7 @@
-import Context from '@/context'
-import Layer from '@/components/layer'
-import Dialog from '@/components/dialog'
+import Context from '~/types/context'
import { CheckerPayload } from '~/types/event'
+import Dialog from '@/components/dialog'
+import Layer from '@/layer'
import * as Utils from '../utils'
import * as Ui from '../ui'
import CaptchaChecker from './captcha-checker'
diff --git a/packages/artalk/src/lib/component.ts b/packages/artalk/src/lib/component.ts
index e30798820..9231979bf 100644
--- a/packages/artalk/src/lib/component.ts
+++ b/packages/artalk/src/lib/component.ts
@@ -1,8 +1,8 @@
import ArtalkConfig from '~/types/artalk-config'
-import Context from '../context'
+import Context from '~/types/context'
import { I18n } from '../i18n'
-export default class Component {
+export default abstract class Component {
public $el!: HTMLElement
public ctx: Context
diff --git a/packages/artalk/src/lib/ui.ts b/packages/artalk/src/lib/ui.ts
index f8950e6bc..1e627177b 100644
--- a/packages/artalk/src/lib/ui.ts
+++ b/packages/artalk/src/lib/ui.ts
@@ -1,10 +1,7 @@
import * as Utils from './utils'
-import Context from '../context'
/** 显示加载 */
-export function showLoading(parentElem: HTMLElement|Context, conf?: { transparentBg?: boolean }) {
- if (parentElem instanceof Context) parentElem = parentElem.$root
-
+export function showLoading(parentElem: HTMLElement, conf?: { transparentBg?: boolean }) {
let $loading = parentElem.querySelector('.atk-loading')
if (!$loading) {
$loading = Utils.createElement(
@@ -29,9 +26,7 @@ export function showLoading(parentElem: HTMLElement|Context, conf?: { transparen
}
/** 隐藏加载 */
-export function hideLoading(parentElem: HTMLElement|Context) {
- if (parentElem instanceof Context) parentElem = parentElem.$root
-
+export function hideLoading(parentElem: HTMLElement) {
const $loading = parentElem.querySelector('.atk-loading')
if ($loading) $loading.style.display = 'none'
}
@@ -133,9 +128,7 @@ export function playFadeOutAnim(elem: HTMLElement, after?: () => void) {
}
/** 设置全局错误 */
-export function setError(parentElem: HTMLElement|Context, html: string | HTMLElement | null, title: string = 'Artalk Error') {
- if (parentElem instanceof Context) parentElem = parentElem.$root
-
+export function setError(parentElem: HTMLElement, html: string | HTMLElement | null, title: string = 'Artalk Error') {
let elem = parentElem.querySelector('.atk-error-layer')
if (html === null) {
if (elem !== null) elem.remove()
diff --git a/packages/artalk/src/lib/user.ts b/packages/artalk/src/lib/user.ts
index 12ca79b6b..1ed26c0f8 100644
--- a/packages/artalk/src/lib/user.ts
+++ b/packages/artalk/src/lib/user.ts
@@ -1,9 +1,13 @@
import ArtalkConfig, { LocalUser } from "~/types/artalk-config"
+import Context from '~/types/context'
export default class User {
+ public ctx: Context
public data: LocalUser
- constructor (conf: ArtalkConfig) {
+ public constructor(ctx: Context) {
+ this.ctx = ctx
+
// 从 localStorage 导入
const localUser = JSON.parse(window.localStorage.getItem('ArtalkUser') || '{}')
this.data = {
@@ -16,12 +20,25 @@ export default class User {
}
/** 保存用户到 localStorage 中 */
- public save () {
+ public update(obj: Partial = {}) {
+ Object.entries(obj).forEach(([key, value]) => {
+ this.data[key] = value
+ })
+
window.localStorage.setItem('ArtalkUser', JSON.stringify(this.data))
+ this.ctx.trigger('user-changed', this.ctx.user.data)
+ }
+
+ /** 注销,清除用户登录状态 */
+ public logout() {
+ this.update({
+ token: '',
+ isAdmin: false
+ })
}
/** 是否已填写基本用户信息 */
- public checkHasBasicUserInfo () {
+ public checkHasBasicUserInfo() {
return !!this.data.nick && !!this.data.email
}
}
diff --git a/packages/artalk/src/lib/utils.ts b/packages/artalk/src/lib/utils.ts
index 8ee1a54ee..ce99593c5 100644
--- a/packages/artalk/src/lib/utils.ts
+++ b/packages/artalk/src/lib/utils.ts
@@ -1,7 +1,7 @@
import { marked as libMarked } from 'marked'
import insane from 'insane'
import hanabi from 'hanabi'
-import Context from '../context'
+import Context from '~/types/context'
export function createElement(htmlStr: string = ''): E {
const div = document.createElement('div')
@@ -118,7 +118,7 @@ export function onImagesLoaded($container: HTMLElement, event: Function) {
}
export function getGravatarURL(ctx: Context, emailMD5: string) {
- return `${(ctx.conf.gravatar?.mirror || '').replace(/\/$/, '')}/${emailMD5}?d=${encodeURIComponent(ctx.conf.gravatar?.default || '')}&s=80`
+ return `${(ctx.conf.gravatar.mirror).replace(/\/$/, '')}/${emailMD5}?d=${encodeURIComponent(ctx.conf.gravatar.default)}&s=80`
}
export function sleep(ms: number) {
@@ -142,6 +142,8 @@ export function versionCompare(a: string, b: string) {
/** 初始化 marked */
export function initMarked(ctx: Context) {
+ if (!libMarked) return
+
const renderer = new libMarked.Renderer()
const orgLinkRenderer = renderer.link
renderer.link = (href, title, text) => {
@@ -186,9 +188,11 @@ export function initMarked(ctx: Context) {
/** 解析 markdown */
export function marked(ctx: Context, src: string): string {
+ const rawContent = ctx.markedInstance?.parse(src) || src
+
// @link https://github.com/markedjs/marked/discussions/1232
// @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225
- let dest = insane(ctx.markedInstance.parse(src), {
+ let dest = insane(rawContent, {
allowedClasses: {},
allowedSchemes: ['http', 'https', 'mailto'],
allowedTags: [
diff --git a/packages/artalk/src/list/index.ts b/packages/artalk/src/list/index.ts
new file mode 100644
index 000000000..6b945ef8f
--- /dev/null
+++ b/packages/artalk/src/list/index.ts
@@ -0,0 +1,3 @@
+import List from './list'
+
+export default List
diff --git a/packages/artalk/src/components/list-lite.ts b/packages/artalk/src/list/list-lite.ts
similarity index 91%
rename from packages/artalk/src/components/list-lite.ts
rename to packages/artalk/src/list/list-lite.ts
index 8d8e1d274..3ba85f5a4 100644
--- a/packages/artalk/src/components/list-lite.ts
+++ b/packages/artalk/src/list/list-lite.ts
@@ -1,12 +1,12 @@
-import { ListData, CommentData, NotifyData, ApiVersionData } from '~/types/artalk-data'
-import Context from '../context'
+import { ListData, CommentData, NotifyData } from '~/types/artalk-data'
+import Context from '~/types/context'
import Component from '../lib/component'
import * as Utils from '../lib/utils'
import * as Ui from '../lib/ui'
import Api from '../api'
-import Comment from './comment'
-import Pagination from './pagination'
-import ReadMoreBtn from './read-more-btn'
+import Comment from '../comment'
+import Pagination from '../components/pagination'
+import ReadMoreBtn from '../components/read-more-btn'
import * as ListNest from './list-nest'
import { backendMinVersion } from '../../package.json'
@@ -14,7 +14,7 @@ export default class ListLite extends Component {
protected $commentsWrap: HTMLElement
protected commentList: Comment[] = [] // Note: 无层级结构 + 无须排列
- protected get commentDataList() { return this.commentList.map(c => c.data) }
+ protected get commentDataList() { return this.commentList.map(c => c.getData()) }
protected data?: ListData
protected isLoading: boolean = false
@@ -184,7 +184,7 @@ export default class ListLite extends Component {
this.$el.append(this.readMoreBtn.$el)
// 滚动到底部自动加载
- if (this.conf.pagination?.autoLoad) {
+ if (this.conf.pagination.autoLoad) {
// 添加滚动事件监听
const at = this.scrollListenerAt || document
if (this.autoLoadScrollEvent) at.removeEventListener('scroll', this.autoLoadScrollEvent) // 解除原有
@@ -296,18 +296,17 @@ export default class ListLite extends Component {
protected createComment(cData: CommentData, ctxData?: CommentData[]) {
if (!ctxData) ctxData = this.commentDataList
- const comment = new Comment(this.ctx, cData)
- comment.flatMode = this.flatMode
- comment.afterRender = () => {
- if (this.renderComment) this.renderComment(comment)
- }
- comment.onDelete = (c) => {
- this.deleteComment(c)
- this.refreshUI()
- }
-
- // 子评论查找回复对象
- comment.replyTo = (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
+ const comment = new Comment(this.ctx, cData, {
+ isFlatMode: this.flatMode,
+ afterRender: () => {
+ if (this.renderComment) this.renderComment(comment)
+ },
+ onDelete: (c: Comment) => {
+ this.deleteComment(c)
+ this.refreshUI()
+ },
+ replyTo: (cData.rid ? ctxData.find(c => c.id === cData.rid) : undefined)
+ })
// 渲染元素
comment.render()
@@ -338,7 +337,7 @@ export default class ListLite extends Component {
// 显示并播放渐入动画
this.$commentsWrap.appendChild(rootC.getEl())
- rootC.playFadeInAnim()
+ rootC.getRender().playFadeAnim()
// 加载子评论
const that = this
@@ -356,7 +355,7 @@ export default class ListLite extends Component {
})(rootC, rootNode)
// 限高检测
- rootC.checkHeightLimit()
+ rootC.getRender().checkHeightLimit()
})
}
@@ -370,11 +369,11 @@ export default class ListLite extends Component {
if (cData.visible) {
if (insertMode === 'append') this.$commentsWrap.append(comment.getEl())
if (insertMode === 'prepend') this.$commentsWrap.prepend(comment.getEl())
- comment.playFadeInAnim()
+ comment.getRender().playFadeAnim()
}
// 平铺评论插入后 · 内容限高检测
- comment.checkHeightLimit()
+ comment.getRender().checkHeightLimit()
return comment
}
@@ -396,15 +395,15 @@ export default class ListLite extends Component {
// 若父评论存在 “子评论部分” 限高,取消限高
comment.getParents().forEach((p) => {
- if (p.$children) p.heightLimitRemove(p.$children)
+ p.getRender().heightLimitRemoveForChildren()
})
}
}
- comment.checkHeightLimit()
+ comment.getRender().checkHeightLimit()
Ui.scrollIntoView(comment.getEl()) // 滚动到可以见
- comment.playFadeInAnim() // 播放评论渐出动画
+ comment.getRender().playFadeAnim() // 播放评论渐出动画
} else {
// 平铺模式
const comment = this.putCommentFlatMode(commentData, this.commentDataList, 'prepend')
@@ -421,7 +420,7 @@ export default class ListLite extends Component {
/** 查找评论项 */
protected findComment(id: number): Comment|undefined {
- return this.commentList.find(c => c.data.id === id)
+ return this.commentList.find(c => c.getData().id === id)
}
/** 删除评论 */
@@ -455,18 +454,18 @@ export default class ListLite extends Component {
// 高亮评论
if (this.unreadHighlight === true) {
this.commentList.forEach((comment) => {
- const notify = this.unread.find(o => o.comment_id === comment.data.id)
+ const notify = this.unread.find(o => o.comment_id === comment.getID())
if (notify) {
- comment.setUnread(true)
- comment.setOpenURL(notify.read_link)
- comment.openEvt = () => {
- this.unread = this.unread.filter(o => o.comment_id !== comment.data.id) // remove
+ comment.getRender().setUnread(true)
+ comment.getRender().setOpenAction(() => {
+ window.open(notify.read_link)
+ this.unread = this.unread.filter(o => o.comment_id !== comment.getID()) // remove
this.ctx.trigger('unread-update', {
notifies: this.unread
})
- }
+ })
} else {
- comment.setUnread(false)
+ comment.getRender().setUnread(false)
}
})
}
diff --git a/packages/artalk/src/components/list-nest.ts b/packages/artalk/src/list/list-nest.ts
similarity index 100%
rename from packages/artalk/src/components/list-nest.ts
rename to packages/artalk/src/list/list-nest.ts
diff --git a/packages/artalk/src/components/html/list.html b/packages/artalk/src/list/list.html
similarity index 91%
rename from packages/artalk/src/components/html/list.html
rename to packages/artalk/src/list/list.html
index 178b4af72..e8099b331 100644
--- a/packages/artalk/src/components/html/list.html
+++ b/packages/artalk/src/list/list.html
@@ -7,7 +7,7 @@
- 通知中心
+