Skip to content

Commit 728b946

Browse files
authored
add copy button (#20)
1 parent 49a4501 commit 728b946

File tree

12 files changed

+153
-37
lines changed

12 files changed

+153
-37
lines changed

CHANGELOG.md

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# Changelog
22

3-
> ## 0.1.2 (Unreleased)
4-
>
5-
> - [#19] fixes some schema descriptions
6-
>
7-
> [#19]: https://github.com/deathbeds/urljsf/pull/19
3+
<details>
4+
<summary>Unreleased</summary>
5+
6+
## 0.1.2
7+
8+
- [#19] fixes some schema descriptions
9+
- [#20] adds a copy-to-clipboard button to all markdown-rendered `pre` tags
10+
11+
[#19]: https://github.com/deathbeds/urljsf/pull/19
12+
[#20]: https://github.com/deathbeds/urljsf/pull/20
13+
14+
</details>
815

916
## [0.1.1](https://github.com/deathbeds/urljsf/releases/tag/v0.1.1)
1017

atest/suites/01_docs/002_installer.robot

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ ${CSS_LICENSE} ${CSS_U_DATALIST} input[id$="_license"]
2020
${CSS_LICENSE_CHECK} ${CSS_U_DATALIST} input[id$="_license__datalist-check"]
2121
${GOOD_LICENSE} BSD-3-Clause
2222
${BAD_LICENSE} WTPL
23+
${CSS_COPY_BUTTON} .urljsf-copybutton
2324

2425

2526
*** Test Cases ***
@@ -66,6 +67,10 @@ Verify Installer URL
6667
${from_toml} = TOML.Loads ${raw}
6768
${expected} = Get TOML Fixture 002_installer.toml
6869
Should Be JSON Equivalent ${from_toml} ${expected}
70+
${copy} = Set Variable css:${CSS_COPY_BUTTON}
71+
Click Element ${copy}
72+
Wait Until Element Contains ${copy} ok
73+
Wait Until Element Contains ${copy} copy
6974

7075
Verify Installer Download
7176
[Documentation] Verify downloaded file

docs/use/advanced/templates.md

+23
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,29 @@ Each `template` gets an object of this form:
2222
}
2323
```
2424

25+
## Markdown
26+
27+
Most non-`url` templates are rendered as [Markdown][md].
28+
29+
Much of [GitHub flavored Markdown][gfm] is supported, but not platform-specific features
30+
like magic `#{issue}` transforms and `mermaid` fenced code blocks. Indeed, no syntax
31+
highlighting is supported, so generally any fence info will be discarded.
32+
33+
[gfm]: https://github.github.com/gfm
34+
[md]: https://daringfireball.net/projects/markdown
35+
36+
### Copy Code
37+
38+
All `pre` tags (generated with triple ticks, tildes, etc) will be rendered with a `copy`
39+
button.
40+
41+
This helps for URL-based workflows that don't allow for populating key parameters such
42+
as GitLab's `/new/` URL, or otherwise complex ones (GitHub's `/edit/`).
43+
44+
In this case, it is recommended to provide a [`below_{form}`](#below-form) template
45+
which shows the file content, with narrative describing how to copy the code and what to
46+
do with it.
47+
2548
### Special Templates
2649

2750
A few well-known template names and patterns are used globally.

js/src/components/array-template.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import Markdown from 'markdown-to-jsx';
1818

1919
import { useMarkdown } from '../utils.js';
20+
import { MD_OPTIONS } from './markdown.js';
2021

2122
export function ArrayFieldTemplate<
2223
T = any,
@@ -62,7 +63,7 @@ export function ArrayFieldTemplate<
6263

6364
let richDescription =
6465
description && useMarkdown(uiOptions) ? (
65-
<Markdown>{description}</Markdown>
66+
<Markdown options={MD_OPTIONS}>{description}</Markdown>
6667
) : (
6768
description
6869
);

js/src/components/check-item.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { RJSFValidationError } from '@rjsf/utils';
44

55
import Markdown from 'markdown-to-jsx';
66

7+
import { MD_OPTIONS } from './markdown.js';
8+
79
export function CheckItem(props: CheckItemProps): JSX.Element {
810
let result: string;
911
if (Array.isArray(props.result)) {
@@ -25,14 +27,20 @@ export function CheckItem(props: CheckItemProps): JSX.Element {
2527
disabled
2628
></input>
2729
<label className="form-check-label">
28-
<em>{props.markdown ? <Markdown>{props.label}</Markdown> : props.label}</em>
30+
<em>
31+
{props.markdown ? (
32+
<Markdown options={MD_OPTIONS}>{props.label}</Markdown>
33+
) : (
34+
props.label
35+
)}
36+
</em>
2937
</label>
3038
</div>
3139
{!result ? (
3240
<></>
3341
) : props.markdown ? (
3442
<div>
35-
<Markdown>{result}</Markdown>
43+
<Markdown options={MD_OPTIONS}>{result}</Markdown>
3644
</div>
3745
) : (
3846
result

js/src/components/copy-paste-pre.tsx

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (C) urljsf contributors.
2+
// Distributed under the terms of the Modified BSD License.
3+
import { useState } from 'react';
4+
5+
const PRE_STYLE = { position: 'relative' };
6+
7+
type TPreProps = React.DetailedHTMLProps<
8+
React.HTMLAttributes<HTMLPreElement>,
9+
HTMLPreElement
10+
>;
11+
12+
const COPY_STYLE = {
13+
position: 'absolute',
14+
top: '0.25em',
15+
right: '0.25em',
16+
opacity: 0.8,
17+
};
18+
19+
const NOT_YET = 0;
20+
const COPIED = 1;
21+
const ERRORED = 2;
22+
23+
const STATE_ICON = ['primary', 'success', 'warning'];
24+
const STATE_LABEL = ['copy', 'ok', 'error'];
25+
26+
export function CopyPastePre(props: TPreProps): JSX.Element {
27+
const button: JSX.Element[] = [];
28+
29+
const text = `${(props.children as any)?.props?.children || ''}`.trim();
30+
31+
if (text) {
32+
const [copyState, setCopyState] = useState(NOT_YET);
33+
const btnClass = `urljsf-copybutton btn btn-${STATE_ICON[copyState]}`;
34+
const btnLabel = STATE_LABEL[copyState];
35+
36+
const onClick = () => {
37+
setCopyState(copyText(text) ? COPIED : ERRORED);
38+
setTimeout(() => setCopyState(NOT_YET), 1000);
39+
};
40+
41+
button.push(
42+
<button
43+
className={btnClass}
44+
style={COPY_STYLE}
45+
onClick={onClick}
46+
aria-label="copy this text"
47+
>
48+
{btnLabel}
49+
</button>,
50+
);
51+
}
52+
53+
return (
54+
<pre {...props} style={PRE_STYLE}>
55+
{...button}
56+
{props.children}
57+
</pre>
58+
);
59+
}
60+
61+
function copyText(text: string): boolean {
62+
const el = document.createElement('textarea');
63+
let result = false;
64+
try {
65+
el.value = text;
66+
document.body.appendChild(el);
67+
el.select();
68+
document.execCommand('copy');
69+
result = true;
70+
} catch (err) {
71+
/* istanbul ignore next */
72+
console.error('failed to copy', err);
73+
} finally {
74+
el.remove();
75+
}
76+
return result;
77+
}

js/src/components/form.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from '../tokens.js';
3333
import { getConfig, getIdPrefix, initFormProps } from '../utils.js';
3434
import { CheckItem } from './check-item.js';
35+
import { MD_OPTIONS } from './markdown.js';
3536
import { Style } from './style.js';
3637

3738
const FORM_PRE_DEFAULTS: Partial<FormProps> = {
@@ -46,6 +47,7 @@ const SUBMIT_MD_OPTIONS: MarkdownToJSX.Options = {
4647
forceInline: true,
4748
forceBlock: false,
4849
forceWrapper: false,
50+
...MD_OPTIONS,
4951
};
5052

5153
const BTN_COMMON: Pick<ButtonProps, 'size' | 'className'> = {
@@ -280,7 +282,7 @@ function UrljsfForm(props: IUrljsfFormProps): JSX.Element {
280282
? []
281283
: [
282284
<li className="list-group-item" key={`${key}-where`}>
283-
<Markdown>{tmpl.value}</Markdown>
285+
<Markdown options={MD_OPTIONS}>{tmpl.value}</Markdown>
284286
</li>,
285287
];
286288
};

js/src/components/markdown.tsx

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (C) urljsf contributors.
2+
// Distributed under the terms of the Modified BSD License.
3+
import type { MarkdownToJSX } from 'markdown-to-jsx';
4+
5+
import { CopyPastePre } from './copy-paste-pre.js';
6+
7+
export const MD_OPTIONS: MarkdownToJSX.Options = {
8+
overrides: { pre: CopyPastePre },
9+
};

js/src/components/object-template.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import Markdown from 'markdown-to-jsx';
2121
import { UrljsfGridOptions } from '../_props.js';
2222
import { emptyObject } from '../tokens.js';
2323
import { useMarkdown } from '../utils.js';
24+
import { MD_OPTIONS } from './markdown.js';
2425

2526
const DEFAULT_GRID_OPTIONS: UrljsfGridOptions = {
2627
default: ['col-12'],
@@ -71,7 +72,7 @@ export function ObjectGridTemplate<
7172

7273
let richDescription =
7374
description && useMarkdown(uiOptions) ? (
74-
<Markdown>{description}</Markdown>
75+
<Markdown options={MD_OPTIONS}>{description}</Markdown>
7576
) : (
7677
description
7778
);

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"config": {
8282
"line-length": false,
8383
"first-line-h1": false,
84-
"no-inline-html": false
84+
"no-inline-html": false,
85+
"link-fragments": false
8586
}
8687
}
8788
}

0 commit comments

Comments
 (0)