Skip to content

Commit e18dcf8

Browse files
committed
Merge branch 't/11621b' into major
2 parents 4d77c1b + d920fc7 commit e18dcf8

File tree

9 files changed

+589
-236
lines changed

9 files changed

+589
-236
lines changed

plugins/clipboard/plugin.js

Lines changed: 174 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,21 @@
117117
icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE%
118118
hidpi: true, // %REMOVE_LINE_CORE%
119119
init: function( editor ) {
120-
var textificationFilter;
120+
var filterType,
121+
filtersFactory = filtersFactoryFactory();
122+
123+
if ( editor.config.forcePasteAsPlainText ) {
124+
filterType = 'plain-text';
125+
} else if ( editor.config.pasteFilter ) {
126+
filterType = editor.config.pasteFilter;
127+
}
128+
// On Webkit the pasteFilter defaults 'semantic-content' because pasted data is so terrible
129+
// that it must be always filtered.
130+
else if ( CKEDITOR.env.webkit && !( 'pasteFilter' in editor.config ) ) {
131+
filterType = 'semantic-content';
132+
}
133+
134+
editor.pasteFilter = filtersFactory.get( filterType );
121135

122136
initPasteClipboard( editor );
123137
initDragDrop( editor );
@@ -233,30 +247,43 @@
233247
data = dataObj.dataValue,
234248
trueType,
235249
// Default is 'html'.
236-
defaultType = editor.config.clipboard_defaultContentType || 'html';
250+
defaultType = editor.config.clipboard_defaultContentType || 'html',
251+
transferType = dataObj.dataTransfer && dataObj.dataTransfer.getTransferType( editor ),
252+
// Treat pasting without dataTransfer as external.
253+
external = !transferType || ( transferType == CKEDITOR.DATA_TRANSFER_EXTERNAL );
237254

238255
// If forced type is 'html' we don't need to know true data type.
239-
if ( type == 'html' || dataObj.preSniffing == 'html' )
256+
if ( type == 'html' || dataObj.preSniffing == 'html' ) {
240257
trueType = 'html';
241-
else
258+
} else {
242259
trueType = recogniseContentType( data );
260+
}
243261

244262
// Unify text markup.
245-
if ( trueType == 'htmlifiedtext' )
263+
if ( trueType == 'htmlifiedtext' ) {
246264
data = htmlifiedTextHtmlification( editor.config, data );
265+
}
266+
247267
// Strip presentional markup & unify text markup.
248-
else if ( type == 'text' && trueType == 'html' ) {
249-
// Init filter only if needed and cache it.
250-
data = htmlTextification( editor.config, data, textificationFilter || ( textificationFilter = getTextificationFilter() ) );
268+
// Forced plain text (dialog or forcePAPT).
269+
if ( type == 'text' && trueType == 'html' ) {
270+
data = filterContent( editor, data, filtersFactory.get( 'plain-text' ) );
271+
}
272+
// External paste and pasteFilter exists.
273+
else if ( external && editor.pasteFilter ) {
274+
data = filterContent( editor, data, editor.pasteFilter );
251275
}
252276

253-
if ( dataObj.startsWithEOL )
277+
if ( dataObj.startsWithEOL ) {
254278
data = '<br data-cke-eol="1">' + data;
255-
if ( dataObj.endsWithEOL )
279+
}
280+
if ( dataObj.endsWithEOL ) {
256281
data += '<br data-cke-eol="1">';
282+
}
257283

258-
if ( type == 'auto' )
284+
if ( type == 'auto' ) {
259285
type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
286+
}
260287

261288
dataObj.type = type;
262289
dataObj.dataValue = data;
@@ -1141,154 +1168,71 @@
11411168
return switchEnterMode( config, data );
11421169
}
11431170

1144-
function getTextificationFilter() {
1145-
var filter = new CKEDITOR.htmlParser.filter();
1146-
1147-
// Elements which creates vertical breaks (have vert margins) - took from HTML5 spec.
1148-
// http://dev.w3.org/html5/markup/Overview.html#toc
1149-
var replaceWithParaIf = { blockquote: 1, dl: 1, fieldset: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ol: 1, p: 1, table: 1, ul: 1 },
1150-
1151-
// All names except of <br>.
1152-
stripInlineIf = CKEDITOR.tools.extend( { br: 0 }, CKEDITOR.dtd.$inline ),
1153-
1154-
// What's finally allowed (cke:br will be removed later).
1155-
allowedIf = { p: 1, br: 1, 'cke:br': 1 },
1156-
1157-
knownIf = CKEDITOR.dtd,
1158-
1159-
// All names that will be removed (with content).
1160-
removeIf = CKEDITOR.tools.extend( { area: 1, basefont: 1, embed: 1, iframe: 1, map: 1, object: 1, param: 1 }, CKEDITOR.dtd.$nonBodyContent, CKEDITOR.dtd.$cdata );
1161-
1162-
var flattenTableCell = function( element ) {
1163-
delete element.name;
1164-
element.add( new CKEDITOR.htmlParser.text( ' ' ) );
1165-
},
1166-
// Squash adjacent headers into one. <h1>A</h1><h2>B</h2> -> <h1>A<br>B</h1><h2></h2>
1167-
// Empty ones will be removed later.
1168-
squashHeader = function( element ) {
1169-
var next = element,
1170-
br, el;
1171-
1172-
while ( ( next = next.next ) && next.name && next.name.match( /^h\d$/ ) ) {
1173-
// TODO shitty code - waitin' for htmlParse.element fix.
1174-
br = new CKEDITOR.htmlParser.element( 'cke:br' );
1175-
br.isEmpty = true;
1176-
element.add( br );
1177-
while ( ( el = next.children.shift() ) )
1178-
element.add( el );
1179-
}
1180-
};
1181-
1182-
filter.addRules( {
1183-
elements: {
1184-
h1: squashHeader,
1185-
h2: squashHeader,
1186-
h3: squashHeader,
1187-
h4: squashHeader,
1188-
h5: squashHeader,
1189-
h6: squashHeader,
1190-
1191-
img: function( element ) {
1192-
var alt = CKEDITOR.tools.trim( element.attributes.alt || '' ),
1193-
txt = ' ';
1194-
1195-
// Replace image with its alt if it doesn't look like an url or is empty.
1196-
if ( alt && !alt.match( /(^http|\.(jpe?g|gif|png))/i ) )
1197-
txt = ' [' + alt + '] ';
1198-
1199-
return new CKEDITOR.htmlParser.text( txt );
1200-
},
1171+
function filtersFactoryFactory() {
1172+
var filters = {};
12011173

1202-
td: flattenTableCell,
1203-
th: flattenTableCell,
1174+
function setUpTags() {
1175+
var tags = {};
12041176

1205-
$: function( element ) {
1206-
var initialName = element.name,
1207-
br;
1208-
1209-
// Remove entirely.
1210-
if ( removeIf[ initialName ] )
1211-
return false;
1212-
1213-
// Remove all attributes.
1214-
element.attributes = {};
1215-
1216-
// Pass brs.
1217-
if ( initialName == 'br' )
1218-
return element;
1219-
1220-
// Elements that we want to replace with paragraphs.
1221-
if ( replaceWithParaIf[ initialName ] )
1222-
element.name = 'p';
1223-
1224-
// Elements that we want to strip (tags only, without the content).
1225-
else if ( stripInlineIf[ initialName ] )
1226-
delete element.name;
1177+
for ( var tag in CKEDITOR.dtd ) {
1178+
if ( tag.charAt( 0 ) != '$' && tag != 'div' && tag != 'span' ) {
1179+
tags[ tag ] = 1;
1180+
}
1181+
}
12271182

1228-
// Surround other known element with <brs> and strip tags.
1229-
else if ( knownIf[ initialName ] ) {
1230-
// TODO shitty code - waitin' for htmlParse.element fix.
1231-
br = new CKEDITOR.htmlParser.element( 'cke:br' );
1232-
br.isEmpty = true;
1183+
return tags;
1184+
}
12331185

1234-
// Replace hrs (maybe sth else too?) with only one br.
1235-
if ( CKEDITOR.dtd.$empty[ initialName ] )
1236-
return br;
1186+
function createSemanticContentFilter() {
1187+
var filter = new CKEDITOR.filter();
12371188

1238-
element.add( br, 0 );
1239-
br = br.clone();
1240-
br.isEmpty = true;
1241-
element.add( br );
1242-
delete element.name;
1243-
}
1189+
filter.allow( {
1190+
$1: {
1191+
elements: setUpTags(),
1192+
attributes: true,
1193+
styles: false,
1194+
classes: false
1195+
}
1196+
} );
12441197

1245-
// Final cleanup - if we can still find some not allowed elements then strip their names.
1246-
if ( !allowedIf[ element.name ] )
1247-
delete element.name;
1198+
return filter;
1199+
}
12481200

1249-
return element;
1201+
return {
1202+
get: function( type ) {
1203+
if ( type == 'plain-text' ) {
1204+
// Does this look confusing to you? Did we forget about enter mode?
1205+
// It is a trick that let's us creating one filter for edidtor, regardless of its
1206+
// activeEnterMode (which as the name indicates can change during runtime).
1207+
//
1208+
// How does it work?
1209+
// The active enter mode is passed to the filter.applyTo method.
1210+
// The filter first marks all elements except <br> as disallowed and then tries to remove
1211+
// them. However, it cannot remove e.g. a <p> element completely, because it's a basic structural element,
1212+
// so it tries to replace it with an element created based on the active enter mode, eventually doing nothing.
1213+
//
1214+
// Now you can sleep well.
1215+
return filters.plainText || ( filters.plainText = new CKEDITOR.filter( 'br' ) );
1216+
} else if ( type == 'semantic-content' ) {
1217+
return filters.semanticContent || ( filters.semanticContent = createSemanticContentFilter() );
1218+
} else if ( type ) {
1219+
// Create filter based on rules (string or object).
1220+
return new CKEDITOR.filter( type );
12501221
}
1251-
}
1252-
}, {
1253-
// Apply this filter to every element.
1254-
applyToAll: true
1255-
} );
12561222

1257-
return filter;
1223+
return null;
1224+
}
1225+
};
12581226
}
12591227

1260-
function htmlTextification( config, data, filter ) {
1261-
var fragment = new CKEDITOR.htmlParser.fragment.fromHtml( data ),
1228+
function filterContent( editor, data, filter ) {
1229+
var fragment = CKEDITOR.htmlParser.fragment.fromHtml( data ),
12621230
writer = new CKEDITOR.htmlParser.basicWriter();
12631231

1264-
fragment.writeHtml( writer, filter );
1265-
data = writer.getHtml();
1266-
1267-
// Cleanup cke:brs.
1268-
data = data.replace( /\s*(<\/?[a-z:]+ ?\/?>)\s*/g, '$1' ) // Remove spaces around tags.
1269-
.replace( /(<cke:br \/>){2,}/g, '<cke:br />' ) // Join multiple adjacent cke:brs
1270-
.replace( /(<cke:br \/>)(<\/?p>|<br \/>)/g, '$2' ) // Strip cke:brs adjacent to original brs or ps.
1271-
.replace( /(<\/?p>|<br \/>)(<cke:br \/>)/g, '$1' )
1272-
.replace( /<(cke:)?br( \/)?>/g, '<br>' ) // Finally - rename cke:brs to brs and fix <br /> to <br>.
1273-
.replace( /<p><\/p>/g, '' ); // Remove empty paragraphs.
1274-
1275-
// Fix nested ps. E.g.:
1276-
// <p>A<p>B<p>C</p>D<p>E</p>F</p>G
1277-
// <p>A</p><p>B</p><p>C</p><p>D</p><p>E</p><p>F</p>G
1278-
var nested = 0;
1279-
data = data.replace( /<\/?p>/g, function( match ) {
1280-
if ( match == '<p>' ) {
1281-
if ( ++nested > 1 )
1282-
return '</p><p>';
1283-
} else {
1284-
if ( --nested > 0 )
1285-
return '</p><p>';
1286-
}
1232+
filter.applyTo( fragment, true, false, editor.activeEnterMode );
1233+
fragment.writeHtml( writer );
12871234

1288-
return match;
1289-
} ).replace( /<p><\/p>/g, '' ); // Step before: </p></p> -> </p><p></p><p>. Fix this here.
1290-
1291-
return switchEnterMode( config, data );
1235+
return writer.getHtml();
12921236
}
12931237

12941238
function switchEnterMode( config, data ) {
@@ -2398,3 +2342,88 @@
23982342
* @param {CKEDITOR.dom.node} data.target Drag target.
23992343
* @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
24002344
*/
2345+
2346+
/**
2347+
* Defines filter which is applied to external data pasted or dropped into editor. Possible values are:
2348+
*
2349+
* * `'plain-text'` &ndash; Content will be pasted as a plain text.
2350+
* * `'semantic-content'` &ndash; Known tags (except `div`, `span`) with all attributes (except
2351+
* `style` and `class`) will be kept.
2352+
* * `'h1 h2 p div'` &ndash; Custom rules compatible with {@link CKEDITOR.filter}.
2353+
* * `null` &ndash; Content will not be filtered by the paste filter (but it still may be filtered
2354+
* by the [Advanvced Content Filter](#!/guide/dev_advanced_content_filter)). This value can be used to
2355+
* disable the paste filter on Chrome and Safari, on which the option defaults to `'semantic-content'`.
2356+
*
2357+
* Example:
2358+
*
2359+
* config.pasteFilter = 'plain-text';
2360+
*
2361+
* Custom setting:
2362+
*
2363+
* config.pasteFilter = 'h1 h2 p ul ol li; img[!src, alt]; a[!href]';
2364+
*
2365+
* Based on this config option, a proper {@link CKEDITOR.filter} instance will be defined and assigned to the editor
2366+
* as a {@link CKEDITOR.editor#pasteFilter}. You can tweak paste filter's settings on the fly on this object
2367+
* as well as delete or replace it.
2368+
*
2369+
* var editor = CKEDITOR.replace( 'editor', {
2370+
* pasteFilter: 'semantic-content'
2371+
* } );
2372+
*
2373+
* editor.on( 'instanceReady', function() {
2374+
* // The result of this will be that all semantic content will be preserved
2375+
* // except tables.
2376+
* editor.pasteFilter.disallow( 'table' );
2377+
* } );
2378+
*
2379+
* Note that the paste filter is applied only to an **external** data. There are three data sources:
2380+
*
2381+
* * copied and pasted in the same editor (internal),
2382+
* * copied from one editor and pasted into another (cross-editor),
2383+
* * coming from all other sources like websites, MS Word, etc. (external).
2384+
*
2385+
* If the {@link CKEDITOR.config#allowedContent Advanced Content Filter} is not disabled, then
2386+
* it will be also applied to the pasted and dropped data. The paste filter's job is to "normalize"
2387+
* external data which often need to be handled differently than content produced by the editor.
2388+
*
2389+
* This setting defaults `'semantic-content'` on Chrome and Safari due to messy HTML which these browsers
2390+
* keep in the clipboard. On other browsers its defaults `null`.
2391+
*
2392+
* @since 4.5
2393+
* @cfg {String} [pasteFilter='semantic-content' on Chrome and Safari and null on other browsers]
2394+
* @member CKEDITOR.config
2395+
*/
2396+
2397+
/**
2398+
* {@link CKEDITOR.filter Content filter} which is used when external data is pasted or dropped into editor or there
2399+
* is forced paste as a plain text.
2400+
*
2401+
* This object might be used on the fly to define rules for pasted external content.
2402+
* This object is available and used if {@link CKEDITOR.plugins.clipboard clipboard} plugin is enabled and
2403+
* {@link CKEDITOR.config#pasteFilter} or {@link CKEDITOR.config#forcePasteAsPlainText} was defined.
2404+
*
2405+
* To enable the filter:
2406+
*
2407+
* var editor = CKEDITOR.replace( 'editor', {
2408+
* pasteFilter: 'plain-text'
2409+
* } );
2410+
*
2411+
* You can also modify the filter on the fly later on:
2412+
*
2413+
* editor.pasteFilter = new CKEDITOR.filter( 'p h1 h2; a[!href]' );
2414+
*
2415+
* Note that the paste filter is applied only to an **external** data. There are three data sources:
2416+
*
2417+
* * copied and pasted in the same editor (internal),
2418+
* * copied from one editor and pasted into another (cross-editor),
2419+
* * coming from all other sources like websites, MS Word, etc. (external).
2420+
*
2421+
* If the {@link CKEDITOR.config#allowedContent Allowed Content Filter} is not disabled, then
2422+
* it will be also applied to the pasted and dropped data. The paste filter's job is to "normalize"
2423+
* external data which often need to be handled differently than content produced by the editor.
2424+
*
2425+
* @since 4.5
2426+
* @readonly
2427+
* @property {CKEDITOR.filter} [pasteFilter]
2428+
* @member CKEDITOR.editor
2429+
*/

tests/plugins/clipboard/_helpers/pasting.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ function assertPasteEvent( editor, eventData, expected, message, async ) {
1919
eventData.type = 'auto';
2020

2121
eventData.method = 'paste';
22-
eventData.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
22+
// Allow passing a dataTransfer mock.
23+
if ( !eventData.dataTransfer ) {
24+
eventData.dataTransfer = new CKEDITOR.plugins.clipboard.dataTransfer();
25+
}
2326

2427
editor.once( 'paste', onPaste, null, null, priority );
2528
editor.fire( 'paste', eventData );

0 commit comments

Comments
 (0)