-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
index.js
230 lines (217 loc) · 6.14 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import { getBlockType, parse } from '@wordpress/blocks';
import { useDispatch, useRegistry } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';
import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import {
hasAlignSupport,
hasBorderSupport,
hasBackgroundColorSupport,
hasTextColorSupport,
hasGradientSupport,
hasCustomClassNameSupport,
hasFontFamilySupport,
hasFontSizeSupport,
hasLayoutSupport,
hasStyleSupport,
} from '../../hooks/supports';
/**
* Determine if the copied text looks like serialized blocks or not.
* Since plain text will always get parsed into a freeform block,
* we check that if the parsed blocks is anything other than that.
*
* @param {string} text The copied text.
* @return {boolean} True if the text looks like serialized blocks, false otherwise.
*/
function hasSerializedBlocks( text ) {
try {
const blocks = parse( text, {
__unstableSkipMigrationLogs: true,
__unstableSkipAutop: true,
} );
if ( blocks.length === 1 && blocks[ 0 ].name === 'core/freeform' ) {
// It's likely that the text is just plain text and not serialized blocks.
return false;
}
return true;
} catch ( err ) {
// Parsing error, the text is not serialized blocks.
// (Even though that it technically won't happen)
return false;
}
}
/**
* Style attributes are attributes being added in `block-editor/src/hooks/*`.
* (Except for some unrelated to style like `anchor` or `settings`.)
* They generally represent the default block supports.
*/
const STYLE_ATTRIBUTES = {
align: hasAlignSupport,
borderColor: ( nameOrType ) => hasBorderSupport( nameOrType, 'color' ),
backgroundColor: hasBackgroundColorSupport,
textColor: hasTextColorSupport,
gradient: hasGradientSupport,
className: hasCustomClassNameSupport,
fontFamily: hasFontFamilySupport,
fontSize: hasFontSizeSupport,
layout: hasLayoutSupport,
style: hasStyleSupport,
};
/**
* Get the "style attributes" from a given block to a target block.
*
* @param {WPBlock} sourceBlock The source block.
* @param {WPBlock} targetBlock The target block.
* @return {Object} the filtered attributes object.
*/
function getStyleAttributes( sourceBlock, targetBlock ) {
return Object.entries( STYLE_ATTRIBUTES ).reduce(
( attributes, [ attributeKey, hasSupport ] ) => {
// Only apply the attribute if both blocks support it.
if (
hasSupport( sourceBlock.name ) &&
hasSupport( targetBlock.name )
) {
// Override attributes that are not present in the block to their defaults.
attributes[ attributeKey ] =
sourceBlock.attributes[ attributeKey ];
}
return attributes;
},
{}
);
}
/**
* Update the target blocks with style attributes recursively.
*
* @param {WPBlock[]} targetBlocks The target blocks to be updated.
* @param {WPBlock[]} sourceBlocks The source blocks to get th style attributes from.
* @param {Function} updateBlockAttributes The function to update the attributes.
*/
function recursivelyUpdateBlockAttributes(
targetBlocks,
sourceBlocks,
updateBlockAttributes
) {
for (
let index = 0;
index < Math.min( sourceBlocks.length, targetBlocks.length );
index += 1
) {
updateBlockAttributes(
targetBlocks[ index ].clientId,
getStyleAttributes( sourceBlocks[ index ], targetBlocks[ index ] )
);
recursivelyUpdateBlockAttributes(
targetBlocks[ index ].innerBlocks,
sourceBlocks[ index ].innerBlocks,
updateBlockAttributes
);
}
}
/**
* A hook to return a pasteStyles event function for handling pasting styles to blocks.
*
* @return {Function} A function to update the styles to the blocks.
*/
export default function usePasteStyles() {
const registry = useRegistry();
const { updateBlockAttributes } = useDispatch( blockEditorStore );
const { createSuccessNotice, createWarningNotice, createErrorNotice } =
useDispatch( noticesStore );
return useCallback(
async ( targetBlocks ) => {
let html = '';
try {
// `http:` sites won't have the clipboard property on navigator.
// (with the exception of localhost.)
if ( ! window.navigator.clipboard ) {
createErrorNotice(
__(
'Unable to paste styles. This feature is only available on secure (https) sites in supporting browsers.'
),
{ type: 'snackbar' }
);
return;
}
html = await window.navigator.clipboard.readText();
} catch ( error ) {
// Possibly the permission is denied.
createErrorNotice(
__(
'Unable to paste styles. Please allow browser clipboard permissions before continuing.'
),
{
type: 'snackbar',
}
);
return;
}
// Abort if the copied text is empty or doesn't look like serialized blocks.
if ( ! html || ! hasSerializedBlocks( html ) ) {
createWarningNotice(
__(
"Unable to paste styles. Block styles couldn't be found within the copied content."
),
{
type: 'snackbar',
}
);
return;
}
const copiedBlocks = parse( html );
if ( copiedBlocks.length === 1 ) {
// Apply styles of the block to all the target blocks.
registry.batch( () => {
recursivelyUpdateBlockAttributes(
targetBlocks,
targetBlocks.map( () => copiedBlocks[ 0 ] ),
updateBlockAttributes
);
} );
} else {
registry.batch( () => {
recursivelyUpdateBlockAttributes(
targetBlocks,
copiedBlocks,
updateBlockAttributes
);
} );
}
if ( targetBlocks.length === 1 ) {
const title = getBlockType( targetBlocks[ 0 ].name )?.title;
createSuccessNotice(
sprintf(
// Translators: Name of the block being pasted, e.g. "Paragraph".
__( 'Pasted styles to %s.' ),
title
),
{ type: 'snackbar' }
);
} else {
createSuccessNotice(
sprintf(
// Translators: The number of the blocks.
__( 'Pasted styles to %d blocks.' ),
targetBlocks.length
),
{ type: 'snackbar' }
);
}
},
[
registry.batch,
updateBlockAttributes,
createSuccessNotice,
createWarningNotice,
createErrorNotice,
]
);
}