11<!--
22 This source file is part of the Swift.org open source project
33
4- Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
4+ Copyright (c) 2021-2025 Apple Inc. and the Swift project authors
55 Licensed under Apache License v2.0 with Runtime Library Exception
66
77 See https://swift.org/LICENSE.txt for license information
2222 >{{ fileName }}
2323 </Filename >
2424 <div class =" container-general" >
25+ <button
26+ v-if =" copyToClipboard"
27+ class =" copy-button"
28+ :class =" copyState"
29+ @click =" copyCodeToClipboard"
30+ :aria-label =" $t('icons.copy')"
31+ :title =" $t('icons.copy')"
32+ >
33+ <CopyIcon v-if =" copyState === CopyState.idle" class =" copy-icon" />
34+ <CheckmarkIcon v-else-if =" copyState === CopyState.success" class =" checkmark-icon" />
35+ <CrossIcon v-else-if =" copyState === CopyState.failure" class =" cross-icon" />
36+
37+ </button >
2538 <!-- Do not add newlines in <pre>, as they'll appear in the rendered HTML. -->
2639 <pre ><CodeBlock ><template
2740 v-for =" (line , index ) in syntaxHighlightedLines "
4558import { escapeHtml } from ' docc-render/utils/strings' ;
4659import Language from ' docc-render/constants/Language' ;
4760import CodeBlock from ' docc-render/components/CodeBlock.vue' ;
61+ import CopyIcon from ' theme/components/Icons/CopyIcon.vue' ;
62+ import CheckmarkIcon from ' theme/components/Icons/CheckmarkIcon.vue' ;
63+ import CrossIcon from ' theme/components/Icons/CrossIcon.vue' ;
4864import { highlightContent , registerHighlightLanguage } from ' docc-render/utils/syntax-highlight' ;
4965
5066import CodeListingFilename from ' ./CodeListingFilename.vue' ;
5167
68+ const CopyState = {
69+ idle: ' idle' ,
70+ success: ' success' ,
71+ failure: ' failure' ,
72+ };
73+
5274export default {
5375 name: ' CodeListing' ,
54- components: { Filename: CodeListingFilename, CodeBlock },
76+ components: {
77+ Filename: CodeListingFilename,
78+ CodeBlock,
79+ CopyIcon,
80+ CheckmarkIcon,
81+ CrossIcon,
82+ },
5583 data () {
5684 return {
5785 syntaxHighlightedLines: [],
86+ copyState: CopyState .idle ,
87+ CopyState,
5888 };
5989 },
6090 props: {
@@ -69,6 +99,10 @@ export default {
6999 type: Array ,
70100 required: true ,
71101 },
102+ copyToClipboard: {
103+ type: Boolean ,
104+ default : () => false ,
105+ },
72106 startLineNumber: {
73107 type: Number ,
74108 default : () => 1 ,
@@ -92,6 +126,9 @@ export default {
92126 const fallbackMap = { occ: Language .objectiveC .key .url };
93127 return fallbackMap[this .syntax ] || this .syntax ;
94128 },
129+ copyableText () {
130+ return this .content .join (' \n ' );
131+ },
95132 },
96133 watch: {
97134 content: {
@@ -122,6 +159,21 @@ export default {
122159 line === ' ' ? ' \n ' : line
123160 ));
124161 },
162+ copyCodeToClipboard () {
163+ navigator .clipboard .writeText (this .copyableText )
164+ .then (() => {
165+ this .copyState = CopyState .success ;
166+ })
167+ .catch ((err ) => {
168+ console .error (' Failed to copy text: ' , err);
169+ this .copyState = CopyState .failure ;
170+ })
171+ .finally (() => {
172+ setTimeout (() => {
173+ this .copyState = CopyState .idle ;
174+ }, 1000 );
175+ });
176+ },
125177 },
126178};
127179 </script >
@@ -187,6 +239,7 @@ code {
187239 flex-direction : column ;
188240 border-radius : var (--code-border-radius , $border-radius );
189241 overflow : hidden ;
242+ position : relative ;
190243 // we need to establish a new stacking context to resolve a Safari bug where
191244 // the scrollbar is not clipped by this element depending on its border-radius
192245 @include new-stacking-context ;
@@ -205,4 +258,59 @@ pre {
205258 flex-grow : 1 ;
206259}
207260
261+ .copy-button {
262+ position : absolute ;
263+ top : 0.2em ;
264+ right : 0.2em ;
265+ width : 1.5em ;
266+ height : 1.5em ;
267+ background : var (--color-fill-gray-tertiary );
268+ border : none ;
269+ border-radius : var (--button-border-radius , $button-radius );
270+ padding : 4px ;
271+ }
272+
273+ @media (hover : hover) {
274+ .copy-button {
275+ opacity : 0 ;
276+ transition : all 0.2s ease-in-out ;
277+ }
278+
279+ .copy-button :hover {
280+ background-color : var (--color-fill-gray );
281+ }
282+
283+ .copy-button .copy-icon {
284+ opacity : 0.8 ;
285+ }
286+
287+ .copy-button :hover .copy-icon {
288+ opacity : 1 ;
289+ }
290+
291+ .container-general :hover .copy-button {
292+ opacity : 1 ;
293+ }
294+ }
295+
296+ @media (hover : none ) {
297+ .copy-button {
298+ opacity : 1 ;
299+ }
300+ }
301+
302+ .copy-button .copy-icon {
303+ fill : var (--color-figure-gray );
304+ }
305+
306+ .copy-button.success .checkmark-icon {
307+ color : var (--color-figure-blue );
308+ fill : currentColor ;
309+ }
310+
311+ .copy-button.failure .cross-icon {
312+ color : var (--color-figure-red );
313+ fill : currentColor ;
314+ }
315+
208316 </style >
0 commit comments