|
117 | 117 | icons: 'copy,copy-rtl,cut,cut-rtl,paste,paste-rtl', // %REMOVE_LINE_CORE%
|
118 | 118 | hidpi: true, // %REMOVE_LINE_CORE%
|
119 | 119 | 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 ); |
121 | 135 |
|
122 | 136 | initPasteClipboard( editor );
|
123 | 137 | initDragDrop( editor );
|
|
233 | 247 | data = dataObj.dataValue,
|
234 | 248 | trueType,
|
235 | 249 | // 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 ); |
237 | 254 |
|
238 | 255 | // 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' ) { |
240 | 257 | trueType = 'html';
|
241 |
| - else |
| 258 | + } else { |
242 | 259 | trueType = recogniseContentType( data );
|
| 260 | + } |
243 | 261 |
|
244 | 262 | // Unify text markup.
|
245 |
| - if ( trueType == 'htmlifiedtext' ) |
| 263 | + if ( trueType == 'htmlifiedtext' ) { |
246 | 264 | data = htmlifiedTextHtmlification( editor.config, data );
|
| 265 | + } |
| 266 | + |
247 | 267 | // 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 ); |
251 | 275 | }
|
252 | 276 |
|
253 |
| - if ( dataObj.startsWithEOL ) |
| 277 | + if ( dataObj.startsWithEOL ) { |
254 | 278 | data = '<br data-cke-eol="1">' + data;
|
255 |
| - if ( dataObj.endsWithEOL ) |
| 279 | + } |
| 280 | + if ( dataObj.endsWithEOL ) { |
256 | 281 | data += '<br data-cke-eol="1">';
|
| 282 | + } |
257 | 283 |
|
258 |
| - if ( type == 'auto' ) |
| 284 | + if ( type == 'auto' ) { |
259 | 285 | type = ( trueType == 'html' || defaultType == 'html' ) ? 'html' : 'text';
|
| 286 | + } |
260 | 287 |
|
261 | 288 | dataObj.type = type;
|
262 | 289 | dataObj.dataValue = data;
|
|
1141 | 1168 | return switchEnterMode( config, data );
|
1142 | 1169 | }
|
1143 | 1170 |
|
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 = {}; |
1201 | 1173 |
|
1202 |
| - td: flattenTableCell, |
1203 |
| - th: flattenTableCell, |
| 1174 | + function setUpTags() { |
| 1175 | + var tags = {}; |
1204 | 1176 |
|
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 | + } |
1227 | 1182 |
|
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 | + } |
1233 | 1185 |
|
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(); |
1237 | 1188 |
|
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 | + } ); |
1244 | 1197 |
|
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 | + } |
1248 | 1200 |
|
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 ); |
1250 | 1221 | }
|
1251 |
| - } |
1252 |
| - }, { |
1253 |
| - // Apply this filter to every element. |
1254 |
| - applyToAll: true |
1255 |
| - } ); |
1256 | 1222 |
|
1257 |
| - return filter; |
| 1223 | + return null; |
| 1224 | + } |
| 1225 | + }; |
1258 | 1226 | }
|
1259 | 1227 |
|
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 ), |
1262 | 1230 | writer = new CKEDITOR.htmlParser.basicWriter();
|
1263 | 1231 |
|
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 ); |
1287 | 1234 |
|
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(); |
1292 | 1236 | }
|
1293 | 1237 |
|
1294 | 1238 | function switchEnterMode( config, data ) {
|
|
2398 | 2342 | * @param {CKEDITOR.dom.node} data.target Drag target.
|
2399 | 2343 | * @param {CKEDITOR.plugins.clipboard.dataTransfer} data.dataTransfer DataTransfer facade.
|
2400 | 2344 | */
|
| 2345 | + |
| 2346 | +/** |
| 2347 | + * Defines filter which is applied to external data pasted or dropped into editor. Possible values are: |
| 2348 | + * |
| 2349 | + * * `'plain-text'` – Content will be pasted as a plain text. |
| 2350 | + * * `'semantic-content'` – Known tags (except `div`, `span`) with all attributes (except |
| 2351 | + * `style` and `class`) will be kept. |
| 2352 | + * * `'h1 h2 p div'` – Custom rules compatible with {@link CKEDITOR.filter}. |
| 2353 | + * * `null` – 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 | + */ |
0 commit comments