Skip to content

Commit 978ee1e

Browse files
committed
Change copy behavior to override with raw text
1 parent 01b44f6 commit 978ee1e

File tree

3 files changed

+112
-52
lines changed

3 files changed

+112
-52
lines changed

src/components/done-text/index.js

+83-33
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/**
22
* External dependencies
33
*/
4-
import { createElement } from 'preact';
4+
import { createElement, Component } from 'preact';
55
import { repeat, reduce, truncate } from 'lodash';
66

77
/**
88
* Internal dependencies
99
*/
1010
import Link from 'components/link';
11+
import { getSelectedOffset } from 'lib/selection';
1112

1213
/**
1314
* Zero width character used as stand-in for replaced character to preserve
@@ -17,18 +18,25 @@ import Link from 'components/link';
1718
*/
1819
const ZERO_WIDTH_SPACE = '​';
1920

20-
export default function DoneText( { onClick, onFocus, onMouseDown, children } ) {
21-
const transforms = [ [
22-
/(^|\s)#(\S+)/,
23-
( [ , whitespace, tag ] ) => [
21+
/**
22+
* Tuples of transforms, first entry as a regular expression to match, second
23+
* entry as a function return element to use in place of the original text.
24+
*
25+
* @type {Array}
26+
*/
27+
const TRANSFORMS = [
28+
{
29+
pattern: /(^|\s)#(\S+)/,
30+
transform: ( [ , whitespace, tag ] ) => [
2431
whitespace,
2532
<Link preload to={ `/tags/${ tag }/` }>
2633
#{ tag }
2734
</Link>,
2835
],
29-
], [
30-
/(^|\s)(https?:\/\/\S+)/,
31-
( [ , whitespace, url ] ) => {
36+
},
37+
{
38+
pattern: /(^|\s)(https?:\/\/\S+)/,
39+
transform: ( [ , whitespace, url ] ) => {
3240
const [ prefix ] = url.match( /^https?:\/\/(www\.)?/ );
3341
const endRepeat = Math.max( 0, url.length - prefix.length - 30 );
3442

@@ -44,16 +52,25 @@ export default function DoneText( { onClick, onFocus, onMouseDown, children } )
4452
</Link>,
4553
];
4654
},
47-
], [
48-
/`([^`]+)`/,
49-
( [ , code ] ) => (
55+
},
56+
{
57+
pattern: /`([^`]+)`/,
58+
transform: ( [ , code ] ) => (
5059
<code>{ ZERO_WIDTH_SPACE }{ code }{ ZERO_WIDTH_SPACE }</code>
5160
),
52-
] ];
61+
},
62+
];
5363

64+
/**
65+
* Given array of children, returns transformed children
66+
*
67+
* @param {Array} children Original children
68+
* @return {Array} Transformed children
69+
*/
70+
function getTransformedDoneText( children ) {
5471
let parts = [ ...children ];
5572

56-
for ( const [ pattern, transform ] of transforms ) {
73+
for ( const { pattern, transform } of TRANSFORMS ) {
5774
parts = reduce( parts, ( memo, part ) => {
5875
if ( 'string' !== typeof part ) {
5976
return memo.concat( part );
@@ -72,26 +89,59 @@ export default function DoneText( { onClick, onFocus, onMouseDown, children } )
7289
}, [] );
7390
}
7491

75-
// Infer focus handlers as intent to edit. Ensure element can receive focus
76-
// and apply ARIA role to indicate editability.
77-
let focusProps;
78-
if ( onFocus ) {
79-
focusProps = {
80-
tabIndex: 0,
81-
role: 'textbox',
82-
onFocus,
83-
};
84-
}
92+
return parts;
93+
}
8594

86-
return (
87-
<div
88-
{ ...focusProps }
89-
onClick={ onClick }
90-
onMouseDown={ onMouseDown }
91-
className="done-text">
92-
<div className="done-text__overflow">
93-
{ parts }
95+
export default class DoneText extends Component {
96+
setCopyText = ( event ) => {
97+
// Override the clipboard data with the original children text to avoid
98+
// zero-width spaces, and because its generally more useful if planned
99+
// to paste into a separate new done.
100+
101+
let setData;
102+
if ( window.clipboardData ) {
103+
setData = ( text ) => window.clipboardData.setData( 'Text', text );
104+
} else if ( event.clipboardData ) {
105+
setData = ( text ) => event.clipboardData.setData( 'text/plain', text );
106+
}
107+
108+
if ( ! setData ) {
109+
return;
110+
}
111+
112+
const { children } = this.props;
113+
const [ start, end ] = getSelectedOffset( this.node );
114+
const text = children.join( '' ).slice( start, end );
115+
setData( text );
116+
event.preventDefault();
117+
};
118+
119+
render() {
120+
const { onClick, onFocus, onMouseDown, children } = this.props;
121+
122+
// Infer focus handlers as intent to edit. Ensure element can receive focus
123+
// and apply ARIA role to indicate editability.
124+
let focusProps;
125+
if ( onFocus ) {
126+
focusProps = {
127+
tabIndex: 0,
128+
role: 'textbox',
129+
onFocus,
130+
};
131+
}
132+
133+
return (
134+
<div
135+
ref={ ( node ) => this.node = node }
136+
{ ...focusProps }
137+
onClick={ onClick }
138+
onMouseDown={ onMouseDown }
139+
onCopy={ this.setCopyText }
140+
className="done-text">
141+
<div className="done-text__overflow">
142+
{ getTransformedDoneText( children ) }
143+
</div>
94144
</div>
95-
</div>
96-
);
145+
);
146+
}
97147
}

src/components/dones-list/index.js

+3-19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { map, sortBy } from 'lodash';
1111
*/
1212
import { USER_ID } from 'constant';
1313
import { translate } from 'lib/i18n';
14+
import { getSelectedOffset } from 'lib/selection';
1415
import DoneStatus from 'components/done-status';
1516
import DoneInput from 'components/done-input';
1617
import DoneText from 'components/done-text';
@@ -45,26 +46,9 @@ class DonesList extends Component {
4546
const editing = this.pendingEdit;
4647
delete this.pendingEdit;
4748

48-
if ( 'A' === event.target.nodeName ) {
49-
return;
49+
if ( 'A' !== event.target.nodeName ) {
50+
this.editDone( editing, getSelectedOffset( event.currentTarget ) );
5051
}
51-
52-
const selection = window.getSelection();
53-
if ( selection.rangeCount === 0 ) {
54-
return;
55-
}
56-
57-
// Find selection offset within DoneText
58-
// See: https://stackoverflow.com/a/4812022/995445
59-
const range = window.getSelection().getRangeAt( 0 );
60-
const rangeBeforeCaret = range.cloneRange();
61-
rangeBeforeCaret.selectNodeContents( event.currentTarget );
62-
rangeBeforeCaret.setEnd( range.startContainer, range.startOffset );
63-
const start = rangeBeforeCaret.toString().length;
64-
rangeBeforeCaret.setEnd( range.endContainer, range.endOffset );
65-
const end = rangeBeforeCaret.toString().length;
66-
67-
this.editDone( editing, [ start, end ] );
6852
};
6953

7054
editIfNonePending = ( id ) => {

src/lib/selection/index.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Returns a tuple of start and end offset for the current selection
3+
*
4+
* @see https://stackoverflow.com/a/4812022/995445
5+
*
6+
* @param {?Element} referenceNode Optional node from which to get offset.
7+
* Defaults to range common ancestor.
8+
* @return {Number[]} Tuple of start and end offset
9+
*/
10+
export function getSelectedOffset( referenceNode ) {
11+
const selection = window.getSelection();
12+
if ( selection.rangeCount === 0 ) {
13+
return [ 0, 0 ];
14+
}
15+
16+
const range = selection.getRangeAt( 0 );
17+
const rangeBeforeCaret = range.cloneRange();
18+
const node = referenceNode || range.commonAncestorContainer;
19+
rangeBeforeCaret.selectNodeContents( node );
20+
rangeBeforeCaret.setEnd( range.startContainer, range.startOffset );
21+
const start = rangeBeforeCaret.toString().length;
22+
rangeBeforeCaret.setEnd( range.endContainer, range.endOffset );
23+
const end = rangeBeforeCaret.toString().length;
24+
25+
return [ start, end ];
26+
}

0 commit comments

Comments
 (0)