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

# 概要

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

（↓こんな感じ）
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/a22f0758-2302-2f2d-100d-3ab856e14a7a.gif)

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

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

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

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


## HTMLファイル

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

```html
<!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`を使ってテキスト取得します。

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

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

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/e130dab2-8ecc-8ba3-eedf-2432240561cb.gif)

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

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


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

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

![Animation3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/8006445b-cdfe-da09-8622-958646e20f5c.gif)

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

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

```js
// リッチテキストをコピー
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/6b3d4d0c-cce9-ebdb-59f9-51ab92dd04a2.gif)

# 問題が発覚

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

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

```html
<!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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/33fd0907-031b-f422-af97-643434d68355.gif)

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

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

![Animation6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/66238e42-bda8-daa7-1033-9183c7e364dc.gif)

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


# プログラム修正

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

```js
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/664bd9b2-42aa-6a58-cc18-8ef943f45931.gif)


# まとめ

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

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

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


# 2024/11/25 追記

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


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/8c28ad55-e1e5-7c58-29de-fb6cdd5ad12e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/473097/e9578325-632b-560a-adc0-d67e62bf53ab.png)