1
1
<!--
2
2
This source file is part of the Swift.org open source project
3
3
4
- Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
4
+ Copyright (c) 2021-2025 Apple Inc. and the Swift project authors
5
5
Licensed under Apache License v2.0 with Runtime Library Exception
6
6
7
7
See https://swift.org/LICENSE.txt for license information
22
22
>{{ fileName }}
23
23
</Filename >
24
24
<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 >
25
38
<!-- Do not add newlines in <pre>, as they'll appear in the rendered HTML. -->
26
39
<pre ><CodeBlock ><template
27
40
v-for =" (line , index ) in syntaxHighlightedLines "
45
58
import { escapeHtml } from ' docc-render/utils/strings' ;
46
59
import Language from ' docc-render/constants/Language' ;
47
60
import 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' ;
48
64
import { highlightContent , registerHighlightLanguage } from ' docc-render/utils/syntax-highlight' ;
49
65
50
66
import CodeListingFilename from ' ./CodeListingFilename.vue' ;
51
67
68
+ const CopyState = {
69
+ idle: ' idle' ,
70
+ success: ' success' ,
71
+ failure: ' failure' ,
72
+ };
73
+
52
74
export default {
53
75
name: ' CodeListing' ,
54
- components: { Filename: CodeListingFilename, CodeBlock },
76
+ components: {
77
+ Filename: CodeListingFilename,
78
+ CodeBlock,
79
+ CopyIcon,
80
+ CheckmarkIcon,
81
+ CrossIcon,
82
+ },
55
83
data () {
56
84
return {
57
85
syntaxHighlightedLines: [],
86
+ copyState: CopyState .idle ,
87
+ CopyState,
58
88
};
59
89
},
60
90
props: {
@@ -69,6 +99,10 @@ export default {
69
99
type: Array ,
70
100
required: true ,
71
101
},
102
+ copyToClipboard: {
103
+ type: Boolean ,
104
+ default : () => false ,
105
+ },
72
106
startLineNumber: {
73
107
type: Number ,
74
108
default : () => 1 ,
@@ -92,6 +126,9 @@ export default {
92
126
const fallbackMap = { occ: Language .objectiveC .key .url };
93
127
return fallbackMap[this .syntax ] || this .syntax ;
94
128
},
129
+ copyableText () {
130
+ return this .content .join (' \n ' );
131
+ },
95
132
},
96
133
watch: {
97
134
content: {
@@ -122,6 +159,21 @@ export default {
122
159
line === ' ' ? ' \n ' : line
123
160
));
124
161
},
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
+ },
125
177
},
126
178
};
127
179
</script >
@@ -187,6 +239,7 @@ code {
187
239
flex-direction : column ;
188
240
border-radius : var (--code-border-radius , $border-radius );
189
241
overflow : hidden ;
242
+ position : relative ;
190
243
// we need to establish a new stacking context to resolve a Safari bug where
191
244
// the scrollbar is not clipped by this element depending on its border-radius
192
245
@include new-stacking-context ;
@@ -205,4 +258,59 @@ pre {
205
258
flex-grow : 1 ;
206
259
}
207
260
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
+
208
316
</style >
0 commit comments