bai-viet/chuyen-hoc-thu-phap/cht.js · Nhập từ GitHub ocd4-assets@e3435aeb (bản đang chạy trên thudao.com 10/11/2026)

/* Chuyện học thư pháp: hiệu ứng, video, nghe bài viết cho khối #cht trong bài đăng Blogspot.
   Nội dung bài luôn hiển thị khi không có file này; file này chỉ thêm tính năng. */
(function () {
  "use strict";
  var PLAY = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2.5v11l9-5.5z"/></svg>';
  var PAUSE = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3" y="2" width="3.6" height="12" rx="1"/><rect x="9.4" y="2" width="3.6" height="12" rx="1"/></svg>';
  var STOP = '<svg viewBox="0 0 16 16" aria-hidden="true"><rect x="3" y="3" width="10" height="10" rx="1.5"/></svg>';

  function each(list, fn) { for (var i = 0; i < list.length; i++) fn(list[i], i); }

  /* ---------- thanh tiến độ đọc ---------- */
  function progress(root) {
    if (document.getElementById("cht-tien-do")) return;
    var bar = document.createElement("div");
    bar.id = "cht-tien-do";
    document.body.appendChild(bar);
    function upd() {
      var r = root.getBoundingClientRect();
      var tot = r.height - window.innerHeight;
      var p = tot > 0 ? Math.min(1, Math.max(0, -r.top / tot)) : 0;
      bar.style.width = (p * 100) + "%";
    }
    window.addEventListener("scroll", upd, { passive: true });
    upd();
  }

  /* ---------- hiện dần khi cuộn (không bao giờ giấu nội dung vĩnh viễn) ---------- */
  function reveal(root) {
    var items = root.querySelectorAll(".than > *, .moi, .khac, .md .net");
    if (!("IntersectionObserver" in window)) return;
    var vh = window.innerHeight;
    var pending = [];
    each(items, function (el) {
      var r = el.getBoundingClientRect();
      if (r.top > vh * 0.92) { el.classList.add("r"); pending.push(el); }   // chỉ hiệu ứng cho phần chưa tới
      else el.classList.add("in");
    });
    var fired = false;
    var io = new IntersectionObserver(function (es) {
      fired = true;
      each(es, function (e) {
        if (e.isIntersecting || e.boundingClientRect.top < 0) { e.target.classList.add("in"); io.unobserve(e.target); }
      });
    }, { rootMargin: "0px 0px -5% 0px", threshold: 0 });
    each(pending, function (el) { io.observe(el); });
    // dự phòng: nếu trình duyệt không gọi observer, hiện hết
    setTimeout(function () { if (!fired) each(pending, function (el) { el.classList.add("in"); }); }, 1500);
    // dự phòng khi nhảy bằng mục lục: phần đã cuộn qua cũng hiện
    window.addEventListener("scroll", function () {
      each(pending, function (el) { if (el.getBoundingClientRect().top < window.innerHeight) el.classList.add("in"); });
    }, { passive: true });
  }

  /* ---------- video: bấm ảnh bìa mới tạo trình phát ---------- */
  function video(root) {
    each(root.querySelectorAll(".vd"), function (box) {
      var link = box.querySelector("a.phat");
      if (!link) return;
      link.addEventListener("click", function (ev) {
        ev.preventDefault();
        var v = document.createElement("video");
        v.src = link.getAttribute("href");
        v.controls = true; v.autoplay = true; v.setAttribute("playsinline", "");
        var img = box.querySelector("img");
        if (img) v.poster = img.getAttribute("src");
        var khung = box.querySelector(".khung");
        khung.innerHTML = ""; khung.appendChild(v);
        var p = v.play(); if (p && p.catch) p.catch(function () {});
        if ("IntersectionObserver" in window) {
          new IntersectionObserver(function (es) { if (!es[0].isIntersecting) v.pause(); }, { threshold: 0.2 }).observe(v);
        }
      });
    });
  }

  /* ---------- nghe bài viết ---------- */
  function reader(root) {
    var slot = root.querySelector(".nghe");
    if (!slot) return;
    if (!("speechSynthesis" in window)) return;          // không hỗ trợ thì không hiện thanh nghe
    slot.innerHTML =
      '<div class="box">' +
      '<button type="button" class="doc" aria-label="Đọc bài viết">' + PLAY + '</button>' +
      '<button type="button" class="dung phu" aria-label="Dừng đọc">' + STOP + '</button>' +
      '<span class="nhan">Nghe bài viết</span><span class="thanh"><i></i></span>' +
      '<select aria-label="Tốc độ đọc"><option value="0.85">Chậm</option><option value="1" selected>Vừa</option><option value="1.2">Nhanh</option></select>' +
      '</div>';
    slot.classList.add("co");
    var btn = slot.querySelector(".doc"), stop = slot.querySelector(".dung"),
        fill = slot.querySelector(".thanh i"), lab = slot.querySelector(".nhan"), sel = slot.querySelector("select");
    var synth = window.speechSynthesis;
    var nodes = [];
    each(root.querySelectorAll(".md .lede, .than h2, .than h3, .than p, .than li, .than blockquote, .than .pull"), function (n) {
      if (n.classList.contains("ky")) return;
      if ((n.innerText || "").replace(/\s+/g, "").length > 2) nodes.push(n);
    });
    var i = 0, playing = false, cur = null;
    function voice() {
      var vs = synth.getVoices(), k;
      for (k = 0; k < vs.length; k++) if (/^vi/i.test(vs[k].lang) && /Google|Natural|Premium|Enhanced|Linh|An/i.test(vs[k].name)) return vs[k];
      for (k = 0; k < vs.length; k++) if (/^vi/i.test(vs[k].lang)) return vs[k];
      return null;
    }
    function clear() { if (cur) cur.classList.remove("dang-doc"); }
    function end() { playing = false; clear(); btn.innerHTML = PLAY; lab.textContent = "Đã đọc xong"; fill.style.width = "100%"; i = 0; }
    function speak(k) {
      if (k >= nodes.length) { end(); return; }
      clear(); cur = nodes[k]; cur.classList.add("dang-doc");
      try { cur.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (e) {}
      var u = new SpeechSynthesisUtterance(cur.innerText.replace(/\s+/g, " "));
      var vo = voice(); if (vo) u.voice = vo;
      u.lang = "vi-VN"; u.rate = parseFloat(sel.value || "1");
      u.onend = function () { if (playing) { i = k + 1; fill.style.width = (i / nodes.length * 100) + "%"; speak(i); } };
      u.onerror = function () { if (playing) { i = k + 1; speak(i); } };
      synth.speak(u);
    }
    btn.onclick = function () {
      if (playing) { synth.pause(); playing = false; btn.innerHTML = PLAY; lab.textContent = "Tạm dừng"; return; }
      if (synth.paused && synth.speaking) { synth.resume(); playing = true; btn.innerHTML = PAUSE; lab.textContent = "Đang đọc…"; return; }
      synth.cancel(); playing = true; btn.innerHTML = PAUSE; lab.textContent = "Đang đọc…"; speak(i);
    };
    stop.onclick = function () { synth.cancel(); playing = false; clear(); btn.innerHTML = PLAY; i = 0; fill.style.width = "0"; lab.textContent = "Nghe bài viết"; };
    sel.onchange = function () { if (playing) { synth.cancel(); speak(i); } };
    if (synth.onvoiceschanged !== undefined) synth.onvoiceschanged = voice;
    window.addEventListener("beforeunload", function () { synth.cancel(); });
    // khi cuộn qua: thu thành nút tròn ở góc dưới phải (không đụng menu cố định của theme)
    function noi() {
      var a = slot.getBoundingClientRect(), b = root.getBoundingClientRect();
      slot.classList.toggle("noi", a.bottom < 0 && b.bottom > window.innerHeight * 0.6);
    }
    window.addEventListener("scroll", noi, { passive: true });
    noi();
  }

  function init() {
    each(document.querySelectorAll(".cht-bai"), function (root) {
      if (root.getAttribute("data-san-sang")) return;
      root.setAttribute("data-san-sang", "1");
      root.classList.add("js");
      try { progress(root); } catch (e) {}
      try { video(root); } catch (e) {}
      try { reader(root); } catch (e) {}
      try { reveal(root); } catch (e) {}
    });
  }
  if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
})();

Nhận xét

Bài đăng phổ biến từ blog này

trang/thudao/dang-ky-hoc.html · Bản trước khi chuyển sang kho (tải từ GitHub/jsDelivr)

trang/thudao/bai-viet/benh-cua-thu-dan-duong-dai-the-trien-lam-xu-thu-va-nguy-menh-de-vuot-co-nhan.html · Bản trước khi chuyển sang kho (có video, tải từ GitHub/jsDelivr)

trang/thudao/tai-lieu.html · Bản đang chạy 11/11/2026 (khối tải nhanh từ kho)