-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle pasted block content #1331
Changes from all commits
88ce9d2
e4281d3
fff2c6b
10f8012
56f93ad
4488307
2687cb8
5b913b6
c127df6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { nodeListToReact } from 'dom-react'; | ||
import { find, get } from 'lodash'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { createElement } from 'element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { createBlock } from './factory'; | ||
import { getBlockTypes, getUnknownTypeHandler } from './registration'; | ||
import { parseBlockAttributes } from './parser'; | ||
|
||
/** | ||
* Normalises array nodes of any node type to an array of block level nodes. | ||
* @param {Array} nodes Array of Nodes. | ||
* @return {Array} Array of block level HTMLElements | ||
*/ | ||
export function normaliseToBlockLevelNodes( nodes ) { | ||
const decu = document.createDocumentFragment(); | ||
const accu = document.createDocumentFragment(); | ||
|
||
// A fragment is easier to work with. | ||
nodes.forEach( node => decu.appendChild( node.cloneNode( true ) ) ); | ||
|
||
while ( decu.firstChild ) { | ||
const node = decu.firstChild; | ||
|
||
// Text nodes: wrap in a paragraph, or append to previous. | ||
if ( node.nodeType === 3 ) { | ||
if ( ! accu.lastChild || accu.lastChild.nodeName !== 'P' ) { | ||
accu.appendChild( document.createElement( 'P' ) ); | ||
} | ||
|
||
accu.lastChild.appendChild( node ); | ||
// Element nodes. | ||
} else if ( node.nodeType === 1 ) { | ||
// BR nodes: create a new paragraph on double, or append to previous. | ||
if ( node.nodeName === 'BR' ) { | ||
if ( node.nextSibling && node.nextSibling.nodeName === 'BR' ) { | ||
accu.appendChild( document.createElement( 'P' ) ); | ||
decu.removeChild( node.nextSibling ); | ||
} | ||
|
||
// Don't append to an empty paragraph. | ||
if ( | ||
accu.lastChild && | ||
accu.lastChild.nodeName === 'P' && | ||
accu.lastChild.hasChildNodes() | ||
) { | ||
accu.lastChild.appendChild( node ); | ||
} else { | ||
decu.removeChild( node ); | ||
} | ||
} else if ( node.nodeName === 'P' ) { | ||
// Only append non-empty paragraph nodes. | ||
if ( /^(\s| )*$/.test( node.innerHTML ) ) { | ||
decu.removeChild( node ); | ||
} else { | ||
accu.appendChild( node ); | ||
} | ||
} else { | ||
accu.appendChild( node ); | ||
} | ||
} else { | ||
decu.removeChild( node ); | ||
} | ||
} | ||
|
||
return Array.from( accu.childNodes ); | ||
} | ||
|
||
export default function( nodes ) { | ||
return normaliseToBlockLevelNodes( nodes ).map( ( node ) => { | ||
const block = getBlockTypes().reduce( ( acc, blockType ) => { | ||
if ( acc ) { | ||
return acc; | ||
} | ||
|
||
const transformsFrom = get( blockType, 'transforms.from', [] ); | ||
const transform = find( transformsFrom, ( { type } ) => type === 'raw' ); | ||
|
||
if ( ! transform || ! transform.matcher( node ) ) { | ||
return acc; | ||
} | ||
|
||
const { name, defaultAttributes = [] } = blockType; | ||
const attributes = parseBlockAttributes( node.outerHTML, transform.attributes ); | ||
|
||
return createBlock( name, { ...defaultAttributes, ...attributes } ); | ||
}, null ); | ||
|
||
if ( block ) { | ||
return block; | ||
} | ||
|
||
return createBlock( getUnknownTypeHandler(), { | ||
content: nodeListToReact( [ node ], createElement ), | ||
} ); | ||
} ); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { describe, it } from 'mocha'; | ||
import { equal } from 'assert'; | ||
import { JSDOM } from 'jsdom'; | ||
|
||
const { window } = new JSDOM(); | ||
const { document } = window; | ||
|
||
import { normaliseToBlockLevelNodes } from '../paste'; | ||
|
||
describe( 'normaliseToBlockLevelNodes', () => { | ||
function createNodes( HTML ) { | ||
document.body.innerHTML = HTML; | ||
return Array.from( document.body.childNodes ); | ||
} | ||
|
||
function outerHTML( nodes ) { | ||
return nodes.map( node => node.outerHTML ).join( '' ); | ||
} | ||
|
||
function transform( HTML ) { | ||
return outerHTML( normaliseToBlockLevelNodes( createNodes( HTML ) ) ); | ||
} | ||
|
||
it( 'should convert double line breaks to paragraphs', () => { | ||
equal( transform( 'test<br><br>test' ), '<p>test</p><p>test</p>' ); | ||
} ); | ||
|
||
it( 'should not convert single line break to paragraphs', () => { | ||
equal( transform( 'test<br>test' ), '<p>test<br>test</p>' ); | ||
} ); | ||
|
||
it( 'should not add extra line at the start', () => { | ||
equal( transform( 'test<br><br><br>test' ), '<p>test</p><p>test</p>' ); | ||
equal( transform( '<br>test<br><br>test' ), '<p>test</p><p>test</p>' ); | ||
} ); | ||
|
||
it( 'should preserve non-inline content', () => { | ||
const HTML = '<p>test</p><div>test<br>test</div>'; | ||
equal( transform( HTML ), HTML ); | ||
} ); | ||
|
||
it( 'should remove empty paragraphs', () => { | ||
equal( transform( '<p> </p>' ), '' ); | ||
} ); | ||
} ); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,23 @@ registerBlockType( 'core/image', { | |
caption: children( 'figcaption' ), | ||
}, | ||
|
||
transforms: { | ||
from: [ | ||
{ | ||
type: 'raw', | ||
matcher: ( node ) => ( | ||
node.nodeName === 'IMG' || | ||
( ! node.textContent && node.querySelector( 'img' ) ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we be stricter here? I'm afraid we'd use lose here. I=agin a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I misread the condition. |
||
), | ||
attributes: { | ||
url: attr( 'img', 'src' ), | ||
alt: attr( 'img', 'alt' ), | ||
caption: children( 'figcaption' ), | ||
}, | ||
}, | ||
], | ||
}, | ||
|
||
getEditWrapperProps( attributes ) { | ||
const { align } = attributes; | ||
if ( 'left' === align || 'right' === align || 'wide' === align || 'full' === align ) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could split out the logic here out of the
Editable
into theparse.js
file. I mean thepasteHandler
could probably check for the block delimiters itself, don't you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I think part of this should be in Editable because it needs to decide what content it con handle itself (inline), and then pass the rest to the paste handler. This can probably be improved though. Maybe we should only do the black check and leave out the comment check.