Cocoon のコードブロックにファイル名表示と折り返しボタンを追加

Cocoon のコードブロックの機能改善、第三段
前回に続き、今回はファイル名表示と折り返しボタンの追加に挑戦した。

前回の記事:

完成後の表示イメージ:


ファイル名表示時の投稿の書き方

ファイル名の表示は、コードブロックの直前に filename: から始まる行があるときに、その行を消して、ファイル名として表示する仕様に。


エレメント構成について

ボタン、copied メッセージ、ファイル名表示を追加するため、以下のように構成を変更した。
<code> の innerHTML は、ハイライト表示に影響するため、触らずにその周りだけ、組み替えとした。

エレメント構成(Before):

<pre>
  <code>...</code>
</pre>

エレメント構成(After):

<div>   <!--  copied メッセージ表示とボタン表示のための container -->
  <pre>    <!-- 表示エリアや背景色はこのpreタグに依存 -->
     <div>    <!-- ファイル名表示時のみ追加 -->
       <span>...</span>   <!-- ファイル名テキスト -->
       <button><svg>...</svg></button>    <!-- copy button -->
     </div>
     <div>
       <code>...</code>
       <div></div>   <!-- スクロール表示の場合に、右側にボタン分のスペースをつくる -->
     </div>
  </pre>
  <div>    <!-- ボタン格納用 -->
    <button><svg>...</svg></button>    <!-- copy button -->
    <button><svg>...</svg></button>    <!-- wrap button -->
  </div>
  <div>...</div>  <!-- copied メッセージ表示時に追加 -->
</div>

JavaScript:

document.addEventListener("DOMContentLoaded", () => {
    const svgImageCopy = `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--> 
<path d="M464 0H144c-26.5 0-48 21.5-48 48v48H48c-26.5 0-48 21.5-48 48v320c0 26.5 21.5 48 48 48h320c26.5 0 48-21.5 48-48v-48h48c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM362 464H54a6 6 0 0 1 -6-6V150a6 6 0 0 1 6-6h42v224c0 26.5 21.5 48 48 48h224v42a6 6 0 0 1 -6 6zm96-96H150a6 6 0 0 1 -6-6V54a6 6 0 0 1 6-6h308a6 6 0 0 1 6 6v308a6 6 0 0 1 -6 6z"/></svg>`;

    const svgImageWrapToggle = `
<svg viewBox="0 0 300 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><g id="g181" transform="matrix(1.5327141,0,0,1.547751,8.455392,56.771369)"><g id="g175"><path d="m 65.93,0 41.93,28.89 -41.93,28.9 V 39.66 c -24.53,0 -44.41,19.88 -44.41,44.41 0,24.53 19.88,44.41 44.41,44.41 H 150 V 150 H 65.93 C 29.52,150 0,120.48 0,84.07 0,47.65 29.52,18.13 65.93,18.13 Z" stroke-miterlimit="10" transform="rotate(180,75,75)" pointer-events="all" id="path173" /></g><g id="g179"><rect x="166" y="20" width="20" height="100" id="rect177" /></g></g></svg>`;

    function createSvgElement(htmlText, width, height) {
        const element = new DOMParser().parseFromString(htmlText, "text/html").body.firstElementChild;
        element.style.fill = "rgb(200, 200, 200)";
        element.style.opacity = "0.8";
        element.style.width = width;
        element.style.height = height;
        return element;
    }

    function createTransparentButton(title, width, height) {
        const buttonElement = document.createElement("button");
        buttonElement.style.width = width;
        buttonElement.style.height = height;
        buttonElement.style.border = "none";
        buttonElement.style.background = "transparent";
        buttonElement.style.cursor = "pointer";
        buttonElement.style.display = "inline";
        buttonElement.title = title;

        // Add push effect
        buttonElement.addEventListener("mousedown", () => {
            buttonElement.style.transform = "translateY(2px)";
        });

        buttonElement.addEventListener("mouseup", () => {
            buttonElement.style.transform = "translateY(0)";
        });

        buttonElement.addEventListener("mouseleave", () => {
            buttonElement.style.transform = "translateY(0)";
        });
        return buttonElement;
    };


    const svgElementCopy = createSvgElement(svgImageCopy, "20px", "20px");    // copy button icon
    const svgElementCopy2 = createSvgElement(svgImageCopy, "15px", "15px");   // filename box copy button icon
    const svgElementWrapToggle = createSvgElement(svgImageWrapToggle, "20px", "20px");  // wrap button icon

    svgElementCopy2.style.marginTop = "1px";   // Adjustment

    // Get all <pre> elements
    const preElements = document.querySelectorAll("pre");

    preElements.forEach(pre => {
        const code = pre.querySelector("code");

        if (code) {
            let filename = null;

            // Add spacing to the right of the <code> block
            const spacer = document.createElement("div");
            spacer.style.width = "70px";
            spacer.style.flexShrink = "0";

            const codeContainer = document.createElement("div");
            codeContainer.style.display = "flex";
            codeContainer.style.alignItems = "stretch";

            code.parentNode.insertBefore(codeContainer, code);
            codeContainer.appendChild(code);
            codeContainer.appendChild(spacer);

            // Get filename if exists
            const previousPElement = pre.previousElementSibling;
            if (previousPElement && previousPElement.tagName.toLowerCase() === 'p') {
                const textContent = previousPElement.textContent;
                const match = textContent.match(/^filename:\s*(.*)$/i);
                if (match) {
                    filename = match[1];
                    previousPElement.remove();
                }
            }

            // Create a filename display
            if (filename) {
                const filenameDisplay = document.createElement("div");
                filenameDisplay.style.position = "absolute";
                filenameDisplay.style.top = "1px";
                filenameDisplay.style.left = "18px";
                filenameDisplay.style.height = "28px";
                filenameDisplay.style.backgroundColor = "#555";
                filenameDisplay.style.color = "white";
                filenameDisplay.style.padding = "2px 10px";
                filenameDisplay.style.borderRadius = "3px";
                filenameDisplay.style.fontSize = "14px";
                filenameDisplay.style.zIndex = "10";
                pre.style.paddingTop = "30px";

                const filenameSpan = document.createElement("span");
                filenameSpan.textContent = filename;
                filenameSpan.style.verticalAlign = "middle";

                const filenameCopyButton = createTransparentButton("Copy to clipboard", "17px", "17px");
                filenameCopyButton.style.verticalAlign = "middle";
                filenameCopyButton.style.marginLeft = "15px";

                filenameCopyButton.addEventListener("click", () => {
                    navigator.clipboard.writeText(filename).then(() => {
                        const copiedMessage = document.createElement("div");
                        copiedMessage.textContent = "Copied!";
                        copiedMessage.style.position = "absolute";
                        copiedMessage.style.top = "-31px";
                        copiedMessage.style.right = "5px";
                        copiedMessage.style.backgroundColor = "#444";
                        copiedMessage.style.color = "white";
                        copiedMessage.style.padding = "5px 10px";
                        copiedMessage.style.borderRadius = "3px";
                        copiedMessage.style.fontFamily = "monospace";
                        copiedMessage.style.fontSize = "12px";
                        copiedMessage.style.letterSpacing = "0.03rem";
                        copiedMessage.style.lineHeight = "20px";
                        copiedMessage.style.zIndex = "10";
                        copiedMessage.style.opacity = "1";
                        copiedMessage.style.transition = "opacity 0.5s";

                        filenameDisplay.appendChild(copiedMessage);

                        setTimeout(() => {
                            copiedMessage.style.opacity = "0";
                            copiedMessage.addEventListener("transitionend", () => {
                                copiedMessage.remove();
                            });
                        }, 1500);
                    }).catch(err => {
                        console.error("Failed to copy", err);
                    });
                });

                filenameCopyButton.appendChild(svgElementCopy2.cloneNode(true));

                filenameDisplay.appendChild(filenameSpan);
                filenameDisplay.appendChild(filenameCopyButton);
                pre.insertBefore(filenameDisplay, pre.firstChild);
            }

            // Create a copy button
            const copyButton = createTransparentButton("Copy to clipboard", "35px", "35px");
            copyButton.appendChild(svgElementCopy.cloneNode(true));

            copyButton.addEventListener("click", () => {
                navigator.clipboard.writeText(code.textContent).then(() => {
                    const copiedMessage = document.createElement("div");
                    copiedMessage.textContent = "Copied!";
                    copiedMessage.style.position = "absolute";
                    copiedMessage.style.top = "-30px";
                    copiedMessage.style.right = "5px";
                    copiedMessage.style.backgroundColor = "#444";
                    copiedMessage.style.color = "white";
                    copiedMessage.style.padding = "5px 10px";
                    copiedMessage.style.borderRadius = "3px";
                    copiedMessage.style.fontFamily = "monospace";
                    copiedMessage.style.fontSize = "12px";
                    copiedMessage.style.letterSpacing = "0.03rem";
                    copiedMessage.style.lineHeight = "20px";
                    copiedMessage.style.zIndex = "10";
                    copiedMessage.style.opacity = "1";
                    copiedMessage.style.transition = "opacity 0.5s";

                    pre.parentNode.appendChild(copiedMessage);

                    setTimeout(() => {
                        copiedMessage.style.opacity = "0";
                        copiedMessage.addEventListener("transitionend", () => {
                            copiedMessage.remove();
                        });
                    }, 1500);
                }).catch(err => {
                    console.error("Failed to copy", err);
                });
            });

            // Create a wrap toggle button
            const wrapToggleButton = createTransparentButton("Toggle wrap", "35px", "35px");
            wrapToggleButton.appendChild(svgElementWrapToggle.cloneNode(true));

            let isWrapped = false;
            wrapToggleButton.addEventListener("click", () => {
                isWrapped = !isWrapped;
                code.style.whiteSpace = isWrapped ? "pre-wrap" : "pre";
                spacer.style.display = isWrapped ? "none" : "block";
            });

            // Create a container for buttons
            const buttonContainer = document.createElement("div");
            buttonContainer.style.position = "absolute";
            buttonContainer.style.top = "7px";
            buttonContainer.style.right = "7px";
            buttonContainer.style.display = "flex";
            buttonContainer.style.gap = "5px";

            // Append buttons to container
            buttonContainer.appendChild(wrapToggleButton);
            buttonContainer.appendChild(copyButton);

            // To maintain the position of the copy button, wrap the <pre> element in a div element
            const container = document.createElement("div");
            container.style.position = "relative";
            container.style.width = "100%";

            pre.parentNode.insertBefore(container, pre);
            container.appendChild(pre);
            container.appendChild(buttonContainer);
        }
    });
});

JavaScript は「外観」→「テーマファイルエディター」→「javascript.js」から追加


コードブロックの機能、デザインについて、Zenn と Qiita を参考にさせていただきました。
https://zenn.dev/
https://qiita.com/