reader.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. // resources/js/modules/reader.js
  2. export function initReader() {
  3. injectCommentaryMarkers();
  4. initTocToggle();
  5. }
  6. // TOC 折叠/展开:点击按钮切换所在 .toc-tree 的 .toc-expanded 类
  7. // offcanvas 与侧边栏各有一份 toc,故对每个按钮分别绑定
  8. function initTocToggle() {
  9. document.querySelectorAll('[data-toc-toggle]').forEach((btn) => {
  10. btn.addEventListener('click', () => {
  11. const tree = btn.closest('[data-toc-tree]');
  12. if (tree) {
  13. tree.classList.toggle('toc-expanded');
  14. }
  15. });
  16. });
  17. }
  18. function injectCommentaryMarkers() {
  19. let counter = 0;
  20. document
  21. .querySelectorAll('div.sentence[data-note-id]')
  22. .forEach((sentenceEl) => {
  23. const uid = sentenceEl.dataset.noteId;
  24. counter++;
  25. const id = `commentary-${counter}`;
  26. // checkbox:控制展开,紧跟在 sentence 后
  27. const checkbox = document.createElement('input');
  28. checkbox.type = 'checkbox';
  29. checkbox.className = 'commentary-toggle';
  30. checkbox.id = id;
  31. // label(icon):行内,插在句子文字末尾
  32. const label = document.createElement('label');
  33. label.htmlFor = id;
  34. label.className = 'commentary-icon';
  35. label.innerHTML = '<i class="ti ti-message-circle"></i>';
  36. // 注释块:紧跟在 checkbox 后(CSS 相邻选择器依赖此顺序)
  37. const note = document.createElement('div');
  38. note.className = 'commentary-note';
  39. note.dataset.uuid = uid;
  40. note.dataset.loaded = 'false';
  41. // label 插入句子内文字末尾(行内不打断文字流)
  42. const innerSpan = sentenceEl.querySelector(':scope > span');
  43. (innerSpan ?? sentenceEl).appendChild(label);
  44. // sentence 后:先插 note,再插 checkbox(after 逆序)
  45. // 最终顺序:div.sentence → input.commentary-toggle → div.commentary-note
  46. sentenceEl.after(note);
  47. sentenceEl.after(checkbox);
  48. // 点击时懒加载
  49. checkbox.addEventListener('change', async () => {
  50. if (!checkbox.checked) {
  51. return;
  52. }
  53. if (note.dataset.loaded === 'true') {
  54. return;
  55. }
  56. note.innerHTML =
  57. '<span class="text-muted small">加载中…</span>';
  58. await fetchCommentary(note);
  59. });
  60. });
  61. }
  62. async function fetchCommentary(noteEl) {
  63. const uuid = noteEl.dataset.uuid;
  64. try {
  65. const res = await fetch(`/api/v2/sentence/${uuid}?format=html`);
  66. if (!res.ok) {
  67. throw new Error(res.status);
  68. }
  69. const json = await res.json();
  70. if (!json.ok) {
  71. throw new Error('api error');
  72. }
  73. noteEl.innerHTML = json.data.html;
  74. noteEl.dataset.loaded = 'true';
  75. } catch {
  76. noteEl.innerHTML = '<span class="text-muted small">加载失败</span>';
  77. }
  78. }