Soanguy / Typora-Theme-Neumorphism

新拟态风格的 typora 主题
37 stars 1 forks source link

通过 JS 实现一键复制代码块代码功能 #4

Open jinghu-moon opened 1 year ago

jinghu-moon commented 1 year ago

感觉这个功能挺不错的。是否可以实现呢?

jinghu-moon commented 1 year ago

在网上找了找。东拼西凑勉强实现了这个功能。以下是我的代码。

var codeblocks = document.getElementsByClassName('md-fences')
for (var i = 0; i < codeblocks.length; i++) {
  //显示 复制代码 按钮
  currentCode = codeblocks[i]
  currentCode.style = "position: relative;"
  var copy = document.createElement('div')
  copy.style = "position: absolute;right: 4px;\
  top: 4px;background-color: white;padding: 2px 8px;\
  margin: 8px;border-radius: 4px;cursor: pointer;\
  z-index: 9999;\
  box-shadow: 0 2px 4px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.05);"
  copy.innerHTML = "复制"
  currentCode.append(copy)
  //让所有 "复制"按钮 全部隐藏
  copy.style.visibility = "hidden"
  console.log(copy)
}

for (var i = 0; i < codeblocks.length; i++) {
  !function (i) {
    //鼠标移到代码块,就显示按钮
    codeblocks[i].onmouseover = function () {
      codeblocks[i].childNodes[1].style.visibility = "visible"
    }

    //执行 复制代码 功能
    function copyArticle(event) {
      const range = document.createRange();

      //范围是 code,不包括刚才创建的div
      range.selectNode(codeblocks[i].childNodes[0]);

      const selection = window.getSelection();
      if (selection.rangeCount > 0) selection.removeAllRanges();
      selection.addRange(range);
      document.execCommand('copy');

      codeblocks[i].childNodes[1].innerHTML = "复制成功"
      setTimeout(function () {
        codeblocks[i].childNodes[1].innerHTML = "复制"
      }, 1000);
      //清除选择区
      if (selection.rangeCount > 0) selection.removeAllRanges(); 0
    }
    codeblocks[i].childNodes[1].addEventListener('click', copyArticle, false);

  }(i);

  !function (i) {
    //鼠标从代码块移开 则不显示复制代码按钮
    codeblocks[i].onmouseout = function () {
      codeblocks[i].childNodes[1].style.visibility = "hidden"
    }
  }(i);
}

我按照你实现自定义鼠标样式的方式,引入了 js 文件。却没有用。必要在控制台执行一遍才行。老哥知道怎么解决吗?我才开始学习 js。

Soanguy commented 1 year ago

我不太会 JS。不过我在网络上找到一个创建复制按钮相关功能的文章。或许会对你目前的情况有一定的用处。

https://www.cnblogs.com/fenggwsx/p/14366094.html

magiceses commented 5 months ago

@jinghu-moon 请问这个怎么去除左侧行号部分?

jinghu-moon commented 5 months ago

@jinghu-moon 请问这个怎么去除左侧行号部分?

  1. 找到 Typora 安装路径,进入 /resources 文件夹。
  2. 打开 window.html
    1. 若是新版本:搜索文件内容 src="./appsrc/window/frame.js"
    2. 若是旧版本:搜索文件内容 src="./app/window/frame.js"
    3. appsrc 目录, Typora 为新版本,无则旧版本。
  3. 在上述搜索内容的 后面 加入 <script src="./code-block-enhanced.js" defer="defer"></script>,保存文件。
  4. code-block-enhanced.js 文件放入 /resources 文件夹。重启 Typora 即可。

此时代码块具有折叠、显示编程语言、备注(代码块语言输入框中空格输入,例如 Java 这是一段测试代码)、刷新、复制代码功能。

后续我也会在我的 jinghu-moon/typora-see-yue-theme: 主题中加入该功能。

code-block-enhanced.js
(() => {

  // 配置项
  const config = {
    ENABLE: true,  // 启用脚本,若为 false,以下配置全部失效
    LOOP_DETECT_INTERVAL: 20,  // 循环检测间隔,单位:毫秒
    CLICK_CHECK_INTERVAL: 300,  // 点击检测间隔,单位:毫秒
    LANG_ENABLED: true,  // 代码块显示编程语言功能开关
    INTRO_ENABLED: true,  // 代码块刷新功能开关
    COPY_ENABLED: true,  // 代码块复制功能开关
    FOLD_ENABLED: true,  // 代码块折叠功能开关
    REFRESH_ENABLED: true,  // 代码块刷新功能开关
    MAX_LINE_NUM: 10,  // 代码块最大行数,超过自动折叠
  };

  // 如果脚本未启用,直接返回
  if (!config.ENABLE) return;

  // 常用编程语言字母和对应的标准格式
  const codeLangList = {
    "ABAP": "ABAP",
    "APL": "APL",
    "ASCIIARMOR": "ASCII Armor",
    "ASN1": "ASN.1",
    "ASP": "ASP",
    "ASSEMBLY": "Assembly",
    "BASH": "Bash",
    "BASIC": "BASIC",
    "Batch": "Batch",
    "C": "C",
    "CSHARP": "C#",
    "CPP": "C++",
    "CASSANDRA": "Cassandra",
    "CEYLON": "Ceylon",
    "CLIKE": "C-like",
    "CLOJURE": "Clojure",
    "CMAKE": "CMake",
    "CMD": "CMD",
    "COBOL": "COBOL",
    "COFFEESCRIPT": "CoffeeScript",
    "COMMONLISP": "Common Lisp",
    "CPP": "CPP",
    "CQL": "CQL",
    "CRYSTAL": "Crystal",
    "CSHARP": "CSharp",
    "CSS": "CSS",
    "CYPHER": "Cypher",
    "CYTHON": "Cython",
    "D": "D",
    "DART": "Dart",
    "DIFF": "Diff",
    "DJANGO": "Django",
    "DOCKERFILE": "Dockerfile",
    "DTD": "DTD",
    "DYLAN": "Dylan",
    "EJS": "EJS",
    "ELIXIR": "Elixir",
    "ELM": "Elm",
    "EMBEDDEDJS": "EmbeddedJS",
    "ERB": "ERB",
    "ERLANG": "Erlang",
    "FSHARP": "F#",
    "FLOW": "Flow",
    "FORTH": "Forth",
    "FORTRAN": "Fortran",
    "FSHARP": "FSharp",
    "GAS": "Gas",
    "GFM": "GFM",
    "GHERKIN": "Gherkin",
    "GLSL": "GLSL",
    "GO": "Go",
    "GROOVY": "Groovy",
    "HANDLEBARS": "Handlebars",
    "HASKELL": "Haskell",
    "HAXE": "Haxe",
    "HIVE": "Hive",
    "HTACCESS": "htaccess",
    "HTML": "HTML",
    "HTTP": "HTTP",
    "HXML": "HXML",
    "IDL": "IDL",
    "INI": "INI",
    "JADE": "Jade",
    "JAVA": "Java",
    "JAVASCRIPT": "JavaScript",
    "JINJA2": "Jinja2",
    "JS": "JavaScript",
    "JSON": "JSON",
    "JSP": "JSP",
    "JSX": "JSX",
    "JULIA": "Julia",
    "KOTLIN": "Kotlin",
    "LATEX": "LaTeX",
    "LESS": "Less",
    "LISP": "Lisp",
    "LIVESCRIPT": "LiveScript",
    "LUA": "Lua",
    "MAKEFILE": "Makefile",
    "MARIADB": "MariaDB",
    "MARKDOWN": "Markdown",
    "MATHEMATICA": "Mathematica",
    "MATLAB": "MATLAB",
    "MBOX": "Mbox",
    "MERMAID": "Mermaid",
    "MODELICA": "Modelica",
    "MSSQL": "MSSQL",
    "MYSQL": "MySQL",
    "NGINX": "Nginx",
    "NIM": "Nim",
    "NSIS": "NSIS",
    "objc": "Objective-C",
    "OBJECTIVE-C": "Objective-C",
    "OCAML": "OCaml",
    "OCTAVE": "Octave",
    "OZ": "Oz",
    "PASCAL": "Pascal",
    "PEGJS": "PEG.js",
    "PERL": "Perl",
    "PERL6": "Perl6",
    "PGP": "PGP",
    "PHP": "PHP",
    "PHPHTML": "PHP+HTML",
    "PLSQL": "PL/SQL",
    "POSTGRESQL": "PostgreSQL",
    "POWERSHELL": "PowerShell",
    "PROPERTIES": "Properties",
    "PROTOBUF": "Protocol Buffers",
    "PSEUDOCODE": "Pseudocode",
    "PUG": "Pug",
    "PYTHON": "Python",
    "Q": "Q",
    "R": "R",
    "REACT": "React",
    "RESTRUCTUREDTEXT": "reStructuredText",
    "RST": "RST",
    "RUBY": "Ruby",
    "RUST": "Rust",
    "SAS": "SAS",
    "SCALA": "Scala",
    "SCHEME": "Scheme",
    "SCSS": "SCSS",
    "SEQUENCE": "Sequence",
    "SH": "Shell",
    "SHELL": "Shell",
    "SMALLTALK": "Smalltalk",
    "SMARTY": "Smarty",
    "SOLIDITY": "Solidity",
    "SPARQL": "SPARQL",
    "SPREADSHEET": "Spreadsheet",
    "SQL": "SQL",
    "SQLITE": "SQLite",
    "SQUIRREL": "Squirrel",
    "STATA": "Stata",
    "STYLUS": "Stylus",
    "SVELTE": "Svelte",
    "SWIFT": "Swift",
    "SYSTEMVERILOG": "SystemVerilog",
    "TCL": "Tcl",
    "TEX": "TeX",
    "TIDDLYWIKI": "TiddlyWiki",
    "TIKIWIki": "Tiki Wiki",
    "TOML": "TOML",
    "TS": "TypeScript",
    "TSX": "TSX",
    "TURTLE": "Turtle",
    "TWIG": "Twig",
    "TYPESCRIPT": "TypeScript",
    "V": "V",
    "VB": "VB",
    "VBSCRIPT": "VBScript",
    "VELOCITY": "Velocity",
    "VERILOG": "Verilog",
    "VHDL": "VHDL",
    "VISUALBASIC": "Visual Basic",
    "VUE": "Vue",
    "WEBIDL": "Web IDL",
    "WIKI": "Wi",
    "XAML": "XAML",
    "XML": "XML",
    "XMLDTD": "XML DTD",
    "XQUERY": "XQuery",
    "YACAS": "Yacas",
    "YAML": "YAML",
    "YARA": "YARA"
  };

  // 创建并配置通用类型的标签/按钮
  const createElement = (type, className, hint, contentOrIconName, isButton = false) => {
    const element = document.createElement(type);
    element.className = className;
    element.setAttribute("ty-hint", hint);
    element.innerHTML = isButton ? `<i class="iconfont icon-${contentOrIconName}"></i>` : contentOrIconName;
    return element;
  };

  // 增强代码块功能
  const enhanceCodeBlock = (target, language) => {
    if (!config.ENABLE) return;

    const codeBlockEnhanced = document.createElement("div");
    codeBlockEnhanced.classList.add("code-enhanced");

    // 功能按钮和标签的创建逻辑
    const features = [
      { enabled: config.FOLD_ENABLED, className: 'fold', hint: "折叠代码块", iconName: 'regular-down' },
      { enabled: config.REFRESH_ENABLED, className: 'refresh', hint: "刷新代码块", iconName: 'shuaxin5' },
      { enabled: config.COPY_ENABLED, className: 'copy', hint: "复制代码", iconName: 'clipboard' },
    ];

    features.forEach(({ enabled, className, hint, iconName }) => {
      codeBlockEnhanced.appendChild(enabled ? createElement("div", `code-block-${className}`, hint, iconName, true) : document.createDocumentFragment());
    });

    // language 以第一个空格为界限,前部分为 codeLang,后部分为 codeIntro
    const codeLang = config.LANG_ENABLED ? language.split(' ')[0] : '';
    const codeIntro = config.INTRO_ENABLED ? language.replace(/^\S+\s*/, '').trim() : '';
    codeBlockEnhanced.appendChild(config.LANG_ENABLED ? createElement('div', "code-block-lang", "编程语言", getCodeLangFullName(codeLang)) : document.createDocumentFragment());
    codeBlockEnhanced.appendChild(config.INTRO_ENABLED ? createElement('div', "code-block-intro", "代码块说明", codeIntro) : document.createDocumentFragment());

    // 设置滚动容器高度
    const scroll = target.querySelector(".CodeMirror-scroll");
    if (scroll) scroll.style.height = `${scroll.scrollHeight}px`;

    target.appendChild(codeBlockEnhanced);

    // 自动折叠过长代码块
    // const codeBlockLines = target.querySelectorAll(".CodeMirror-scroll .CodeMirror-code .CodeMirror-line").length;
    // if (codeBlockLines > config.MAX_LINE_NUM) {
    //   const foldButton = target.querySelector(".code-block-fold i");
    //   if (foldButton) {
    //     foldCodeBlock(null, foldButton); // 模拟点击折叠按钮
    //   }
    // }
  };

  // 获取编程语言的标准形式
  const getCodeLangFullName = (language) => codeLangList[language.toUpperCase()] || language.toUpperCase();

  // 增强新添加的代码块
  const enhanceNewCodeBlock = (codeBlock, language) => {
    if (!codeBlock.querySelector('.code-enhanced')) enhanceCodeBlock(codeBlock, language);
  }

  // 装饰器函数,用于动态修改 File.editor.fences.addCodeBlock 函数的行为
  const decorateAddCodeBlock = () => {
    if (File?.editor?.fences?.addCodeBlock) {
      const original = File.editor.fences.addCodeBlock;
      File.editor.fences.addCodeBlock = function (...args) {
        const result = original.apply(this, args);
        const [cid] = args;
        const codeBlock = document.querySelector(`pre.md-fences[cid="${cid}"]`);
        if (codeBlock) {
          const language = codeBlock.getAttribute('lang');
          enhanceNewCodeBlock(codeBlock, language);
        }
        return result;
      }
      observer.disconnect(); // 目标函数可用,停止观察
    }
  }

  // 使用 MutationObserver 监视 DOM 变化
  const writeContainer = document.querySelector('#write');
  const observer = new MutationObserver(decorateAddCodeBlock);
  observer.observe(writeContainer, { childList: true, subtree: true });

  // 设置点击事件监听器
  document.getElementById("write").addEventListener("click", (ev) => {
    const { target } = ev;

    const handleAction = (actionFunction) => (ev) => {
      ev.preventDefault();
      ev.stopPropagation();
      actionFunction(ev, ev.target);
    };

    config.COPY_ENABLED && target.closest(".code-block-copy") && handleAction(copyCodeBlock)(ev);
    config.FOLD_ENABLED && target.closest(".code-block-fold") && handleAction(foldCodeBlock)(ev);
    config.REFRESH_ENABLED && target.closest(".code-block-refresh") && handleAction(refreshCodeBlock)(ev);
  });

  // 成功反馈功能(刷新、复制按钮)
  const successFeedback = (type, button) => {
    button.style.visibility = 'hidden';

    const parentRect = button.parentNode.getBoundingClientRect();
    const buttonRect = button.getBoundingClientRect();
    const successIcon = document.createElement('i');
    successIcon.className = `iconfont icon-check code-block-${type}`;
    successIcon.style.position = 'absolute';
    successIcon.style.left = `${buttonRect.left - parentRect.left}px`;
    successIcon.style.top = `${buttonRect.top - parentRect.top}px`;
    button.parentNode.insertBefore(successIcon, button);
    setTimeout(() => {
      successIcon.remove();
      button.style.visibility = 'visible';
    }, 1000);
  };

  // 折叠代码块功能
  const foldCodeBlock = (ev, foldButton) => {
    document.activeElement.blur();

    const scrollArea = foldButton.closest(".md-fences").querySelector(".CodeMirror-scroll");
    const lineHeight = window.getComputedStyle(foldButton).lineHeight;

    const isCollapsed = scrollArea.classList.contains('folded');
    const [newHeight, newOverflowY] = isCollapsed ? [scrollArea.scrollHeight + 'px', ''] : [lineHeight, 'hidden'];

    // 切换折叠状态并更新样式
    [scrollArea, foldButton].forEach(el => el.classList.toggle('folded'));

    foldButton.style.transform = `rotate(${!isCollapsed ? -90 : 0}deg)`;
    foldButton.style.transition = 'transform 0.3s ease-in-out';

    scrollArea.style.height = `${parseFloat(newHeight) + (!isCollapsed ? 3.2 : 0)}px`;
    scrollArea.style.overflowY = newOverflowY;
    scrollArea.style.transition = 'height 0.3s ease-in-out, overflow-y 0.3s ease-in-out';
  };

  let lastClickTime = 0;

  // 刷新代码块功能(代码块高度、编程语言、说明)
  const refreshCodeBlock = (ev, refreshButton) => {
    document.activeElement.blur();
    if (ev.timeStamp - lastClickTime < config.CLICK_CHECK_INTERVAL) return;
    lastClickTime = ev.timeStamp;

    const fenceContainer = refreshButton.closest(".md-fences");
    const codeBlockMsg = fenceContainer.getAttribute("lang");
    const codeLang = codeBlockMsg.split(' ')[0];
    const codeIntro = codeBlockMsg.replace(/^\S+\s*/, '').trim();
    const codeLangTag = fenceContainer.querySelector(".code-block-lang");
    const codeIntroTag = fenceContainer.querySelector(".code-block-intro");
    const scroll = fenceContainer.querySelector(".CodeMirror-scroll");

    if (codeLangTag) codeLangTag.innerHTML = getCodeLangFullName(codeLang);
    if (codeIntroTag) codeIntroTag.textContent = codeIntro || '';

    if (scroll) {
      // 获取实际内容的高度
      const contentHeight = scroll.querySelector(".CodeMirror-code").scrollHeight;

      scroll.style.transition = 'height 0.3s ease-in-out, overflow-y 0.3s ease-in-out';
      // 设置高度为实际内容高度
      scroll.style.height = `${contentHeight}px`;
      scroll.classList.remove('folded');

      // 移除折叠按钮的折叠状态
      const foldButton = fenceContainer.querySelector(".code-block-fold .folded");
      if (foldButton) {
        foldButton.classList.remove('folded');
        foldButton.style.transform = 'rotate(0deg)';
      }

      // 移除溢出设置
      scroll.style.overflowY = '';
    }

    successFeedback("refresh", refreshButton);
  };

  // 复制代码功能
  const copyCodeBlock = async (ev, copyButton) => {
    document.activeElement.blur();
    if (ev.timeStamp - lastClickTime < config.CLICK_CHECK_INTERVAL) return;
    lastClickTime = ev.timeStamp;

    const fenceContainer = copyButton.closest(".md-fences");
    const lines = fenceContainer.querySelectorAll(".CodeMirror-code .CodeMirror-line");
    if (!lines.length) return;

    let content = '';
    const badCharReplacements = {
      '\u200b': '',      // 零宽空格
      '\u00A0': ' ',     // 不换行空格
      '\n': '\n'         // 保持换行符
    };
    const badCharRegex = new RegExp(`[${Object.keys(badCharReplacements).join('')}]`, 'g');
    lines.forEach(line => {
      content += line.textContent.replace(badCharRegex, char => badCharReplacements[char]) + '\n';
    });
    content = content.slice(0, -1); // 移除最后一个多余的换行符
    navigator.clipboard.writeText(content);

    successFeedback("copy", copyButton);
  };

  // 监听是否导出,如果有,就展开所有已折叠的代码块
  const exportObserver = new MutationObserver(mutationsList => {
    mutationsList.forEach(mutation => {
      if (mutation.attributeName === 'class' && document.body.classList.contains('ty-show-notification')) {
        document.querySelectorAll('.md-fences .CodeMirror-scroll.folded').forEach(block => {
          const foldButton = block.closest('.md-fences').querySelector('.code-block-fold i');
          foldCodeBlock(null, foldButton);
        });
      }
    });
  });

  exportObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });

  console.log("代码块增加插件加载完成!");
})();
magiceses commented 5 months ago

好像不兼容 macos

jinghu-moon commented 5 months ago
  1. 代码块工具栏图标为小方框
  2. 不显示代码块工具栏

如果是 1,下载附件 iconfont.woff2,在主题中引用该字体。

如果是 2,请自行修改。我没有 Mac 电脑,无能为力。 iconfont.zip