artalk 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -368,6 +368,29 @@ let Api$1 = class Api extends HttpClient {
368
368
  type: "application/json",
369
369
  format: "json"
370
370
  }, params)),
371
+ /**
372
+ * @description Get the detail of a comment by comment id
373
+ *
374
+ * @tags Comment
375
+ * @name GetComment
376
+ * @summary Get a comment
377
+ * @request GET:/comments/{id}
378
+ * @response `200` `HandlerResponseCommentGet` OK
379
+ * @response `404` `(HandlerMap & {
380
+ msg?: string,
381
+
382
+ })` Not Found
383
+ * @response `500` `(HandlerMap & {
384
+ msg?: string,
385
+
386
+ })` Internal Server Error
387
+ */
388
+ getComment: (id, params = {}) => this.request(__spreadValues({
389
+ path: `/comments/${id}`,
390
+ method: "GET",
391
+ type: "application/json",
392
+ format: "json"
393
+ }, params)),
371
394
  /**
372
395
  * @description Update a specific comment
373
396
  *
@@ -2449,7 +2472,7 @@ const en = {
2449
2472
  link: "Website",
2450
2473
  emoticon: "Emoji",
2451
2474
  preview: "Preview",
2452
- image: "Image",
2475
+ uploadImage: "Upload Image",
2453
2476
  uploadFail: "Upload Failed",
2454
2477
  commentFail: "Failed to comment",
2455
2478
  restoredMsg: "Content has been restored",
@@ -2522,14 +2545,14 @@ const zhCN = {
2522
2545
  /* Editor */
2523
2546
  placeholder: "键入内容...",
2524
2547
  noComment: "「此时无声胜有声」",
2525
- send: "发送评论",
2526
- save: "保存评论",
2548
+ send: "发送",
2549
+ save: "保存",
2527
2550
  nick: "昵称",
2528
2551
  email: "邮箱",
2529
2552
  link: "网址",
2530
2553
  emoticon: "表情",
2531
2554
  preview: "预览",
2532
- image: "图片",
2555
+ uploadImage: "上传图片",
2533
2556
  uploadFail: "上传失败",
2534
2557
  commentFail: "评论失败",
2535
2558
  restoredMsg: "内容已自动恢复",
@@ -3131,7 +3154,7 @@ function showLoading(parentElem, conf) {
3131
3154
  let $loading = parentElem.querySelector(":scope > .atk-loading");
3132
3155
  if (!$loading) {
3133
3156
  $loading = createElement(
3134
- `<div class="atk-loading atk-fade-in" style="display: none;">
3157
+ `<div class="atk-loading" style="display: none;">
3135
3158
  <div class="atk-loading-spinner">
3136
3159
  <svg viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle></svg>
3137
3160
  </div>
@@ -3146,7 +3169,8 @@ function showLoading(parentElem, conf) {
3146
3169
  if ($spinner) {
3147
3170
  $spinner.style.display = "none";
3148
3171
  window.setTimeout(() => {
3149
- $spinner.style.display = "";
3172
+ if ($spinner.isConnected)
3173
+ $spinner.style.display = "";
3150
3174
  }, 500);
3151
3175
  }
3152
3176
  }
@@ -3166,8 +3190,7 @@ function scrollIntoView(elem, enableAnim = true, relativeTo) {
3166
3190
  if (relativeTo) {
3167
3191
  const containerRect = relativeTo.getBoundingClientRect();
3168
3192
  const elementRect = elem.getBoundingClientRect();
3169
- top = elementRect.top - containerRect.top + relativeTo.scrollTop;
3170
- top += getHeight(relativeTo) / 2 - getHeight(elem) / 2;
3193
+ top = elementRect.top - containerRect.top + relativeTo.scrollTop - relativeTo.clientHeight / 2 + elem.clientHeight / 2;
3171
3194
  } else {
3172
3195
  const rect = elem.getBoundingClientRect();
3173
3196
  const elemTop = rect.top + window.scrollY;
@@ -3501,8 +3524,11 @@ class Component {
3501
3524
  get conf() {
3502
3525
  return this.ctx.conf;
3503
3526
  }
3527
+ getEl() {
3528
+ return this.$el;
3529
+ }
3504
3530
  }
3505
- const EditorHTML = '<div class="atk-main-editor">\n <div class="atk-header">\n <input name="nick" class="atk-nick" type="text" required="required">\n <input name="email" class="atk-email" type="email" required="required">\n <input name="link" class="atk-link" type="url">\n </div>\n <div class="atk-textarea-wrap">\n <textarea class="atk-textarea"></textarea>\n </div>\n <div class="atk-plug-panel-wrap" style="display: none;"></div>\n <div class="atk-bottom">\n <div class="atk-item atk-plug-btn-wrap"></div>\n <div class="atk-item">\n <button type="button" class="atk-send-btn"></button>\n </div>\n </div>\n <div class="atk-notify-wrap"></div>\n</div>\n';
3531
+ const EditorHTML = '<div class="atk-main-editor">\n <div class="atk-header">\n <input name="nick" class="atk-nick" type="text" required="required">\n <input name="email" class="atk-email" type="email" required="required">\n <input name="link" class="atk-link" type="url">\n </div>\n <div class="atk-textarea-wrap">\n <textarea class="atk-textarea"></textarea>\n </div>\n <div class="atk-plug-panel-wrap" style="display: none;"></div>\n <div class="atk-bottom">\n <div class="atk-item atk-bottom-left">\n <span class="atk-state-wrap"></span>\n <span class="atk-plug-btn-wrap"></span>\n </div>\n <div class="atk-item">\n <button type="button" class="atk-send-btn"></button>\n </div>\n </div>\n <div class="atk-notify-wrap"></div>\n</div>\n';
3506
3532
  const Sel = {
3507
3533
  $header: ".atk-header",
3508
3534
  $nick: '.atk-header [name="nick"]',
@@ -3513,6 +3539,8 @@ const Sel = {
3513
3539
  $bottom: ".atk-bottom",
3514
3540
  $submitBtn: ".atk-send-btn",
3515
3541
  $notifyWrap: ".atk-notify-wrap",
3542
+ $bottomLeft: ".atk-bottom-left",
3543
+ $stateWrap: ".atk-state-wrap",
3516
3544
  $plugBtnWrap: ".atk-plug-btn-wrap",
3517
3545
  $plugPanelWrap: ".atk-plug-panel-wrap"
3518
3546
  };
@@ -3885,39 +3913,18 @@ function makeNestCommentNodeList(srcData, sortBy = "DATE_DESC", nestMax = 2) {
3885
3913
  })(nodeList);
3886
3914
  return nodeList;
3887
3915
  }
3888
- class ListLayout {
3889
- constructor(options) {
3890
- this.options = options;
3891
- }
3892
- // TODO: refactor `if syntax` to strategy pattern
3893
- import(comments) {
3894
- if (this.options.flatMode) {
3895
- comments.forEach((commentData) => {
3896
- this.putCommentFlatMode(commentData, comments, "append");
3897
- });
3898
- } else {
3899
- this.importCommentsNestMode(comments);
3900
- }
3901
- }
3902
- insert(comment) {
3903
- if (!this.options.flatMode) {
3904
- this.insertCommentNest(comment);
3905
- } else {
3906
- this.insertCommentFlatMode(comment);
3907
- }
3908
- }
3909
- // 导入评论 · 嵌套模式
3910
- importCommentsNestMode(srcData) {
3911
- const rootNodes = makeNestCommentNodeList(srcData, this.options.nestSortBy, this.options.nestMax);
3916
+ const createNestStrategy = (opts) => ({
3917
+ import: (comments) => {
3918
+ const rootNodes = makeNestCommentNodeList(comments, opts.nestSortBy, opts.nestMax);
3912
3919
  rootNodes.forEach((rootNode) => {
3913
3920
  var _a;
3914
- const rootC = this.options.createCommentNode(rootNode.comment, srcData);
3915
- (_a = this.options.$commentsWrap) == null ? void 0 : _a.appendChild(rootC.getEl());
3921
+ const rootC = opts.createCommentNode(rootNode.comment);
3922
+ (_a = opts.$commentsWrap) == null ? void 0 : _a.appendChild(rootC.getEl());
3916
3923
  rootC.getRender().playFadeAnim();
3917
3924
  const loadChildren = (parentC, parentNode) => {
3918
3925
  parentNode.children.forEach((node) => {
3919
3926
  const childD = node.comment;
3920
- const childC = this.options.createCommentNode(childD, srcData);
3927
+ const childC = opts.createCommentNode(childD, parentC.getData());
3921
3928
  parentC.putChild(childC);
3922
3929
  loadChildren(childC, node);
3923
3930
  });
@@ -3925,44 +3932,66 @@ class ListLayout {
3925
3932
  loadChildren(rootC, rootNode);
3926
3933
  rootC.getRender().checkHeightLimit();
3927
3934
  });
3928
- }
3929
- /** 导入评论 · 平铺模式 */
3930
- putCommentFlatMode(cData, ctxData, insertMode) {
3931
- var _a, _b;
3932
- if (cData.is_collapsed)
3933
- cData.is_allow_reply = false;
3934
- const comment = this.options.createCommentNode(cData, ctxData);
3935
- if (cData.visible) {
3936
- if (insertMode === "append")
3937
- (_a = this.options.$commentsWrap) == null ? void 0 : _a.append(comment.getEl());
3938
- if (insertMode === "prepend")
3939
- (_b = this.options.$commentsWrap) == null ? void 0 : _b.prepend(comment.getEl());
3940
- comment.getRender().playFadeAnim();
3941
- }
3942
- comment.getRender().checkHeightLimit();
3943
- return comment;
3944
- }
3945
- insertCommentNest(commentData) {
3935
+ },
3936
+ insert: (comment, replyComment) => {
3946
3937
  var _a;
3947
- const comment = this.options.createCommentNode(commentData, this.options.getCommentDataList());
3948
- if (commentData.rid === 0) {
3949
- (_a = this.options.$commentsWrap) == null ? void 0 : _a.prepend(comment.getEl());
3938
+ const node = opts.createCommentNode(comment, replyComment);
3939
+ if (comment.rid === 0) {
3940
+ (_a = opts.$commentsWrap) == null ? void 0 : _a.prepend(node.getEl());
3950
3941
  } else {
3951
- const parent = this.options.findCommentNode(commentData.rid);
3942
+ const parent = opts.findCommentNode(comment.rid);
3952
3943
  if (parent) {
3953
- parent.putChild(comment, this.options.nestSortBy === "DATE_ASC" ? "append" : "prepend");
3954
- comment.getParents().forEach((p) => {
3944
+ parent.putChild(node, opts.nestSortBy === "DATE_ASC" ? "append" : "prepend");
3945
+ node.getParents().forEach((p) => {
3955
3946
  p.getRender().heightLimitRemoveForChildren();
3956
3947
  });
3957
3948
  }
3958
3949
  }
3959
- comment.getRender().checkHeightLimit();
3960
- scrollIntoView(comment.getEl());
3961
- comment.getRender().playFadeAnim();
3950
+ node.getRender().checkHeightLimit();
3951
+ node.scrollIntoView();
3952
+ node.getRender().playFadeAnim();
3953
+ }
3954
+ });
3955
+ const createFlatStrategy = (opts) => ({
3956
+ import: (comments) => {
3957
+ comments.forEach((comment) => {
3958
+ const replyComment = comment.rid === 0 ? void 0 : comments.find((c) => c.id === comment.rid);
3959
+ insertComment(opts, "append", comment, replyComment);
3960
+ });
3961
+ },
3962
+ insert: (comment, replyComment) => {
3963
+ const node = insertComment(opts, "prepend", comment, replyComment);
3964
+ node.scrollIntoView();
3965
+ }
3966
+ });
3967
+ function insertComment(opts, insertMode, comment, replyComment) {
3968
+ if (comment.is_collapsed)
3969
+ comment.is_allow_reply = false;
3970
+ const node = opts.createCommentNode(comment, replyComment);
3971
+ if (comment.visible) {
3972
+ const $comment = node.getEl();
3973
+ const $listCommentsWrap = opts.$commentsWrap;
3974
+ if (insertMode === "append")
3975
+ $listCommentsWrap == null ? void 0 : $listCommentsWrap.append($comment);
3976
+ if (insertMode === "prepend")
3977
+ $listCommentsWrap == null ? void 0 : $listCommentsWrap.prepend($comment);
3978
+ node.getRender().playFadeAnim();
3979
+ }
3980
+ node.getRender().checkHeightLimit();
3981
+ return node;
3982
+ }
3983
+ class ListLayout {
3984
+ constructor(options) {
3985
+ this.options = options;
3962
3986
  }
3963
- insertCommentFlatMode(commentData) {
3964
- const comment = this.putCommentFlatMode(commentData, this.options.getCommentDataList(), "prepend");
3965
- scrollIntoView(comment.getEl());
3987
+ getStrategy() {
3988
+ return this.options.flatMode ? createFlatStrategy(this.options) : createNestStrategy(this.options);
3989
+ }
3990
+ import(comments) {
3991
+ this.getStrategy().import(comments);
3992
+ }
3993
+ insert(comment, replyComment) {
3994
+ this.getStrategy().insert(comment, replyComment);
3966
3995
  }
3967
3996
  }
3968
3997
  function Detect(userAgent) {
@@ -4225,7 +4254,7 @@ const CommentHTML = '<div class="atk-comment-wrap">\n <div class="atk-comment">
4225
4254
  function renderAvatar(r) {
4226
4255
  const $avatar = r.$el.querySelector(".atk-avatar");
4227
4256
  const $avatarImg = createElement("<img />");
4228
- const avatarURLBuilder = r.conf.avatarURLBuilder;
4257
+ const avatarURLBuilder = r.opts.avatarURLBuilder;
4229
4258
  $avatarImg.src = avatarURLBuilder ? avatarURLBuilder(r.data) : r.comment.getGravatarURL();
4230
4259
  if (r.data.link) {
4231
4260
  const $avatarA = createElement('<a target="_blank" rel="noreferrer noopener nofollow"></a>');
@@ -4264,12 +4293,12 @@ function renderVerifyBadge(ctx) {
4264
4293
  const badgeColor = ctx.data.badge_color;
4265
4294
  if (badgeText) {
4266
4295
  const $badge = createElement(`<span class="atk-badge"></span>`);
4267
- $badge.innerText = badgeText.replace("管理员", ctx.ctx.$t("admin"));
4296
+ $badge.innerText = badgeText.replace("管理员", t("admin"));
4268
4297
  $badge.style.backgroundColor = badgeColor || "";
4269
4298
  ctx.$headerBadgeWrap.append($badge);
4270
4299
  }
4271
4300
  if (ctx.data.is_pinned) {
4272
- const $pinnedBadge = createElement(`<span class="atk-pinned-badge">${ctx.ctx.$t("pin")}</span>`);
4301
+ const $pinnedBadge = createElement(`<span class="atk-pinned-badge">${t("pin")}</span>`);
4273
4302
  ctx.$headerBadgeWrap.append($pinnedBadge);
4274
4303
  }
4275
4304
  }
@@ -4279,7 +4308,7 @@ function renderDate(ctx) {
4279
4308
  $date.setAttribute("data-atk-comment-date", String(+new Date(ctx.data.date)));
4280
4309
  }
4281
4310
  function renderUABadge(ctx) {
4282
- if (!ctx.ctx.conf.uaBadge && !ctx.data.ip_region)
4311
+ if (!ctx.opts.uaBadge && !ctx.data.ip_region)
4283
4312
  return;
4284
4313
  let $uaWrap = ctx.$header.querySelector("atk-ua-wrap");
4285
4314
  if (!$uaWrap) {
@@ -4292,7 +4321,7 @@ function renderUABadge(ctx) {
4292
4321
  $regionBadge.innerText = ctx.data.ip_region;
4293
4322
  $uaWrap.append($regionBadge);
4294
4323
  }
4295
- if (ctx.ctx.conf.uaBadge) {
4324
+ if (ctx.opts.uaBadge) {
4296
4325
  const { browser, os } = ctx.comment.getUserUA();
4297
4326
  if (String(browser).trim()) {
4298
4327
  const $uaBrowser = createElement(`<span class="atk-ua ua-browser"></span>`);
@@ -4315,8 +4344,8 @@ function renderContent(r) {
4315
4344
  r.$content.classList.add("atk-hide", "atk-type-collapsed");
4316
4345
  const collapsedInfoEl = createElement(`
4317
4346
  <div class="atk-collapsed">
4318
- <span class="atk-text">${r.ctx.$t("collapsedMsg")}</span>
4319
- <span class="atk-show-btn">${r.ctx.$t("expand")}</span>
4347
+ <span class="atk-text">${t("collapsedMsg")}</span>
4348
+ <span class="atk-show-btn">${t("expand")}</span>
4320
4349
  </div>`);
4321
4350
  r.$body.insertAdjacentElement("beforeend", collapsedInfoEl);
4322
4351
  const contentShowBtn = collapsedInfoEl.querySelector(".atk-show-btn");
@@ -4326,51 +4355,51 @@ function renderContent(r) {
4326
4355
  r.$content.innerHTML = r.comment.getContentMarked();
4327
4356
  r.$content.classList.remove("atk-hide");
4328
4357
  playFadeInAnim(r.$content);
4329
- contentShowBtn.innerText = r.ctx.$t("collapse");
4358
+ contentShowBtn.innerText = t("collapse");
4330
4359
  } else {
4331
4360
  r.$content.innerHTML = "";
4332
4361
  r.$content.classList.add("atk-hide");
4333
- contentShowBtn.innerText = r.ctx.$t("expand");
4362
+ contentShowBtn.innerText = t("expand");
4334
4363
  }
4335
4364
  });
4336
4365
  }
4337
4366
  function renderReplyAt(r) {
4338
- if (r.cConf.isFlatMode || r.data.rid === 0)
4367
+ if (r.opts.flatMode || r.data.rid === 0)
4339
4368
  return;
4340
- if (!r.cConf.replyTo)
4369
+ if (!r.opts.replyTo)
4341
4370
  return;
4342
4371
  r.$replyAt = createElement(`<span class="atk-item atk-reply-at"><span class="atk-arrow"></span><span class="atk-nick"></span></span>`);
4343
- r.$replyAt.querySelector(".atk-nick").innerText = `${r.cConf.replyTo.nick}`;
4372
+ r.$replyAt.querySelector(".atk-nick").innerText = `${r.opts.replyTo.nick}`;
4344
4373
  r.$replyAt.onclick = () => {
4345
4374
  r.comment.getActions().goToReplyComment();
4346
4375
  };
4347
4376
  r.$headerBadgeWrap.insertAdjacentElement("afterend", r.$replyAt);
4348
4377
  }
4349
4378
  function renderReplyTo(r) {
4350
- if (!r.cConf.isFlatMode)
4379
+ if (!r.opts.flatMode)
4351
4380
  return;
4352
- if (!r.cConf.replyTo)
4381
+ if (!r.opts.replyTo)
4353
4382
  return;
4354
4383
  r.$replyTo = createElement(`
4355
4384
  <div class="atk-reply-to">
4356
- <div class="atk-meta">${r.ctx.$t("reply")} <span class="atk-nick"></span>:</div>
4385
+ <div class="atk-meta">${t("reply")} <span class="atk-nick"></span>:</div>
4357
4386
  <div class="atk-content"></div>
4358
4387
  </div>`);
4359
4388
  const $nick = r.$replyTo.querySelector(".atk-nick");
4360
- $nick.innerText = `@${r.cConf.replyTo.nick}`;
4389
+ $nick.innerText = `@${r.opts.replyTo.nick}`;
4361
4390
  $nick.onclick = () => {
4362
4391
  r.comment.getActions().goToReplyComment();
4363
4392
  };
4364
- let replyContent = marked(r.cConf.replyTo.content);
4365
- if (r.cConf.replyTo.is_collapsed)
4366
- replyContent = `[${r.ctx.$t("collapsed")}]`;
4393
+ let replyContent = marked(r.opts.replyTo.content);
4394
+ if (r.opts.replyTo.is_collapsed)
4395
+ replyContent = `[${t("collapsed")}]`;
4367
4396
  r.$replyTo.querySelector(".atk-content").innerHTML = replyContent;
4368
4397
  r.$body.prepend(r.$replyTo);
4369
4398
  }
4370
4399
  function renderPending(r) {
4371
4400
  if (!r.data.is_pending)
4372
4401
  return;
4373
- const pendingEl = createElement(`<div class="atk-pending">${r.ctx.$t("pendingMsg")}</div>`);
4402
+ const pendingEl = createElement(`<div class="atk-pending">${t("pendingMsg")}</div>`);
4374
4403
  r.$body.prepend(pendingEl);
4375
4404
  }
4376
4405
  class ActionBtn {
@@ -4523,14 +4552,14 @@ function renderActions(r) {
4523
4552
  });
4524
4553
  }
4525
4554
  function renderVote(r) {
4526
- if (!r.ctx.conf.vote)
4555
+ if (!r.opts.vote)
4527
4556
  return;
4528
- r.voteBtnUp = new ActionBtn(() => `${r.ctx.$t("voteUp")} (${r.data.vote_up || 0})`).appendTo(r.$actions);
4557
+ r.voteBtnUp = new ActionBtn(() => `${t("voteUp")} (${r.data.vote_up || 0})`).appendTo(r.$actions);
4529
4558
  r.voteBtnUp.setClick(() => {
4530
4559
  r.comment.getActions().vote("up");
4531
4560
  });
4532
- if (r.ctx.conf.voteDown) {
4533
- r.voteBtnDown = new ActionBtn(() => `${r.ctx.$t("voteDown")} (${r.data.vote_down || 0})`).appendTo(r.$actions);
4561
+ if (r.opts.voteDown) {
4562
+ r.voteBtnDown = new ActionBtn(() => `${t("voteDown")} (${r.data.vote_down || 0})`).appendTo(r.$actions);
4534
4563
  r.voteBtnDown.setClick(() => {
4535
4564
  r.comment.getActions().vote("down");
4536
4565
  });
@@ -4539,20 +4568,16 @@ function renderVote(r) {
4539
4568
  function renderReply(r) {
4540
4569
  if (!r.data.is_allow_reply)
4541
4570
  return;
4542
- const replyBtn = createElement(`<span>${r.ctx.$t("reply")}</span>`);
4571
+ const replyBtn = createElement(`<span>${t("reply")}</span>`);
4543
4572
  r.$actions.append(replyBtn);
4544
4573
  replyBtn.addEventListener("click", (e) => {
4545
4574
  e.stopPropagation();
4546
- if (!r.cConf.onReplyBtnClick) {
4547
- r.ctx.replyComment(r.data, r.$el);
4548
- } else {
4549
- r.cConf.onReplyBtnClick();
4550
- }
4575
+ r.opts.replyComment(r.data, r.$el);
4551
4576
  });
4552
4577
  }
4553
4578
  function renderCollapse(r) {
4554
4579
  const collapseBtn = new ActionBtn({
4555
- text: () => r.data.is_collapsed ? r.ctx.$t("expand") : r.ctx.$t("collapse"),
4580
+ text: () => r.data.is_collapsed ? t("expand") : t("collapse"),
4556
4581
  adminOnly: true
4557
4582
  });
4558
4583
  collapseBtn.appendTo(r.$actions);
@@ -4562,7 +4587,7 @@ function renderCollapse(r) {
4562
4587
  }
4563
4588
  function renderModerator(r) {
4564
4589
  const pendingBtn = new ActionBtn({
4565
- text: () => r.data.is_pending ? r.ctx.$t("pending") : r.ctx.$t("approved"),
4590
+ text: () => r.data.is_pending ? t("pending") : t("approved"),
4566
4591
  adminOnly: true
4567
4592
  });
4568
4593
  pendingBtn.appendTo(r.$actions);
@@ -4572,7 +4597,7 @@ function renderModerator(r) {
4572
4597
  }
4573
4598
  function renderPin(r) {
4574
4599
  const pinnedBtn = new ActionBtn({
4575
- text: () => r.data.is_pinned ? r.ctx.$t("unpin") : r.ctx.$t("pin"),
4600
+ text: () => r.data.is_pinned ? t("unpin") : t("pin"),
4576
4601
  adminOnly: true
4577
4602
  });
4578
4603
  pinnedBtn.appendTo(r.$actions);
@@ -4582,19 +4607,19 @@ function renderPin(r) {
4582
4607
  }
4583
4608
  function renderEdit(r) {
4584
4609
  const editBtn = new ActionBtn({
4585
- text: r.ctx.$t("edit"),
4610
+ text: t("edit"),
4586
4611
  adminOnly: true
4587
4612
  });
4588
4613
  editBtn.appendTo(r.$actions);
4589
4614
  editBtn.setClick(() => {
4590
- r.ctx.editComment(r.data, r.$el);
4615
+ r.opts.editComment(r.data, r.$el);
4591
4616
  });
4592
4617
  }
4593
4618
  function renderDel(r) {
4594
4619
  const delBtn = new ActionBtn({
4595
- text: r.ctx.$t("delete"),
4620
+ text: t("delete"),
4596
4621
  confirm: true,
4597
- confirmText: r.ctx.$t("deleteConfirm"),
4622
+ confirmText: t("deleteConfirm"),
4598
4623
  adminOnly: true
4599
4624
  });
4600
4625
  delBtn.appendTo(r.$actions);
@@ -4636,18 +4661,21 @@ class Render {
4636
4661
  __publicField(this, "$replyAt");
4637
4662
  this.comment = comment;
4638
4663
  }
4639
- get ctx() {
4640
- return this.comment.ctx;
4641
- }
4642
4664
  get data() {
4643
4665
  return this.comment.getData();
4644
4666
  }
4645
- get conf() {
4646
- return this.comment.conf;
4647
- }
4648
- get cConf() {
4649
- return this.comment.getConf();
4667
+ get opts() {
4668
+ return this.comment.getOpts();
4650
4669
  }
4670
+ /**
4671
+ * Render the comment ui
4672
+ *
4673
+ * If comment data is updated, call this method to re-render the comment ui.
4674
+ * The method will be called multiple times, so it should be idempotent.
4675
+ *
4676
+ * Renders may add event listeners to the comment ui, so it should be called only once or override the original.
4677
+ * Please be aware of the memory leak caused by the event listener.
4678
+ */
4651
4679
  render() {
4652
4680
  this.$el = createElement(CommentHTML);
4653
4681
  this.$main = this.$el.querySelector(".atk-main");
@@ -4657,12 +4685,14 @@ class Render {
4657
4685
  this.$actions = this.$el.querySelector(".atk-actions");
4658
4686
  this.$el.setAttribute("id", `atk-comment-${this.data.id}`);
4659
4687
  loadRenders(this);
4660
- this.recoveryChildrenWrap();
4688
+ if (this.$childrenWrap) {
4689
+ this.$main.append(this.$childrenWrap);
4690
+ }
4661
4691
  return this.$el;
4662
4692
  }
4663
4693
  /** 内容限高检测 */
4664
4694
  checkHeightLimit() {
4665
- const conf = this.ctx.conf.heightLimit;
4695
+ const conf = this.opts.heightLimit;
4666
4696
  if (!conf || !conf.content || !conf.children)
4667
4697
  return;
4668
4698
  const contentMaxH = conf.content;
@@ -4696,24 +4726,21 @@ class Render {
4696
4726
  playFadeAnimForBody() {
4697
4727
  playFadeInAnim(this.comment.getRender().$body);
4698
4728
  }
4729
+ /** Perform the flash animation */
4730
+ playFlashAnim() {
4731
+ this.$el.classList.remove("atk-flash-once");
4732
+ window.setTimeout(() => {
4733
+ this.$el.classList.add("atk-flash-once");
4734
+ }, 150);
4735
+ }
4699
4736
  /** 获取子评论 Wrap */
4700
4737
  getChildrenWrap() {
4701
- return this.$childrenWrap;
4702
- }
4703
- /** 初始化子评论区域 Wrap */
4704
- renderChildrenWrap() {
4705
4738
  if (!this.$childrenWrap) {
4706
4739
  this.$childrenWrap = createElement('<div class="atk-comment-children"></div>');
4707
4740
  this.$main.append(this.$childrenWrap);
4708
4741
  }
4709
4742
  return this.$childrenWrap;
4710
4743
  }
4711
- /** 恢复原有的子评论区域 Wrap */
4712
- recoveryChildrenWrap() {
4713
- if (this.$childrenWrap) {
4714
- this.$main.append(this.$childrenWrap);
4715
- }
4716
- }
4717
4744
  /** 设置已读 */
4718
4745
  setUnread(val) {
4719
4746
  if (val)
@@ -4732,17 +4759,15 @@ class Render {
4732
4759
  setOpenURL(url) {
4733
4760
  this.setOpenable(true);
4734
4761
  this.$el.onclick = (evt) => {
4735
- evt.preventDefault();
4762
+ evt.stopPropagation();
4736
4763
  window.open(url);
4737
- if (this.cConf.openEvt)
4738
- this.cConf.openEvt();
4739
4764
  };
4740
4765
  }
4741
4766
  /** 设置点击评论时的操作 */
4742
4767
  setOpenAction(action) {
4743
4768
  this.setOpenable(true);
4744
4769
  this.$el.onclick = (evt) => {
4745
- evt.preventDefault();
4770
+ evt.stopPropagation();
4746
4771
  action();
4747
4772
  };
4748
4773
  }
@@ -4752,26 +4777,26 @@ class CommentActions {
4752
4777
  __publicField(this, "comment");
4753
4778
  this.comment = comment;
4754
4779
  }
4755
- get ctx() {
4756
- return this.comment.ctx;
4757
- }
4758
4780
  get data() {
4759
4781
  return this.comment.getData();
4760
4782
  }
4761
- get cConf() {
4762
- return this.comment.getConf();
4783
+ get opts() {
4784
+ return this.comment.getOpts();
4785
+ }
4786
+ getApi() {
4787
+ return this.comment.getOpts().getApi();
4763
4788
  }
4764
4789
  /** 投票操作 */
4765
4790
  vote(type) {
4766
4791
  const actionBtn = type === "up" ? this.comment.getRender().voteBtnUp : this.comment.getRender().voteBtnDown;
4767
- this.ctx.getApi().votes.vote(`comment_${type}`, this.data.id, __spreadValues({}, this.ctx.getApi().getUserFields())).then((res) => {
4792
+ this.getApi().votes.vote(`comment_${type}`, this.data.id, __spreadValues({}, this.getApi().getUserFields())).then((res) => {
4768
4793
  var _a, _b;
4769
4794
  this.data.vote_up = res.data.up;
4770
4795
  this.data.vote_down = res.data.down;
4771
4796
  (_a = this.comment.getRender().voteBtnUp) == null ? void 0 : _a.updateText();
4772
4797
  (_b = this.comment.getRender().voteBtnDown) == null ? void 0 : _b.updateText();
4773
4798
  }).catch((err) => {
4774
- actionBtn == null ? void 0 : actionBtn.setError(this.ctx.$t("voteFail"));
4799
+ actionBtn == null ? void 0 : actionBtn.setError(t("voteFail"));
4775
4800
  console.log(err);
4776
4801
  });
4777
4802
  }
@@ -4779,7 +4804,7 @@ class CommentActions {
4779
4804
  adminEdit(type, btnElem) {
4780
4805
  if (btnElem.isLoading)
4781
4806
  return;
4782
- btnElem.setLoading(true, `${this.ctx.$t("editing")}...`);
4807
+ btnElem.setLoading(true, `${t("editing")}...`);
4783
4808
  const modify = __spreadValues({}, this.data);
4784
4809
  if (type === "collapsed") {
4785
4810
  modify.is_collapsed = !modify.is_collapsed;
@@ -4788,26 +4813,26 @@ class CommentActions {
4788
4813
  } else if (type === "pinned") {
4789
4814
  modify.is_pinned = !modify.is_pinned;
4790
4815
  }
4791
- this.ctx.getApi().comments.updateComment(this.data.id, __spreadValues({}, modify)).then((res) => {
4816
+ this.getApi().comments.updateComment(this.data.id, __spreadValues({}, modify)).then((res) => {
4792
4817
  btnElem.setLoading(false);
4793
4818
  this.comment.setData(res.data);
4794
4819
  }).catch((err) => {
4795
4820
  console.error(err);
4796
- btnElem.setError(this.ctx.$t("editFail"));
4821
+ btnElem.setError(t("editFail"));
4797
4822
  });
4798
4823
  }
4799
4824
  /** 管理员 - 评论删除 */
4800
4825
  adminDelete(btnElem) {
4801
4826
  if (btnElem.isLoading)
4802
4827
  return;
4803
- btnElem.setLoading(true, `${this.ctx.$t("deleting")}...`);
4804
- this.ctx.getApi().comments.deleteComment(this.data.id).then(() => {
4828
+ btnElem.setLoading(true, `${t("deleting")}...`);
4829
+ this.getApi().comments.deleteComment(this.data.id).then(() => {
4805
4830
  btnElem.setLoading(false);
4806
- if (this.cConf.onDelete)
4807
- this.cConf.onDelete(this.comment);
4831
+ if (this.opts.onDelete)
4832
+ this.opts.onDelete(this.comment);
4808
4833
  }).catch((e) => {
4809
4834
  console.error(e);
4810
- btnElem.setError(this.ctx.$t("deleteFail"));
4835
+ btnElem.setError(t("deleteFail"));
4811
4836
  });
4812
4837
  }
4813
4838
  /** 快速跳转到该评论 */
@@ -4819,21 +4844,18 @@ class CommentActions {
4819
4844
  window.dispatchEvent(new Event("hashchange"));
4820
4845
  }
4821
4846
  }
4822
- class CommentNode extends Component {
4823
- // 最大嵌套层数
4824
- constructor(ctx, data, conf) {
4825
- super(ctx);
4847
+ class CommentNode {
4848
+ // 当前嵌套层数
4849
+ constructor(data, opts) {
4850
+ __publicField(this, "$el");
4826
4851
  __publicField(this, "renderInstance");
4827
4852
  __publicField(this, "actionInstance");
4828
4853
  __publicField(this, "data");
4829
- __publicField(this, "cConf");
4854
+ __publicField(this, "opts");
4830
4855
  __publicField(this, "parent");
4831
4856
  __publicField(this, "children", []);
4832
4857
  __publicField(this, "nestCurt");
4833
- // 当前嵌套层数
4834
- __publicField(this, "nestMax");
4835
- this.nestMax = ctx.conf.nestMax || 3;
4836
- this.cConf = conf;
4858
+ this.opts = opts;
4837
4859
  this.data = __spreadValues({}, data);
4838
4860
  this.data.date = this.data.date.replace(/-/g, "/");
4839
4861
  this.parent = null;
@@ -4847,8 +4869,8 @@ class CommentNode extends Component {
4847
4869
  if (this.$el)
4848
4870
  this.$el.replaceWith(newEl);
4849
4871
  this.$el = newEl;
4850
- if (this.cConf.afterRender)
4851
- this.cConf.afterRender();
4872
+ if (this.opts.onAfterRender)
4873
+ this.opts.onAfterRender();
4852
4874
  }
4853
4875
  /** 获取评论操作实例对象 */
4854
4876
  getActions() {
@@ -4889,51 +4911,76 @@ class CommentNode extends Component {
4889
4911
  return this.data.id;
4890
4912
  }
4891
4913
  /** 置入子评论 */
4892
- putChild(childC, insertMode = "append") {
4893
- childC.parent = this;
4894
- childC.nestCurt = this.nestCurt + 1;
4895
- this.children.push(childC);
4896
- const $children = this.getChildrenEl();
4914
+ putChild(childNode, insertMode = "append") {
4915
+ childNode.parent = this;
4916
+ childNode.nestCurt = this.nestCurt + 1;
4917
+ this.children.push(childNode);
4918
+ const $childrenWrap = this.getChildrenWrapEl();
4919
+ const $childComment = childNode.getEl();
4897
4920
  if (insertMode === "append")
4898
- $children.append(childC.getEl());
4921
+ $childrenWrap.append($childComment);
4899
4922
  else if (insertMode === "prepend")
4900
- $children.prepend(childC.getEl());
4901
- childC.getRender().playFadeAnim();
4902
- childC.getRender().checkHeightLimit();
4923
+ $childrenWrap.prepend($childComment);
4924
+ childNode.getRender().playFadeAnim();
4925
+ childNode.getRender().checkHeightLimit();
4903
4926
  }
4904
4927
  /** 获取存放子评论的元素对象 */
4905
- getChildrenEl() {
4906
- let $children = this.getRender().getChildrenWrap();
4907
- if (!$children) {
4908
- if (this.nestCurt < this.nestMax) {
4909
- $children = this.getRender().renderChildrenWrap();
4910
- } else {
4911
- $children = this.parent.getChildrenEl();
4912
- }
4928
+ getChildrenWrapEl() {
4929
+ if (this.nestCurt >= this.opts.nestMax) {
4930
+ return this.parent.getChildrenWrapEl();
4913
4931
  }
4914
- return $children;
4932
+ return this.getRender().getChildrenWrap();
4915
4933
  }
4916
4934
  /** 获取所有父评论 */
4917
4935
  getParents() {
4918
- const parents = [];
4919
- const once = (c) => {
4920
- if (c.parent) {
4921
- parents.push(c.parent);
4922
- once(c.parent);
4923
- }
4924
- };
4925
- once(this);
4926
- return parents;
4936
+ const flattenParents = [];
4937
+ let parent = this.parent;
4938
+ while (parent) {
4939
+ flattenParents.push(parent);
4940
+ parent = parent.getParent();
4941
+ }
4942
+ return flattenParents;
4927
4943
  }
4928
- /** 获取评论元素对象 */
4944
+ /**
4945
+ * Get the element of the comment
4946
+ *
4947
+ * The `getEl()` will always return the latest $el after calling `render()`.
4948
+ * Please be aware of the memory leak if you use the $el reference directly.
4949
+ */
4929
4950
  getEl() {
4951
+ if (!this.$el)
4952
+ throw new Error("comment element not initialized before `getEl()`");
4930
4953
  return this.$el;
4931
4954
  }
4955
+ /**
4956
+ * Focus on the comment
4957
+ *
4958
+ * Scroll to the comment and perform flash animation
4959
+ */
4960
+ focus() {
4961
+ if (!this.$el)
4962
+ throw new Error("comment element not initialized before `focus()`");
4963
+ this.getParents().forEach((p) => {
4964
+ p.getRender().heightLimitRemoveForChildren();
4965
+ });
4966
+ this.scrollIntoView();
4967
+ this.getRender().playFlashAnim();
4968
+ }
4969
+ scrollIntoView() {
4970
+ this.$el && scrollIntoView(this.$el, false, this.opts.scrollRelativeTo && this.opts.scrollRelativeTo());
4971
+ }
4972
+ /**
4973
+ * Remove the comment node
4974
+ */
4975
+ remove() {
4976
+ var _a;
4977
+ (_a = this.$el) == null ? void 0 : _a.remove();
4978
+ }
4932
4979
  /** 获取 Gravatar 头像 URL */
4933
4980
  getGravatarURL() {
4934
4981
  return getGravatarURL({
4935
- mirror: this.ctx.conf.gravatar.mirror,
4936
- params: this.ctx.conf.gravatar.params,
4982
+ mirror: this.opts.gravatar.mirror,
4983
+ params: this.opts.gravatar.params,
4937
4984
  emailMD5: this.data.email_encrypted
4938
4985
  });
4939
4986
  }
@@ -4943,7 +4990,7 @@ class CommentNode extends Component {
4943
4990
  }
4944
4991
  /** 获取格式化后的日期 */
4945
4992
  getDateFormatted() {
4946
- return timeAgo(new Date(this.data.date), this.ctx.$t);
4993
+ return timeAgo(new Date(this.data.date), t);
4947
4994
  }
4948
4995
  /** 获取用户 UserAgent 信息 */
4949
4996
  getUserUA() {
@@ -4954,21 +5001,33 @@ class CommentNode extends Component {
4954
5001
  };
4955
5002
  }
4956
5003
  /** 获取配置 */
4957
- getConf() {
4958
- return this.cConf;
5004
+ getOpts() {
5005
+ return this.opts;
4959
5006
  }
4960
5007
  }
4961
- function createComment(ctx, comment, ctxComments) {
4962
- var _a;
4963
- const instance2 = new CommentNode(ctx, comment, {
4964
- isFlatMode: (_a = ctx.getData().getListLastFetch()) == null ? void 0 : _a.params.flatMode,
4965
- afterRender: () => {
5008
+ function createCommentNode(ctx, comment, replyComment, opts) {
5009
+ const instance2 = new CommentNode(comment, {
5010
+ onAfterRender: () => {
4966
5011
  ctx.trigger("comment-rendered", instance2);
4967
5012
  },
4968
5013
  onDelete: (c) => {
4969
5014
  ctx.getData().deleteComment(c.getID());
4970
5015
  },
4971
- replyTo: comment.rid ? ctxComments.find((c) => c.id === comment.rid) : void 0
5016
+ replyTo: replyComment,
5017
+ // TODO simplify reference
5018
+ flatMode: typeof (opts == null ? void 0 : opts.forceFlatMode) === "boolean" ? opts == null ? void 0 : opts.forceFlatMode : ctx.conf.flatMode,
5019
+ gravatar: ctx.conf.gravatar,
5020
+ nestMax: ctx.conf.nestMax,
5021
+ heightLimit: ctx.conf.heightLimit,
5022
+ avatarURLBuilder: ctx.conf.avatarURLBuilder,
5023
+ scrollRelativeTo: ctx.conf.scrollRelativeTo,
5024
+ vote: ctx.conf.vote,
5025
+ voteDown: ctx.conf.voteDown,
5026
+ uaBadge: ctx.conf.uaBadge,
5027
+ // TODO: move to plugin folder and remove from core
5028
+ getApi: () => ctx.getApi(),
5029
+ replyComment: (c, $el) => ctx.replyComment(c, $el),
5030
+ editComment: (c, $el) => ctx.editComment(c, $el)
4972
5031
  });
4973
5032
  instance2.render();
4974
5033
  return instance2;
@@ -5344,20 +5403,19 @@ class List extends Component {
5344
5403
  getCommentNodes() {
5345
5404
  return this.commentNodes;
5346
5405
  }
5347
- getListLayout() {
5406
+ getListLayout({ forceFlatMode } = {}) {
5348
5407
  return new ListLayout({
5349
5408
  $commentsWrap: this.$commentsWrap,
5350
5409
  nestSortBy: this.ctx.conf.nestSort,
5351
5410
  nestMax: this.ctx.conf.nestMax,
5352
- flatMode: this.ctx.conf.flatMode,
5411
+ flatMode: typeof forceFlatMode === "boolean" ? forceFlatMode : this.ctx.conf.flatMode,
5353
5412
  // flatMode must be boolean because it had been handled when Artalk.init
5354
- createCommentNode: (d, c) => {
5355
- const node = createComment(this.ctx, d, c);
5413
+ createCommentNode: (d, r) => {
5414
+ const node = createCommentNode(this.ctx, d, r, { forceFlatMode });
5356
5415
  this.commentNodes.push(node);
5357
5416
  return node;
5358
5417
  },
5359
- findCommentNode: (id) => this.commentNodes.find((c) => c.getID() === id),
5360
- getCommentDataList: () => this.ctx.getData().getComments()
5418
+ findCommentNode: (id) => this.commentNodes.find((c) => c.getID() === id)
5361
5419
  });
5362
5420
  }
5363
5421
  initCrudEvents() {
@@ -5365,8 +5423,10 @@ class List extends Component {
5365
5423
  this.getListLayout().import(comments);
5366
5424
  });
5367
5425
  this.ctx.on("list-loaded", (comments) => {
5368
- if (comments.length === 0)
5426
+ if (comments.length === 0) {
5427
+ this.commentNodes = [];
5369
5428
  this.$commentsWrap.innerHTML = "";
5429
+ }
5370
5430
  });
5371
5431
  this.ctx.on("comment-inserted", (comment) => {
5372
5432
  this.getListLayout().insert(comment);
@@ -5377,7 +5437,7 @@ class List extends Component {
5377
5437
  console.error(`comment node id=${comment.id} not found`);
5378
5438
  return;
5379
5439
  }
5380
- node.getEl().remove();
5440
+ node.remove();
5381
5441
  this.commentNodes = this.commentNodes.filter((c) => c.getID() !== comment.id);
5382
5442
  });
5383
5443
  this.ctx.on("comment-updated", (comment) => {
@@ -5894,15 +5954,16 @@ class StateReply extends EditorPlug {
5894
5954
  }
5895
5955
  setReply(commentData) {
5896
5956
  const ui = this.kit.useUI();
5897
- if (!ui.$sendReply) {
5898
- ui.$sendReply = createElement(
5899
- `<div class="atk-send-reply">${t("reply")} <span class="atk-text"></span><span class="atk-cancel">×</span></div>`
5957
+ if (!ui.$sendReplyBtn) {
5958
+ const $btn = createElement(
5959
+ `<span class="atk-state-btn"><span class="atk-text-wrap">${t("reply")} <span class="atk-text"></span></span><span class="atk-cancel">×</span></span>`
5900
5960
  );
5901
- ui.$sendReply.querySelector(".atk-text").innerText = `@${commentData.nick}`;
5902
- ui.$sendReply.addEventListener("click", () => {
5961
+ $btn.querySelector(".atk-text").innerText = `@${commentData.nick}`;
5962
+ $btn.addEventListener("click", () => {
5903
5963
  this.kit.useEditor().resetState();
5904
5964
  });
5905
- ui.$textareaWrap.append(ui.$sendReply);
5965
+ ui.$stateWrap.append($btn);
5966
+ ui.$sendReplyBtn = $btn;
5906
5967
  }
5907
5968
  this.comment = commentData;
5908
5969
  ui.$textarea.focus();
@@ -5911,9 +5972,9 @@ class StateReply extends EditorPlug {
5911
5972
  if (!this.comment)
5912
5973
  return;
5913
5974
  const ui = this.kit.useUI();
5914
- if (ui.$sendReply) {
5915
- ui.$sendReply.remove();
5916
- ui.$sendReply = void 0;
5975
+ if (ui.$sendReplyBtn) {
5976
+ ui.$sendReplyBtn.remove();
5977
+ ui.$sendReplyBtn = void 0;
5917
5978
  }
5918
5979
  this.comment = void 0;
5919
5980
  }
@@ -5960,12 +6021,12 @@ class StateEdit extends EditorPlug {
5960
6021
  const ui = this.kit.useUI();
5961
6022
  if (!ui.$editCancelBtn) {
5962
6023
  const $btn = createElement(
5963
- `<div class="atk-send-reply">${t("editCancel")} <span class="atk-cancel">×</span></div>`
6024
+ `<span class="atk-state-btn"><span class="atk-text-wrap">${t("editCancel")}</span><span class="atk-cancel">×</span></span>`
5964
6025
  );
5965
6026
  $btn.onclick = () => {
5966
6027
  this.kit.useEditor().resetState();
5967
6028
  };
5968
- ui.$textareaWrap.append($btn);
6029
+ ui.$stateWrap.append($btn);
5969
6030
  ui.$editCancelBtn = $btn;
5970
6031
  }
5971
6032
  this.comment = comment;
@@ -6162,7 +6223,7 @@ class Emoticons extends EditorPlug {
6162
6223
  __publicField(this, "isImgLoaded", false);
6163
6224
  this.kit.useMounted(() => {
6164
6225
  this.usePanel(`<div class="atk-editor-plug-emoticons"></div>`);
6165
- this.useBtn(t("emoticon"));
6226
+ this.useBtn(`<i aria-label="${t("emoticon")}"><svg fill="currentColor" aria-hidden="true" height="14" viewBox="0 0 14 14" width="14"><path d="m4.26829 5.29294c0-.94317.45893-1.7074 1.02439-1.7074.56547 0 1.02439.76423 1.02439 1.7074s-.45892 1.7074-1.02439 1.7074c-.56546 0-1.02439-.76423-1.02439-1.7074zm4.43903 1.7074c.56546 0 1.02439-.76423 1.02439-1.7074s-.45893-1.7074-1.02439-1.7074c-.56547 0-1.02439.76423-1.02439 1.7074s.45892 1.7074 1.02439 1.7074zm-1.70732 2.73184c-1.51883 0-2.06312-1.52095-2.08361-1.58173l-1.29551.43231c.03414.10244.868 2.51604 3.3798 2.51604 2.51181 0 3.34502-2.41291 3.37982-2.51604l-1.29484-.43573c-.02254.06488-.56683 1.58583-2.08498 1.58583zm7-2.73252c0 3.86004-3.1401 7.00034-7 7.00034s-7-3.1396-7-6.99966c0-3.86009 3.1401-7.00034 7-7.00034s7 3.14025 7 7.00034zm-1.3659 0c0-3.10679-2.5275-5.63442-5.6341-5.63442-3.10663 0-5.63415 2.52832-5.63415 5.6351 0 3.10676 2.52752 5.63446 5.63415 5.63446 3.1066 0 5.6341-2.5277 5.6341-5.63446z"/></svg></i>`);
6166
6227
  });
6167
6228
  this.kit.useUnmounted(() => {
6168
6229
  });
@@ -6413,7 +6474,7 @@ class Upload extends EditorPlug {
6413
6474
  this.$imgUploadInput.type = "file";
6414
6475
  this.$imgUploadInput.style.display = "none";
6415
6476
  this.$imgUploadInput.accept = AllowImgExts.map((o) => `.${o}`).join(",");
6416
- const $btn = this.useBtn(`${t("image")}`);
6477
+ const $btn = this.useBtn(`<i aria-label="${t("uploadImage")}"><svg fill="currentColor" aria-hidden="true" height="14" viewBox="0 0 14 14" width="14"><path d="m0 1.94444c0-1.074107.870333-1.94444 1.94444-1.94444h10.11116c1.0741 0 1.9444.870333 1.9444 1.94444v10.11116c0 1.0741-.8703 1.9444-1.9444 1.9444h-10.11116c-1.074107 0-1.94444-.8703-1.94444-1.9444zm1.94444-.38888c-.21466 0-.38888.17422-.38888.38888v7.06689l2.33333-2.33333 2.33333 2.33333 3.88888-3.88889 2.3333 2.33334v-5.51134c0-.21466-.1742-.38888-.3888-.38888zm10.49996 8.09977-2.3333-2.33333-3.88888 3.8889-2.33333-2.33334-2.33333 2.33334v.8447c0 .2146.17422.3888.38888.3888h10.11116c.2146 0 .3888-.1742.3888-.3888zm-7.1944-6.54422c-.75133 0-1.36111.60978-1.36111 1.36111 0 .75134.60978 1.36111 1.36111 1.36111s1.36111-.60977 1.36111-1.36111c0-.75133-.60978-1.36111-1.36111-1.36111z"/></svg></i>`);
6417
6478
  $btn.after(this.$imgUploadInput);
6418
6479
  $btn.onclick = () => {
6419
6480
  const $input = this.$imgUploadInput;
@@ -6517,10 +6578,7 @@ class Preview extends EditorPlug {
6517
6578
  __publicField(this, "isPlugPanelShow", false);
6518
6579
  this.kit.useMounted(() => {
6519
6580
  this.usePanel(`<div class="atk-editor-plug-preview"></div>`);
6520
- let btnText = t("preview");
6521
- if (getInstance())
6522
- btnText += ` <i title="Markdown is supported"><svg class="markdown" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></i>`;
6523
- this.useBtn(btnText);
6581
+ this.useBtn(`<i aria-label="${t("preview")}"><svg fill="currentColor" aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M14.85 3H1.15C.52 3 0 3.52 0 4.15v7.69C0 12.48.52 13 1.15 13h13.69c.64 0 1.15-.52 1.15-1.15v-7.7C16 3.52 15.48 3 14.85 3zM9 11H7V8L5.5 9.92 4 8v3H2V5h2l1.5 2L7 5h2v6zm2.99.5L9.5 8H11V5h2v3h1.5l-2.51 3.5z"></path></svg></i>`);
6524
6582
  });
6525
6583
  this.kit.useUnmounted(() => {
6526
6584
  });
@@ -6862,62 +6920,50 @@ const UnreadBadge = (ctx) => {
6862
6920
  showUnreadBadge(notifies.length || 0);
6863
6921
  });
6864
6922
  };
6865
- const Goto = (ctx) => {
6866
- let delayGoto = true;
6867
- const check2 = () => {
6923
+ const GotoDispatcher = (ctx) => {
6924
+ let lastID = 0;
6925
+ const check2 = ({ locker }) => {
6868
6926
  const commentID = extractCommentID();
6869
6927
  if (!commentID)
6870
6928
  return;
6929
+ if (locker && lastID === commentID)
6930
+ return;
6931
+ lastID = commentID;
6871
6932
  ctx.trigger("list-goto", commentID);
6872
- delayGoto = true;
6873
- };
6874
- const hashChangeHandler = () => {
6875
- delayGoto = false;
6876
- check2();
6877
6933
  };
6934
+ const hashChangeHandler = () => check2({ locker: false });
6935
+ const listLoadedHandler = () => check2({ locker: true });
6878
6936
  ctx.on("mounted", () => {
6879
6937
  window.addEventListener("hashchange", hashChangeHandler);
6880
- ctx.on("list-loaded", check2);
6938
+ ctx.on("list-loaded", listLoadedHandler);
6881
6939
  });
6882
6940
  ctx.on("unmounted", () => {
6883
6941
  window.removeEventListener("hashchange", hashChangeHandler);
6884
- ctx.off("list-loaded", check2);
6885
- });
6886
- let foundID = 0;
6887
- ctx.on("list-goto", (commentID) => {
6888
- if (foundID === commentID)
6889
- return;
6890
- const comment = ctx.get("list").getCommentNodes().find((c) => c.getID() === commentID);
6891
- if (!comment)
6892
- return;
6893
- foundID = commentID;
6894
- comment.getParents().forEach((p) => {
6895
- p.getRender().heightLimitRemoveForChildren();
6896
- });
6897
- const goTo = () => {
6898
- scrollIntoView(comment.getEl(), false);
6899
- comment.getEl().classList.remove("atk-flash-once");
6900
- window.setTimeout(() => {
6901
- comment.getEl().classList.add("atk-flash-once");
6902
- }, 150);
6903
- };
6904
- if (!delayGoto)
6905
- goTo();
6906
- else
6907
- window.setTimeout(() => goTo(), 350);
6942
+ ctx.off("list-loaded", listLoadedHandler);
6908
6943
  });
6909
6944
  };
6910
6945
  function extractCommentID() {
6911
- let commentId = Number(getQueryParam("atk_comment"));
6946
+ const match = window.location.hash.match(/#atk-comment-([0-9]+)/);
6947
+ let commentId = match && match[1] && !Number.isNaN(parseFloat(match[1])) ? parseFloat(match[1]) : null;
6912
6948
  if (!commentId) {
6913
- const match = window.location.hash.match(/#atk-comment-([0-9]+)/);
6914
- if (!match || !match[1] || Number.isNaN(Number(match[1])))
6915
- return null;
6916
- commentId = Number(match[1]);
6949
+ commentId = Number(getQueryParam("atk_comment"));
6917
6950
  }
6918
6951
  return commentId || null;
6919
6952
  }
6920
- const version = "2.8.0";
6953
+ const GotoFocus = (ctx) => {
6954
+ ctx.on("list-goto", (commentID) => __async(void 0, null, function* () {
6955
+ let comment = ctx.getCommentNodes().find((c) => c.getID() === commentID);
6956
+ if (!comment) {
6957
+ const data = (yield ctx.getApi().comments.getComment(commentID)).data;
6958
+ ctx.get("list").getListLayout({ forceFlatMode: true }).insert(data.comment, data.reply_comment);
6959
+ comment = ctx.getCommentNodes().find((c) => c.getID() === commentID);
6960
+ }
6961
+ if (!comment)
6962
+ return;
6963
+ comment.focus();
6964
+ }));
6965
+ };
6966
+ const version = "2.8.2";
6921
6967
  const Copyright = (ctx) => {
6922
6968
  ctx.on("mounted", () => {
6923
6969
  const list = ctx.get("list");
@@ -6928,14 +6974,7 @@ const Copyright = (ctx) => {
6928
6974
  });
6929
6975
  };
6930
6976
  const NoComment = (ctx) => {
6931
- let isFetching = false;
6932
- ctx.on("list-fetch", (params) => {
6933
- isFetching = true;
6934
- });
6935
6977
  ctx.on("list-loaded", (comments) => {
6936
- if (!isFetching)
6937
- return;
6938
- isFetching = false;
6939
6978
  const list = ctx.get("list");
6940
6979
  const isNoComment = comments.length <= 0;
6941
6980
  let $noComment = list.getCommentsWrapEl().querySelector(".atk-list-no-comment");
@@ -7187,7 +7226,8 @@ const ListPlugins = [
7187
7226
  SidebarBtn,
7188
7227
  UnreadBadge,
7189
7228
  Dropdown,
7190
- Goto,
7229
+ GotoDispatcher,
7230
+ GotoFocus,
7191
7231
  NoComment,
7192
7232
  Copyright,
7193
7233
  TimeTicking,
@@ -7211,6 +7251,7 @@ const PvCountWidget = (ctx) => {
7211
7251
  ctx.watchConf([
7212
7252
  "site",
7213
7253
  "pageKey",
7254
+ "pageTitle",
7214
7255
  "countEl",
7215
7256
  "pvEl"
7216
7257
  ], (conf) => {
@@ -7218,6 +7259,7 @@ const PvCountWidget = (ctx) => {
7218
7259
  getApi: () => ctx.getApi(),
7219
7260
  siteName: conf.site,
7220
7261
  pageKey: conf.pageKey,
7262
+ pageTitle: conf.pageTitle,
7221
7263
  countEl: conf.countEl,
7222
7264
  pvEl: conf.pvEl,
7223
7265
  pvAdd: typeof ctx.conf.pvAdd === "boolean" ? ctx.conf.pvAdd : true
@@ -7229,10 +7271,10 @@ function initCountWidget(opt) {
7229
7271
  if (opt.countEl && document.querySelector(opt.countEl)) {
7230
7272
  refreshStatCount(opt, { query: "page_comment", numEl: opt.countEl });
7231
7273
  }
7232
- const initialData = opt.pvAdd ? {
7274
+ const initialData = opt.pvAdd && opt.pageKey ? {
7233
7275
  [opt.pageKey]: (yield opt.getApi().pages.logPv({
7234
7276
  page_key: opt.pageKey,
7235
- page_title: opt.pageKey,
7277
+ page_title: opt.pageTitle,
7236
7278
  site_name: opt.siteName
7237
7279
  })).data.pv
7238
7280
  // pv+1 and get pv count
@@ -7249,7 +7291,7 @@ function initCountWidget(opt) {
7249
7291
  function refreshStatCount(opt, args) {
7250
7292
  return __async(this, null, function* () {
7251
7293
  let data = args.data || {};
7252
- let queryPageKeys = Array.from(document.querySelectorAll(args.numEl)).map((e) => e.getAttribute("data-page-key") || opt.pageKey).filter((k) => typeof data[k] !== "number");
7294
+ let queryPageKeys = Array.from(document.querySelectorAll(args.numEl)).map((e) => e.getAttribute("data-page-key") || opt.pageKey).filter((k) => k && typeof data[k] !== "number");
7253
7295
  queryPageKeys = [...new Set(queryPageKeys)];
7254
7296
  if (queryPageKeys.length > 0) {
7255
7297
  const res = (yield opt.getApi().stats.getStats(args.query, {
@@ -7258,7 +7300,8 @@ function refreshStatCount(opt, args) {
7258
7300
  })).data.data;
7259
7301
  data = __spreadValues(__spreadValues({}, data), res);
7260
7302
  }
7261
- applyCountData(args.numEl, data, data[opt.pageKey]);
7303
+ const defaultCount = opt.pageKey ? data[opt.pageKey] : 0;
7304
+ applyCountData(args.numEl, data, defaultCount);
7262
7305
  });
7263
7306
  }
7264
7307
  function applyCountData(selector, data, defaultCount) {
@@ -7447,7 +7490,6 @@ class Artalk {
7447
7490
  initCountWidget({
7448
7491
  getApi: () => new Api2(convertApiOptions(conf)),
7449
7492
  siteName: conf.site,
7450
- pageKey: conf.pageKey,
7451
7493
  countEl: conf.countEl,
7452
7494
  pvEl: conf.pvEl,
7453
7495
  pvAdd: false