-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Editable: state as a cleaned up tree #3048
Changes from all commits
2bfc296
7d243fa
0662a94
2a0611e
2a1989c
54d7631
e50084a
7177a6d
9dab041
df23eea
e9b7111
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,65 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { forEach } from 'lodash'; | ||
|
||
export function createSimpleNode( node, filter = Array ) { | ||
if ( ! node ) { | ||
return null; | ||
} | ||
|
||
if ( node.nodeType === 3 ) { | ||
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. This should either have a comment explaining the magic number, or referencing the node type constant https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType#Node_type_constants I think previously these were avoided due to browser support but with increased browser support, we should be okay. I've confirmed the constants exist in IE11: 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 know we've done that everywhere else, sorry. It's just a quick POC. |
||
return node.nodeValue; | ||
} | ||
|
||
if ( node.nodeType !== 1 ) { | ||
return null; | ||
} | ||
|
||
return filter( | ||
node.nodeName.toLowerCase(), | ||
Array.from( node.attributes || [] ).reduce( ( acc, { name, value } ) => ( { | ||
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. Micro-optimization: Use Lodash's reduce( node.attributes, ( acc, { name, value } ) => ( { |
||
...acc, | ||
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. Micro-optimization / readability: While this reads nicely, it also creates a shallow clone unnecessarily on each iteration, where we'd be fine to mutate the accumulator: reduce( node.attributes, ( acc, { name, value } ) => ( {
acc[ name ] = value;
return acc;
} ) 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. or use a forEach( node.attributes, ( { name, value } ) => acc[ name ] = value ); |
||
[ name ]: value, | ||
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 wonder, could Lodash's toPlainObject( node.attributes ) |
||
} ), {} ), | ||
...createSimpleNodeList( node.childNodes || [], filter ) | ||
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. When will |
||
); | ||
} | ||
|
||
export function createSimpleNodeList( nodeList, filter ) { | ||
return Array.from( nodeList ).reduce( ( acc, node ) => { | ||
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. Micro-optimization / readability: Use Lodash's reduce( nodeList, ( acc, node ) => ( { 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. Actually, given the task performed, this could be a good place for flatMap( nodeList, ( node ) => {
const child = createSimpleNode( node, filter );
const shouldNest = …;
return shouldNest ? [ child ] : child;
} ); this assumes that, when |
||
const child = createSimpleNode( node, filter ); | ||
|
||
if ( Array.isArray( child ) && typeof child[ 0 ] !== 'string' ) { | ||
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. What is the cases where this condition varies? Needs comment and unit tests. |
||
acc.push( ...child ); | ||
} else if ( child ) { | ||
acc.push( child ); | ||
} | ||
|
||
return acc; | ||
}, [] ); | ||
} | ||
|
||
// Smarter application in the future? | ||
export function applySimpleNodeList( tree, node ) { | ||
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 was initially confused on the naming here, though I think maybe this function could be more useful / understandable if it were to return a document fragment that could be used in Editable via 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. Yeah, wasn't sure what the best way is, but went for appending right away. I thought we night eventually decide to create HTML from the tree and then set that instead. The nice thing about this is that it's fairly easy to create, and in the future we could do some DOM riffing to avoid updates where we can. I actually wonder if we there's nothing from React we can use there. |
||
while ( node.firstChild ) { | ||
node.removeChild( node.firstChild ); | ||
} | ||
|
||
forEach( tree, ( piece ) => { | ||
if ( typeof piece === 'string' ) { | ||
node.appendChild( document.createTextNode( piece ) ); | ||
} else { | ||
const [ name, attributes, ...children ] = piece; | ||
const element = document.createElement( name ); | ||
|
||
forEach( attributes, ( value, key ) => { | ||
element.setAttribute( key, value ); | ||
} ); | ||
|
||
applySimpleNodeList( children, element ); | ||
|
||
node.appendChild( element ); | ||
} | ||
} ); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,13 +14,12 @@ import { | |
defer, | ||
noop, | ||
} from 'lodash'; | ||
import { nodeListToReact } from 'dom-react'; | ||
import 'element-closest'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { createElement, Component, renderToString } from '@wordpress/element'; | ||
import { createElement, Component } from '@wordpress/element'; | ||
import { keycodes, createBlobURL } from '@wordpress/utils'; | ||
import { Slot, Fill } from '@wordpress/components'; | ||
|
||
|
@@ -29,6 +28,8 @@ import { Slot, Fill } from '@wordpress/components'; | |
*/ | ||
import './style.scss'; | ||
import { rawHandler } from '../api'; | ||
import * as matchers from '../api/matchers'; | ||
import { applySimpleNodeList } from '../api/simple-dom'; | ||
import FormatToolbar from './format-toolbar'; | ||
import TinyMCE from './tinymce'; | ||
import { pickAriaProps } from './aria'; | ||
|
@@ -37,7 +38,10 @@ import { EVENTS } from './constants'; | |
|
||
const { BACKSPACE, DELETE, ENTER } = keycodes; | ||
|
||
function createTinyMCEElement( type, props, ...children ) { | ||
const createNode = matchers.node( null, nodeFilter ); | ||
const createChildren = matchers.children( null, nodeFilter ); | ||
|
||
function nodeFilter( type, props, ...children ) { | ||
if ( props[ 'data-mce-bogus' ] === 'all' ) { | ||
return null; | ||
} | ||
|
@@ -46,11 +50,11 @@ function createTinyMCEElement( type, props, ...children ) { | |
return children; | ||
} | ||
|
||
return createElement( | ||
return [ | ||
type, | ||
omitBy( props, ( value, key ) => key.indexOf( 'data-mce-' ) === 0 ), | ||
...children | ||
); | ||
omitBy( props, ( value, key ) => key.indexOf( 'data-mce-' ) === 0 || key === 'key' ), | ||
...children, | ||
]; | ||
} | ||
|
||
function isLinkBoundary( fragment ) { | ||
|
@@ -72,6 +76,39 @@ function getFormatProperties( formatName, parents ) { | |
|
||
const DEFAULT_FORMATS = [ 'bold', 'italic', 'strikethrough', 'link' ]; | ||
|
||
/** | ||
* Transforms internal block's representation into an Element. | ||
* | ||
* @param {Array} value Value to transform | ||
* @return {WPElement} Element. | ||
*/ | ||
function valueToElement( value ) { | ||
if ( ! Array.isArray( value ) ) { | ||
return value; | ||
} | ||
|
||
return value.map( ( element, i ) => { | ||
if ( typeof element === 'string' ) { | ||
return element; | ||
} | ||
|
||
const [ type, props, ...children ] = element; | ||
const reactProps = Object.keys( props ).reduce( ( accumulator, key ) => { | ||
const mapping = { | ||
class: 'className', | ||
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. It looks like adding those 2 mappings is enough to satisfy React. 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. Okey, style needs its own handling, because I see this: https://reactjs.org/docs/error-decoder.html?invariant=62&args[]= 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. All keys must go camel case, too. |
||
for: 'htmlFor', | ||
}; | ||
|
||
return { | ||
...accumulator, | ||
[ mapping[ key ] ? mapping[ key ] : key ]: props[ key ], | ||
}; | ||
}, { key: i } ); | ||
|
||
return createElement( type, { ...reactProps }, ...valueToElement( children ) ); | ||
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. It can be simply |
||
} ); | ||
} | ||
|
||
export default class Editable extends Component { | ||
constructor( props ) { | ||
super( ...arguments ); | ||
|
@@ -547,8 +584,8 @@ export default class Editable extends Component { | |
const index = dom.nodeIndex( selectedNode ); | ||
const beforeNodes = childNodes.slice( 0, index ); | ||
const afterNodes = childNodes.slice( index + 1 ); | ||
const beforeElement = nodeListToReact( beforeNodes, createTinyMCEElement ); | ||
const afterElement = nodeListToReact( afterNodes, createTinyMCEElement ); | ||
const beforeElement = beforeNodes.map( createNode ); | ||
const afterElement = afterNodes.map( createNode ); | ||
|
||
this.setContent( beforeElement ); | ||
this.props.onSplit( beforeElement, afterElement ); | ||
|
@@ -602,8 +639,8 @@ export default class Editable extends Component { | |
const beforeFragment = beforeRange.extractContents(); | ||
const afterFragment = afterRange.extractContents(); | ||
|
||
const beforeElement = nodeListToReact( beforeFragment.childNodes, createTinyMCEElement ); | ||
const afterElement = isLinkBoundary( afterFragment ) ? [] : nodeListToReact( afterFragment.childNodes, createTinyMCEElement ); | ||
const beforeElement = createChildren( beforeFragment ); | ||
const afterElement = isLinkBoundary( afterFragment ) ? [] : createChildren( afterFragment ); | ||
|
||
this.setContent( beforeElement ); | ||
this.props.onSplit( beforeElement, afterElement, ...blocks ); | ||
|
@@ -656,8 +693,8 @@ export default class Editable extends Component { | |
this.setContent( this.props.value ); | ||
|
||
this.props.onSplit( | ||
nodeListToReact( before, createTinyMCEElement ), | ||
nodeListToReact( after, createTinyMCEElement ) | ||
before.map( createNode ), | ||
after.map( createNode ) | ||
); | ||
} | ||
|
||
|
@@ -687,17 +724,12 @@ export default class Editable extends Component { | |
this.editor.save(); | ||
} | ||
|
||
setContent( content ) { | ||
if ( ! content ) { | ||
content = ''; | ||
} | ||
|
||
content = renderToString( content ); | ||
this.editor.setContent( content, { format: 'raw' } ); | ||
setContent( content = [] ) { | ||
applySimpleNodeList( content, this.editor.getBody() ); | ||
} | ||
|
||
getContent() { | ||
return nodeListToReact( this.editor.getBody().childNodes || [], createTinyMCEElement ); | ||
return createChildren( this.editor.getBody() ); | ||
} | ||
|
||
updateFocus() { | ||
|
@@ -850,7 +882,7 @@ export default class Editable extends Component { | |
getSettings={ this.getSettings } | ||
onSetup={ this.onSetup } | ||
style={ style } | ||
defaultValue={ value } | ||
defaultValue={ valueToElement( value ) } | ||
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. Can we use |
||
isPlaceholderVisible={ isPlaceholderVisible } | ||
aria-label={ placeholder } | ||
{ ...ariaProps } | ||
|
@@ -879,3 +911,5 @@ Editable.defaultProps = { | |
formattingControls: DEFAULT_FORMATS, | ||
formatters: [], | ||
}; | ||
|
||
Editable.Value = ( { value } ) => valueToElement( value ); |
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.
Can we add some JSDoc for these functions?
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.
Sure it's a WIP.