上記の例だと css nesting とか入れると うまくいかないのに気が付いたので
実際の動作確認する場合のメモ
確認方法
上記のサンプルのスタイルを
& {
.tomato {
color: red;
font-size: 20px;
font-weight: bold;
}
}
にする
上記の適用を行ったサンプルとして以下に playground を用意しておきます。
確認する分には paste イベントの DataTransfer 読めば良さそうですね。
Pasteイベントからクリップボード の 確認する用のサンプルとして下記を用意
See the Pen use DataTranscfer example ver.20240907 by juner clarinet (@juner) on CodePen.
確認結果としては以下
リッチテキストコピー
<html>
<body>
<!--StartFragment--><html><head><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></body></html><!--EndFragment-->
</body>
</html>
直接選択してコピーした場合
<html>
<body>
<!--StartFragment--><div class="tomato" style="color: red; font-size: 20px; font-weight: bold; font-family: Meiryo; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">トマト</div><div style="font-family: Meiryo; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; color: green; font-size: 24px; font-weight: bold;">ピーマン</div><!--EndFragment-->
</body>
</html>
スタイルシートが CSS nesting を考慮していなくて壊れていますね……。
具体的にはこの適用部分か。
// スタイルを抽出して適用
const styleSheets = Array.from(document.styleSheets);
let styleContent = "";
styleSheets.forEach(sheet => {
const rules = Array.from(sheet.cssRules || []);
rules.forEach(rule => {
styleContent += rule.cssText + "\n";
});
});
この感じから言うと getComputedStyle
でスタイルを取得して style 属性として設定している感じですね。
なので下記の様な修正が必要と思われます。
// スタイルを抽出して適用
{
const a1 = richContent.querySelectorAll("*");
const a2 = clone.querySelectorAll("*");
for (let i = 0, max = a1.length; i < max; i++) {
const e1 = a1[i];
const e2 = a2[i];
const styles = globalThis.getComputedStyle(e1);
for (const key of styles) {
if ((styles[key] ?? "") === "") continue;
e2.style[key] = styles[key];
}
}
}
対応した playground は下記の様な感じです
以上。
確認環境
Microsoft Edge for Business
バージョン 131.0.2903.70 (64ビット)
補足
上記の方法だと下記の様な text/html がコピーされるのでまぁ、更に絞り込む必要がある気はします。
多分 一般的にコピーに必要のないスタイルは除外とかしているのではないでしょうか……?
最終的に出力されたtext/html
<html>
<body>
<!--StartFragment--><html><head></head><body><div id="message">
<div class="tomato" style="accent-color: auto; place-content: normal; place-items: normal; place-self: auto; alignment-baseline: auto; anchor-name: none; anchor-scope: none; animation-composition: replace; animation: 0s ease 0s 1 normal none running none; app-region: none; appearance: none; backdrop-filter: none; backface-visibility: visible; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); background-blend-mode: normal; baseline-shift: 0px; baseline-source: auto; block-size: 30px; border-block-end: 0px none rgb(255, 0, 0); border-block-start: 0px none rgb(255, 0, 0); border-color: rgb(255, 0, 0); border-radius: 0px; border-style: none; border-width: 0px; border-collapse: separate; border-end-end-radius: 0px; border-end-start-radius: 0px; border-image: none 100% / 1 / 0 stretch; border-inline-end: 0px none rgb(255, 0, 0); border-inline-start: 0px none rgb(255, 0, 0); border-start-end-radius: 0px; border-start-start-radius: 0px; inset: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; buffered-rendering: auto; caption-side: top; caret-color: rgb(255, 0, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color: rgb(255, 0, 0); color-interpolation: srgb; color-interpolation-filters: linearrgb; color-rendering: auto; columns: auto; gap: normal; column-rule: 0px rgb(255, 0, 0); column-span: none; contain-intrinsic-block-size: none; contain-intrinsic-size: none; contain-intrinsic-inline-size: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; field-sizing: fixed; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font-family: Meiryo; font-kerning: auto; font-optical-sizing: auto; font-palette: normal; font-size: 20px; font-size-adjust: none; font-stretch: 100%; font-style: normal; font-synthesis: weight style small-caps; font-variant: normal; font-weight: 700; grid: none; grid-area: auto; height: 30px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; initial-letter: normal; inline-size: 882px; inset-block: auto; inset-inline: auto; interpolate-size: numeric-only; isolation: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; line-height: normal; list-style: outside none disc; margin-block: 0px; margin: 0px; margin-inline: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; object-view-box: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(255, 0, 0) none 0px; outline-offset: 0px; overflow-anchor: auto; overflow-clip-margin: 0px; overflow-wrap: normal; overflow: visible; overlay: none; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; padding-block: 0px; padding: 0px; padding-inline: 0px; paint-order: normal; perspective: none; perspective-origin: 441px 15px; pointer-events: auto; position: static; position-anchor: auto; position-area: none; position-try: none; position-visibility: always; r: 0px; resize: none; rotate: none; ruby-align: space-around; ruby-position: over; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-inline: 0px; scroll-padding-block: auto; scroll-padding-inline: auto; scroll-timeline: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; speak: normal; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-decoration: none solid rgb(255, 0, 0); text-decoration-skip-ink: auto; text-emphasis: none rgb(255, 0, 0); text-emphasis-position: over; text-indent: 0px; text-overflow: clip; text-rendering: auto; text-shadow: none; text-size-adjust: auto; text-spacing-trim: normal; text-transform: none; text-underline-position: auto; text-wrap: wrap; timeline-scope: none; touch-action: auto; transform: none; transform-origin: 441px 15px; transform-style: flat; transition: all; translate: none; unicode-bidi: isolate; user-select: auto; vector-effect: none; vertical-align: baseline; view-timeline: none; view-transition-class: none; view-transition-name: none; visibility: visible; white-space-collapse: collapse; widows: 2; width: 882px; will-change: auto; word-break: normal; word-spacing: 0px; writing-mode: horizontal-tb; x: 0px; y: 0px; z-index: auto; zoom: 1; border-spacing: 0px; -webkit-border-image: none; -webkit-box-align: stretch; -webkit-box-decoration-break: slice; -webkit-box-direction: normal; -webkit-box-flex: 0; -webkit-box-ordinal-group: 1; -webkit-box-orient: horizontal; -webkit-box-pack: start; -webkit-font-smoothing: auto; -webkit-line-break: auto; -webkit-line-clamp: none; -webkit-locale: "en"; -webkit-mask-box-image-source: none; -webkit-mask-box-image-slice: 0 fill; -webkit-mask-box-image-width: auto; -webkit-mask-box-image-outset: 0; -webkit-mask-box-image-repeat: stretch; -webkit-print-color-adjust: economy; -webkit-rtl-ordering: logical; -webkit-tap-highlight-color: rgba(0, 0, 0, 0.18); -webkit-text-combine: none; -webkit-text-decorations-in-effect: none; -webkit-text-fill-color: rgb(255, 0, 0); -webkit-text-orientation: vertical-right; -webkit-text-security: none; -webkit-text-stroke: 0px rgb(255, 0, 0); -webkit-user-drag: auto; -webkit-user-modify: read-only; -webkit-writing-mode: horizontal-tb;">トマト</div>
<div style="color: rgb(0, 128, 0); font-size: 24px; font-weight: 700; accent-color: auto; place-content: normal; place-items: normal; place-self: auto; alignment-baseline: auto; anchor-name: none; anchor-scope: none; animation-composition: replace; animation: 0s ease 0s 1 normal none running none; app-region: none; appearance: none; backdrop-filter: none; backface-visibility: visible; background: none 0% 0% / auto repeat scroll padding-box border-box rgba(0, 0, 0, 0); background-blend-mode: normal; baseline-shift: 0px; baseline-source: auto; block-size: 36px; border-block-end: 0px none rgb(0, 128, 0); border-block-start: 0px none rgb(0, 128, 0); border-color: rgb(0, 128, 0); border-radius: 0px; border-style: none; border-width: 0px; border-collapse: separate; border-end-end-radius: 0px; border-end-start-radius: 0px; border-image: none 100% / 1 / 0 stretch; border-inline-end: 0px none rgb(0, 128, 0); border-inline-start: 0px none rgb(0, 128, 0); border-start-end-radius: 0px; border-start-start-radius: 0px; inset: auto; box-decoration-break: slice; box-shadow: none; box-sizing: content-box; break-after: auto; break-before: auto; break-inside: auto; buffered-rendering: auto; caption-side: top; caret-color: rgb(0, 128, 0); clear: none; clip: auto; clip-path: none; clip-rule: nonzero; color-interpolation: srgb; color-interpolation-filters: linearrgb; color-rendering: auto; columns: auto; gap: normal; column-rule: 0px rgb(0, 128, 0); column-span: none; contain-intrinsic-block-size: none; contain-intrinsic-size: none; contain-intrinsic-inline-size: none; container: none; content: normal; cursor: auto; cx: 0px; cy: 0px; d: none; direction: ltr; display: block; dominant-baseline: auto; empty-cells: show; field-sizing: fixed; fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; filter: none; flex: 0 1 auto; flex-flow: row; float: none; flood-color: rgb(0, 0, 0); flood-opacity: 1; font-family: Meiryo; font-kerning: auto; font-optical-sizing: auto; font-palette: normal; font-size-adjust: none; font-stretch: 100%; font-style: normal; font-synthesis: weight style small-caps; font-variant: normal; grid: none; grid-area: auto; height: 36px; hyphenate-character: auto; hyphenate-limit-chars: auto; hyphens: manual; image-orientation: from-image; image-rendering: auto; initial-letter: normal; inline-size: 882px; inset-block: auto; inset-inline: auto; interpolate-size: numeric-only; isolation: auto; letter-spacing: normal; lighting-color: rgb(255, 255, 255); line-break: auto; line-height: normal; list-style: outside none disc; margin-block: 0px; margin: 0px; margin-inline: 0px; marker: none; mask: none; mask-type: luminance; math-depth: 0; math-shift: normal; math-style: normal; max-block-size: none; max-height: none; max-inline-size: none; max-width: none; min-block-size: 0px; min-height: 0px; min-inline-size: 0px; min-width: 0px; mix-blend-mode: normal; object-fit: fill; object-position: 50% 50%; object-view-box: none; offset: normal; opacity: 1; order: 0; orphans: 2; outline: rgb(0, 128, 0) none 0px; outline-offset: 0px; overflow-anchor: auto; overflow-clip-margin: 0px; overflow-wrap: normal; overflow: visible; overlay: none; overscroll-behavior-block: auto; overscroll-behavior-inline: auto; padding-block: 0px; padding: 0px; padding-inline: 0px; paint-order: normal; perspective: none; perspective-origin: 441px 18px; pointer-events: auto; position: static; position-anchor: auto; position-area: none; position-try: none; position-visibility: always; r: 0px; resize: none; rotate: none; ruby-align: space-around; ruby-position: over; rx: auto; ry: auto; scale: none; scroll-behavior: auto; scroll-margin-block: 0px; scroll-margin-inline: 0px; scroll-padding-block: auto; scroll-padding-inline: auto; scroll-timeline: none; scrollbar-color: auto; scrollbar-gutter: auto; scrollbar-width: auto; shape-image-threshold: 0; shape-margin: 0px; shape-outside: none; shape-rendering: auto; speak: normal; stop-color: rgb(0, 0, 0); stop-opacity: 1; stroke: none; stroke-dasharray: none; stroke-dashoffset: 0px; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-opacity: 1; stroke-width: 1px; tab-size: 8; table-layout: auto; text-align: start; text-align-last: auto; text-anchor: start; text-decoration: none solid rgb(0, 128, 0); text-decoration-skip-ink: auto; text-emphasis: none rgb(0, 128, 0); text-emphasis-position: over; text-indent: 0px; text-overflow: clip; text-rendering: auto; text-shadow: none; text-size-adjust: auto; text-spacing-trim: normal; text-transform: none; text-underline-position: auto; text-wrap: wrap; timeline-scope: none; touch-action: auto; transform: none; transform-origin: 441px 18px; transform-style: flat; transition: all; translate: none; unicode-bidi: isolate; user-select: auto; vector-effect: none; vertical-align: baseline; view-timeline: none; view-transition-class: none; view-transition-name: none; visibility: visible; white-space-collapse: collapse; widows: 2; width: 882px; will-change: auto; word-break: normal; word-spacing: 0px; writing-mode: horizontal-tb; x: 0px; y: 0px; z-index: auto; zoom: 1; border-spacing: 0px; -webkit-border-image: none; -webkit-box-align: stretch; -webkit-box-decoration-break: slice; -webkit-box-direction: normal; -webkit-box-flex: 0; -webkit-box-ordinal-group: 1; -webkit-box-orient: horizontal; -webkit-box-pack: start; -webkit-font-smoothing: auto; -webkit-line-break: auto; -webkit-line-clamp: none; -webkit-locale: "en"; -webkit-mask-box-image-source: none; -webkit-mask-box-image-slice: 0 fill; -webkit-mask-box-image-width: auto; -webkit-mask-box-image-outset: 0; -webkit-mask-box-image-repeat: stretch; -webkit-print-color-adjust: economy; -webkit-rtl-ordering: logical; -webkit-tap-highlight-color: rgba(0, 0, 0, 0.18); -webkit-text-combine: none; -webkit-text-decorations-in-effect: none; -webkit-text-fill-color: rgb(0, 128, 0); -webkit-text-orientation: vertical-right; -webkit-text-security: none; -webkit-text-stroke: 0px rgb(0, 128, 0); -webkit-user-drag: auto; -webkit-user-modify: read-only; -webkit-writing-mode: horizontal-tb;">ピーマン</div>
</div></body></html><!--EndFragment-->
</body>
</html>