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/