282
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

テキストをコピペするときにスタイルごとコピーされちゃうのってどんな仕組み?

Last updated at Posted at 2024-11-24

概要

文章をコピペしてエクセルに張り付けたときに、画面のスタイルもコピーされてしまって困ったことはありますか?ありますよね!

(↓こんな感じ)
Animation.gif

私もよくやってしまうのですが、実際にどのような処理が行われているのかよく分かっていませんでした。理解を深めるためにも、自分で実装して謎を解いていきたいと思います。

3つパターンの処理を実装

比較のため、プレーンテキスト・HTMLテキスト・リッチテキストのコピー機能をサンプルプログラムを実装してみました。

(リッチテキストのコピーが、範囲選択してコピペしたときと同じ機能を想定しています。)

HTMLファイル

画面表示されるHTMLは下記のような感じです。各コピー処理でid="message"の部分を固定でコピーするようにします。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>コピーサンプル</title>
</head>
<body>

    <div id="message">
        <div style="color:red; font-size: 20px; font-weight: bold;">トマト</div>
        <div style="color:green; font-size: 24px; font-weight: bold;">ピーマン</div>
    </div>

    <div id="buttons">
        <button onclick="copyPlainText()">プレーンテキストコピー</button>
        <button onclick="copyHTML()">HTMLコピー</button>
        <button onclick="copyRichText()">リッチテキストコピー</button>
    </div>
</body>
</html>

1. プレーンテキストをコピーする機能

プレーンテキストをコピーするには、innerTextを使ってテキスト取得します。

// プレーンテキストをコピー
function copyPlainText() {
    const textContent = document.getElementById("message").innerText;
    navigator.clipboard.writeText(textContent);
}

動作はこんな感じになります。ストレスないコピペライフが送れそうです。

Animation2.gif

2. HTMLコンテンツをコピーする機能

HTMLコンテンツをコピーするには、innerHTMLを使います。

// HTMLコンテンツをコピー
function copyHTML() {
    const htmlContent = document.getElementById("message").innerHTML;
    navigator.clipboard.writeText(htmlContent);
}

動作はこんな感じになり、対象のHTMLタグの中身がテキストで取得できました。

Animation3.gif

3. リッチテキストをコピーする機能

さて、本題です。
リッチテキストをコピーするには、innerTextinnerHTMLの両方使うようです。ClipboardItemに定義するとスタイルを維持したままコピーされるみたいです。

// リッチテキストをコピー
function copyRichText() {
    const richContent = document.getElementById("message");
    navigator.clipboard.write([new ClipboardItem({
        "text/plain": new Blob([richContent.innerText], { type: "text/plain" }),
        "text/html": new Blob([richContent.innerHTML], { type: "text/html" })
    })]);
}

動作はこんな感じです。ちゃんとスタイルが維持されてコピーできました。

Animation4.gif

問題が発覚

実はinnerHTMLだと直接範囲選択したときと挙動が違います。

以下のようにインラインでのスタイル指定をやめてみます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>コピーサンプル</title>
    <script type="text/javascript" src="./copy.js"></script>

    <style>
        .tomato {
            color: red;
            font-size: 20px;
            font-weight: bold;
        }
    </style>
</head>
<body>

    <div id="message">
        <div class="tomato">トマト</div>
        <div style="color:green; font-size: 24px; font-weight: bold;">ピーマン</div>
    </div>

    <div id="buttons">
        <button onclick="copyPlainText()">プレーンテキストコピー</button>
        <button onclick="copyHTML()">HTMLコピー</button>
        <button onclick="copyRichText()">リッチテキストコピー</button>
    </div>

</body>
</html>

実装した機能でコピペした場合

インラインで定義されていない部分のスタイルがコピペできていません。

Animation5.gif

範囲選択してコピペした場合

実装した機能を使わず範囲選択でコピペしたときは、正しくスタイルごとコピペできています。

Animation6.gif

同じ挙動をするように直していきたいと思います。

プログラム修正

document.styleSheetsで適応されているスタイルを取得し、それを利用することでスタイルを維持するように修正しました。

function copyRichText() {
    const richContent = document.getElementById("message");
    
    // コピーするコンテンツを複製
    const clone = richContent.cloneNode(true);

    // スタイルを抽出して適用
    const styleSheets = Array.from(document.styleSheets);

    let styleContent = "";
    styleSheets.forEach(sheet => {
        const rules = Array.from(sheet.cssRules || []);
        rules.forEach(rule => {
            styleContent += rule.cssText + "\n";
        });
    });

    // 抽出したスタイルを<style>タグに追加
    const styleTag = document.createElement("style");
    styleTag.textContent = styleContent;

    // 新しいHTML構造を作成
    const container = document.createElement("div");
    container.appendChild(styleTag);
    container.appendChild(clone);

    // HTMLコンテンツをコピー
    navigator.clipboard.write([new ClipboardItem({
        "text/plain": new Blob([richContent.innerText], { type: "text/plain" }),
        "text/html": new Blob([container.innerHTML], { type: "text/html" })
    })]);
}

実際の動作です。範囲選択してコピペしたときと同じ動きにすることができました。
(範囲選択してコピペしたときに、この処理をしているかは不明です。)

Animation7.gif

まとめ

実際にリッチテキストのコピーを含めた3つの機能を実装してみました。
これでリッチテキストの謎が少し解けてきましたが、まだわかっていないことが多いです。

クリップボードの保存先保存したリッチテキストの参照方法 も調べてみたいなと思いました。

ここまで読んでいただき、ありがとうございました。

2024/11/25 追記

見直していたら、「フォント」と「セルの折り返し」に少し違いがあることを発見しました。クリップボード奥が深いですね...

image.png

image.png

282
176
9

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
282
176

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?