[css][js] 识别开头内容,应用风格样式

本贴最后更新于 186 天前,其中的信息可能已经时过境迁

检测特定开头文本为符合条件的引述块添加自定义 CSS

增加新的样式,只需要做两件事:

  1. 在 JS 规则数组中添加一行规则:{ class: "warning", regex: /^WARNING[::]?/i },

    字段 说明
    class 将添加到 <blockquote> 的 class 名称(用于匹配 CSS)
    regex 用于检测块首文本是否匹配的正则表达式
  2. 在 CSS 中添加对应样式,可以用 Emoji、图标、颜色、渐变背景等来自定义风格。

示例:

当引述块的首行为时间戳(例如:2025-06-16 13:05:14),通过代码片段将引述块改为时间线样式。

JS:

function applyCustomBlockquoteClasses() {
  const rules = [
    { class: "timestamp", regex: /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/ },

  ];

  const blockquotes = document.querySelectorAll('[data-type="NodeBlockquote"]');

  blockquotes.forEach((bq) => {
    const firstP = bq.querySelector('.p');
    if (!firstP) return;

    const textDiv = firstP.querySelector('div[contenteditable]');
    if (!textDiv) return;

    const text = textDiv.textContent.trim();

    // 清除旧的 class
    rules.forEach((r) => bq.classList.remove(r.class));

    // 添加匹配到的 class
    for (const rule of rules) {
      if (rule.regex.test(text)) {
        bq.classList.add(rule.class);
        break;
      }
    }
  });
}

// 初始加载 & 动态更新
document.addEventListener("DOMContentLoaded", applyCustomBlockquoteClasses);
new MutationObserver(applyCustomBlockquoteClasses).observe(document.body, {
  childList: true,
  subtree: true
});

CSS:

.bq.timestamp::before {
    content: "";
    position: absolute;
    left: -12px;
    top: 12px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: #a678f1;
    border: 3px solid white;
    box-sizing: border-box;
    z-index: 1;
}
.bq.timestamp {
    border-left: 3px solid #a678f1;
    padding-left: 15px;
    position: relative;
    background-color: hsl(260deg 100% 77% / 10%);
    border-radius: 0px 10px 10px 0px;
    color: #222222;
}

更多样式

PixPin20250618213249.png

JS:

function applyCustomBlockquoteClasses() {
  const rules = [
    { class: "timestamp", regex: /^(?:\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2})?|\d{2}:\d{2}:\d{2})$/ },
    { class: "note", regex: /^NOTE/i },
    { class: "tip", regex: /^TIP/i },
    { class: "bug", regex: /^bug/i },
    { class: "example", regex: /^example/i },
    { class: "question", regex: /^question/i },
    { class: "todo", regex: /^todo/i },
  ];

  const blockquotes = document.querySelectorAll('[data-type="NodeBlockquote"]');

  blockquotes.forEach((bq) => {
    const firstP = bq.querySelector('.p');
    if (!firstP) return;

    const textDiv = firstP.querySelector('div[contenteditable]');
    if (!textDiv) return;

    const text = textDiv.textContent.trim();

    // 清除旧的 class
    rules.forEach((r) => bq.classList.remove(r.class));

    // 添加匹配到的 class
    for (const rule of rules) {
      if (rule.regex.test(text)) {
        bq.classList.add(rule.class);
        break;
      }
    }
  });
}

// 配置选项
const TODO_CONFIG = {
  countOnlyTopLevel: true, // true: 只计算一级任务, false: 计算所有任务
  showSubtaskProgress: true, // 是否在统计中显示子任务信息
  defaultStatsMode: 'top' // 默认显示模式: 'top' 或 'all'
};

// 测试点击事件
function testClickEvent() {
  // 为所有todo块添加测试点击
  const todoBlocks = document.querySelectorAll('.bq.todo');
  
  if (todoBlocks.length === 0) {
    setTimeout(testClickEvent, 500);
    return;
  }
  
  todoBlocks.forEach((block, index) => {
    const firstP = block.querySelector('.p:first-child');
    if (firstP) {
      // todo块已准备就绪,点击事件由主函数处理
    }
  });
}

// 添加点击切换功能
function addTodoStatsToggle() {
  // 移除可能存在的旧事件监听器
  if (window._todoClickHandler) {
    document.removeEventListener('click', window._todoClickHandler);
  }
  
  // 创建新的事件处理器
  window._todoClickHandler = (e) => {
    const todoBlock = e.target.closest('.bq.todo');
    if (!todoBlock) return;
  
    const firstP = todoBlock.querySelector('.p:first-child');
    if (!firstP) return;
  
    // 检查点击是否在统计文字区域
    const rect = firstP.getBoundingClientRect();
    const clickX = e.clientX;
    const clickY = e.clientY;
  
    // 获取统计文字的内容
    const statsText = firstP.style.getPropertyValue('--task-stats');
  
    if (!statsText) return;
  
    // 创建一个临时元素来计算统计文字的实际大小
    const tempSpan = document.createElement('span');
    tempSpan.style.cssText = `
      position: absolute;
      visibility: hidden;
      font-size: 0.85em;
      font-weight: normal;
      white-space: nowrap;
    `;
    tempSpan.textContent = statsText.replace(/"/g, ''); // 移除引号
    document.body.appendChild(tempSpan);
  
    const textRect = tempSpan.getBoundingClientRect();
    const textWidth = textRect.width;
    const textHeight = textRect.height;
    document.body.removeChild(tempSpan);
  
    // 计算统计文字的实际位置
    const statsRight = rect.right - 12; // 距离右边缘12px
    const statsLeft = statsRight - textWidth - 8; // 文字宽度 + padding
    const statsTop = rect.top + (rect.height - textHeight) / 2; // 垂直居中
    const statsBottom = statsTop + textHeight;
  
    // 检查点击是否在统计文字区域内
    const isStatsText = clickX >= statsLeft && clickX <= statsRight && 
                       clickY >= statsTop && clickY <= statsBottom;
  
    if (isStatsText) {
      const currentMode = firstP.dataset.statsMode || TODO_CONFIG.defaultStatsMode;
      const newMode = currentMode === 'top' ? 'all' : 'top';
      firstP.dataset.statsMode = newMode;
  
      updateSingleTodoProgress(todoBlock, newMode);
      e.stopPropagation();
    }
  };
  
  // 添加新的事件监听器
  document.addEventListener('click', window._todoClickHandler);
}

// 更新单个todo块的进度条
function updateSingleTodoProgress(bq, mode = null) {
  const firstP = bq.querySelector('.p:first-child');
  if (!firstP) return;
  
  // 查找任务列表
  const taskList = bq.querySelector('.list');
  if (!taskList) return;
  
  // 确定显示模式
  const displayMode = mode || firstP.dataset.statsMode || TODO_CONFIG.defaultStatsMode;
  
  let allTasks, completedTasks, progress, statsText;
  
  if (displayMode === 'top') {
    // 模式1:只计算一级任务
    allTasks = taskList.querySelectorAll('.li:not(.li .li)');
    completedTasks = taskList.querySelectorAll('.li:not(.li .li).protyle-task--done');
  
    if (allTasks.length === 0) return;
  
    progress = (completedTasks.length / allTasks.length) * 100;
    statsText = `"${completedTasks.length}/${allTasks.length} (${Math.round(progress)}%) [一级]"`;
  
  } else {
    // 模式2:计算所有任务
    allTasks = taskList.querySelectorAll('.li');
    completedTasks = taskList.querySelectorAll('.li.protyle-task--done');
  
    if (allTasks.length === 0) return;
  
    progress = (completedTasks.length / allTasks.length) * 100;
    statsText = `"${completedTasks.length}/${allTasks.length} (${Math.round(progress)}%) [全部]"`;
  }
  
  // 检查是否需要更新(避免不必要的DOM操作)
  const currentProgress = firstP.style.getPropertyValue('--progress');
  const currentStats = firstP.style.getPropertyValue('--task-stats');
  
  if (currentProgress !== `${progress}%` || currentStats !== statsText) {
    // 更新CSS变量
    firstP.style.setProperty('--progress', `${progress}%`);
    firstP.style.setProperty('--task-stats', statsText);
  }
}

// 更新todo块的进度条
function updateTodoProgress() {
  const todoBlockquotes = document.querySelectorAll('.bq.todo');
  
  todoBlockquotes.forEach((bq, index) => {
    updateSingleTodoProgress(bq);
  });
}

// 监听任务状态变化
function observeTaskChanges() {
  const observer = new MutationObserver((mutations) => {
    let shouldUpdate = false;
  
    mutations.forEach((mutation) => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
        const target = mutation.target;
        if (target.classList.contains('li') && 
            (target.classList.contains('protyle-task--done') || 
             !target.classList.contains('protyle-task--done'))) {
          shouldUpdate = true;
        }
      }
    });
  
    if (shouldUpdate) {
      updateTodoProgress();
    }
  });
  
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['class']
  });
}

// 手动测试函数
function manualTest() {
  const todoBlocks = document.querySelectorAll('.bq.todo');
  if (todoBlocks.length > 0) {
    const firstP = todoBlocks[0].querySelector('.p:first-child');
    if (firstP) {
      const currentMode = firstP.dataset.statsMode || TODO_CONFIG.defaultStatsMode;
      const newMode = currentMode === 'top' ? 'all' : 'top';
      firstP.dataset.statsMode = newMode;
      updateSingleTodoProgress(todoBlocks[0], newMode);
    }
  }
}

// 初始加载 & 动态更新
document.addEventListener("DOMContentLoaded", () => {
  applyCustomBlockquoteClasses();
  updateTodoProgress();
  observeTaskChanges();
  addTodoStatsToggle();
  
  // 立即执行一次测试
  testClickEvent();
  
  // 延迟执行测试函数,确保DOM完全加载
  setTimeout(() => {
    testClickEvent();
  }, 1000);
});

// 也监听window的load事件作为备用
window.addEventListener('load', () => {
  setTimeout(() => {
    testClickEvent();
    // 确保点击事件监听器已绑定
    addTodoStatsToggle();
  }, 500);
});

// 立即执行一次(如果DOM已经加载)
if (document.readyState === 'loading') {
  // DOM还在加载中,等待DOMContentLoaded
} else {
  testClickEvent();
  addTodoStatsToggle();
}

// 优化MutationObserver,避免无限循环
let isUpdating = false;
let lastUpdateTime = 0;

new MutationObserver((mutations) => {
  if (isUpdating) return;
  
  // 防止频繁更新
  const now = Date.now();
  if (now - lastUpdateTime < 200) return;
  
  // 检查是否有相关的DOM变化
  const hasRelevantChanges = mutations.some(mutation => {
    // 忽略我们自己的CSS变量变化
    if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
      return false;
    }
  
    return mutation.type === 'childList' || 
           (mutation.type === 'attributes' && 
            (mutation.target.classList.contains('bq') || 
             mutation.target.classList.contains('li')));
  });
  
  if (hasRelevantChanges) {
    isUpdating = true;
    lastUpdateTime = now;
  
    setTimeout(() => {
      applyCustomBlockquoteClasses();
      updateTodoProgress();
      // 重新绑定点击事件
      addTodoStatsToggle();
      isUpdating = false;
    }, 100);
  }
}).observe(document.body, {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ['class']
});




CSS:

/* 时间线 ------------------------------------------------- */
.bq.timestamp::before {
    content: "";
    position: absolute;
    left: -12px;
    top: 12px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background-color: #a678f1;
    border: 3px solid white;
    box-sizing: border-box;
    z-index: 1;
}
.bq.timestamp {
    border-left: 3px solid #a678f1;
    padding-left: 15px;
    position: relative;
    background-color: hsl(260deg 100% 77% / 10%);
    border-radius: 0px 10px 10px 0px;
    color: #222222;
}



/* Callout通用样式 ------------------------------------------------- */
.bq.note,
.bq.tip,
.bq.bug,
.bq.question,
.bq.example,
.bq.todo {
  border: 2px solid;
  border-radius: 10px;
  overflow: hidden;
  margin: .5em 0;
  font-family: sans-serif;
  background-color: transparent;
  padding: 0;
}

.bq.note::before,
.bq.tip::before,
.bq.bug::before,
.bq.question::before,
.bq.example::before,
.bq.todo::before {
  width: 0px;
}

.bq.note > .p:first-child,
.bq.tip > .p:first-child,
.bq.bug > .p:first-child,
.bq.question > .p:first-child,
.bq.example > .p:first-child,
.bq.todo > .p:first-child {
  font-weight: bold;
  padding: 8px 12px;
  border-radius: 0;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
}

.bq.note > .p:first-child::before,
.bq.tip > .p:first-child::before,
.bq.bug > .p:first-child::before,
.bq.question > .p:first-child::before,
.bq.example > .p:first-child::before,
.bq.todo > .p:first-child::before {
  font-size: 1em;
}

.bq.note > *:not(:first-child),
.bq.tip > *:not(:first-child),
.bq.bug > *:not(:first-child),
.bq.question > *:not(:first-child),
.bq.example > *:not(:first-child),
.bq.todo > *:not(:first-child) {
  color: var(--b3-theme-on-background);
  padding: 8px 12px;
  margin: 0;
  border-radius: 0;
  background-color: transparent;
}

/* 个性化样式 ------------------------------------------------- */
/* Note ------------------------------------------------- */
.bq.note {
  border-color: #9ddcd6;
}
.bq.note > .p:first-child {
  background-color: #d4f5f3;
  color: #005f73;
}
.bq.note > .p:first-child::before {
  content: "📝";
}
/* Tip ------------------------------------------------- */
.bq.tip {
  border-color: #b1daff;
}
.bq.tip > .p:first-child {
  background-color: #DEEFFB;
  color: #43596C;
}
.bq.tip > .p:first-child::before {
  content: "💡";
}
/* Bug ------------------------------------------------- */
.bq.bug {
  border-color: #f5a4a4;
}
.bq.bug > .p:first-child {
  background-color: #fde6e6;
  color: #9c1c1c;
}
.bq.bug > .p:first-child::before {
  content: "🐛";
}
/* Question ------------------------------------------------- */
.bq.question {
  border-color: #e0c2ff;
}
.bq.question > .p:first-child {
  background-color: #f1e7ff;
  color: #5e3a8c;
}
.bq.question > .p:first-child::before {
  content: "❓";
}
/* Example ------------------------------------------------- */
.bq.example {
  border-color: #ffd28a;
}
.bq.example > .p:first-child {
  background-color: #fff3dd;
  color: #7b4c00;
}
.bq.example > .p:first-child::before {
  content: "🌟";
}
/* Todo ------------------------------------------------- */
.bq.todo {
  border-color: #ffb366;
}
.bq.todo > .p:first-child {
  background-image:
    linear-gradient(to right, rgba(255, 179, 102, 0.5) var(--progress), transparent var(--progress));
  background-color: #fff2e6;
  color: #cc6600;
  position: relative;
}

.bq.todo > .p:first-child::before {
  content: "✅";
}

.bq.todo > .p:first-child::after {
  content: var(--task-stats, "0/0 (0%)");
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.85em;
  font-weight: normal;
  opacity: 0.8;
  cursor: pointer;
  transition: opacity 0.2s ease;
  padding: 2px 4px;
  border-radius: 3px;
}

.bq.todo > .p:first-child::after:hover {
  opacity: 1;
  background-color: rgba(255, 179, 102, 0.2);
}


  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28446 引用 • 119786 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    285 引用 • 1986 回帖
4 操作
onemo 在 2025-06-18 21:55:31 更新了该帖
onemo 在 2025-06-18 18:20:08 更新了该帖
onemo 在 2025-06-16 23:32:45 更新了该帖
JeffreyChen 在 2025-06-16 18:09:05 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    200 引用 • 545 回帖 • 1 关注
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    188 引用 • 319 回帖 • 222 关注
  • Windows

    Microsoft Windows 是美国微软公司研发的一套操作系统,它问世于 1985 年,起初仅仅是 Microsoft-DOS 模拟环境,后续的系统版本由于微软不断的更新升级,不但易用,也慢慢的成为家家户户人们最喜爱的操作系统。

    232 引用 • 484 回帖
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖 • 1 关注
  • OnlyOffice
    4 引用 • 41 关注
  • frp

    frp 是一个可用于内网穿透的高性能的反向代理应用,支持 TCP、UDP、 HTTP 和 HTTPS 协议。

    17 引用 • 7 回帖 • 1 关注
  • 叶归
    25 引用 • 100 回帖 • 37 关注
  • Netty

    Netty 是一个基于 NIO 的客户端-服务器编程框架,使用 Netty 可以让你快速、简单地开发出一个可维护、高性能的网络应用,例如实现了某种协议的客户、服务端应用。

    49 引用 • 33 回帖 • 63 关注
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 668 关注
  • 星云链

    星云链是一个开源公链,业内简单的将其称为区块链上的谷歌。其实它不仅仅是区块链搜索引擎,一个公链的所有功能,它基本都有,比如你可以用它来开发部署你的去中心化的 APP,你可以在上面编写智能合约,发送交易等等。3 分钟快速接入星云链 (NAS) 测试网

    3 引用 • 16 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    950 引用 • 1460 回帖 • 2 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    293 引用 • 4496 回帖 • 688 关注
  • Mobi.css

    Mobi.css is a lightweight, flexible CSS framework that focus on mobile.

    1 引用 • 6 回帖 • 799 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 740 关注
  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    736 引用 • 1307 回帖 • 2 关注
  • 人工智能

    人工智能(Artificial Intelligence)是研究、开发用于模拟、延伸和扩展人的智能的理论、方法、技术及应用系统的一门技术科学。

    140 引用 • 407 回帖
  • WordPress

    WordPress 是一个使用 PHP 语言开发的博客平台,用户可以在支持 PHP 和 MySQL 数据库的服务器上架设自己的博客。也可以把 WordPress 当作一个内容管理系统(CMS)来使用。WordPress 是一个免费的开源项目,在 GNU 通用公共许可证(GPLv2)下授权发布。

    46 引用 • 114 回帖 • 139 关注
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 224 关注
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 736 关注
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 404 关注
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖 • 2 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 133 关注
  • Wide

    Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。

    欢迎访问我们运维的实例: https://wide.b3log.org

    30 引用 • 218 回帖 • 664 关注
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    188 引用 • 833 回帖 • 1 关注
  • Node.js

    Node.js 是一个基于 Chrome JavaScript 运行时建立的平台, 用于方便地搭建响应速度快、易于扩展的网络应用。Node.js 使用事件驱动, 非阻塞 I/O 模型而得以轻量和高效。

    139 引用 • 269 回帖 • 1 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 51 关注
  • BAE

    百度应用引擎(Baidu App Engine)提供了 PHP、Java、Python 的执行环境,以及云存储、消息服务、云数据库等全面的云服务。它可以让开发者实现自动地部署和管理应用,并且提供动态扩容和负载均衡的运行环境,让开发者不用考虑高成本的运维工作,只需专注于业务逻辑,大大降低了开发者学习和迁移的成本。

    19 引用 • 75 回帖 • 702 关注