-
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: separate out content ops and switch to using tinymce tree output #3713
Changes from all commits
95e925f
a66fecc
a8590c2
2010ef1
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,117 @@ | ||
import { renderToString } from '@wordpress/element'; | ||
import { last } from 'lodash'; | ||
import { childrenToReact } from './tree'; | ||
|
||
export function nodeListToReact( editor, nodeList ) { | ||
const fragment = editor.getDoc().createDocumentFragment(); | ||
|
||
nodeList.forEach( function( node ) { | ||
fragment.appendChild( node.cloneNode( true ) ); | ||
} ); | ||
|
||
return childrenToReact( editor.serializer.serialize( fragment, { format: 'tree' } ) ); | ||
} | ||
|
||
function splitResult( before, after ) { | ||
return { before: before, after: after }; | ||
} | ||
|
||
export function setContent( editor, content ) { | ||
if ( ! content ) { | ||
content = ''; | ||
} | ||
|
||
content = renderToString( content ); | ||
editor.setContent( content, { format: 'raw' } ); | ||
} | ||
|
||
export function getContent( editor ) { | ||
return childrenToReact( editor.getContent( { format: 'tree' } ) ); | ||
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 shape has this "tree" format? Can you share an example? Do you think it makes sense to avoid the "react" element format at all and just use the tree format in the state as well (outside Editable), in which case, it requires some refactoring but might be worth it (same as #3048) 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 created a simple gist doc on the structure of this tree. I guess using it for tinymce related things makes sense but unsure if it's a general enough format for usage for other things since each node has methods on it for mutation. The react format seems simpler in that regard at the least it's input format. 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 you serialize this "tree" using 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. We could convert the tree into the simple json format that was suggested else where. It's pretty fast to just convert a one tree structure to another if it's just simple objects. We just need to implement a way to set the tree back in tiny just before the filters apply. |
||
} | ||
|
||
export function getSplitAtLine( editor ) { | ||
const rootNode = editor.getBody(); | ||
const selectedNode = editor.selection.getNode(); | ||
|
||
if ( selectedNode.parentNode !== rootNode ) { | ||
return null; | ||
} | ||
|
||
const dom = editor.dom; | ||
|
||
if ( ! dom.isEmpty( selectedNode ) ) { | ||
return null; | ||
} | ||
|
||
const childNodes = Array.from( rootNode.childNodes ); | ||
const index = dom.nodeIndex( selectedNode ); | ||
const beforeNodes = childNodes.slice( 0, index ); | ||
const afterNodes = childNodes.slice( index + 1 ); | ||
const beforeElement = nodeListToReact( editor, beforeNodes ); | ||
const afterElement = nodeListToReact( editor, afterNodes ); | ||
|
||
return splitResult( beforeElement, afterElement ); | ||
} | ||
|
||
export function splitAtCaret( editor ) { | ||
const { dom } = editor; | ||
const rootNode = editor.getBody(); | ||
const beforeRange = dom.createRng(); | ||
const afterRange = dom.createRng(); | ||
const selectionRange = editor.selection.getRng(); | ||
|
||
beforeRange.setStart( rootNode, 0 ); | ||
beforeRange.setEnd( selectionRange.startContainer, selectionRange.startOffset ); | ||
|
||
afterRange.setStart( selectionRange.endContainer, selectionRange.endOffset ); | ||
afterRange.setEnd( rootNode, dom.nodeIndex( rootNode.lastChild ) + 1 ); | ||
|
||
const beforeFragment = beforeRange.cloneContents(); | ||
const afterFragment = afterRange.cloneContents(); | ||
|
||
const beforeElement = nodeListToReact( editor, beforeFragment.childNodes ); | ||
const afterElement = nodeListToReact( editor, afterFragment.childNodes ); | ||
|
||
return splitResult( beforeElement, afterElement ); | ||
} | ||
|
||
export function splitAtBlock( editor ) { | ||
// Getting the content before and after the cursor | ||
const childNodes = Array.from( editor.getBody().childNodes ); | ||
let selectedChild = editor.selection.getStart(); | ||
while ( childNodes.indexOf( selectedChild ) === -1 && selectedChild.parentNode ) { | ||
selectedChild = selectedChild.parentNode; | ||
} | ||
const splitIndex = childNodes.indexOf( selectedChild ); | ||
if ( splitIndex === -1 ) { | ||
return null; | ||
} | ||
const beforeNodes = childNodes.slice( 0, splitIndex ); | ||
const lastNodeBeforeCursor = last( beforeNodes ); | ||
// Avoid splitting on single enter | ||
if ( | ||
! lastNodeBeforeCursor || | ||
beforeNodes.length < 2 || | ||
!! lastNodeBeforeCursor.textContent | ||
) { | ||
return null; | ||
} | ||
|
||
const before = beforeNodes.slice( 0, beforeNodes.length - 1 ); | ||
|
||
// Removing empty nodes from the beginning of the "after" | ||
// avoids empty paragraphs at the beginning of newly created blocks. | ||
const after = childNodes.slice( splitIndex ).reduce( ( memo, node ) => { | ||
if ( ! memo.length && ! node.textContent ) { | ||
return memo; | ||
} | ||
|
||
memo.push( node ); | ||
return memo; | ||
}, [] ); | ||
|
||
const beforeElement = nodeListToReact( editor, before ); | ||
const afterElement = nodeListToReact( editor, after ); | ||
|
||
return splitResult( beforeElement, afterElement ); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { createElement } from '@wordpress/element'; | ||
import { domreact } from '@wordpress/utils'; | ||
|
||
const ELEMENT_NODE = 1; | ||
const TEXT_NODE = 3; | ||
|
||
function attributesToReact( attributes ) { | ||
const reactAttrs = {}; | ||
|
||
attributes.forEach( ( { name, value } ) => { | ||
const canonicalKey = domreact.toCanonical( name ); | ||
const key = canonicalKey ? canonicalKey : name; | ||
reactAttrs[ key ] = key === 'style' ? domreact.styleStringToJSON( value ) : value; | ||
} ); | ||
|
||
return reactAttrs; | ||
} | ||
|
||
function elementToReact( node ) { | ||
const props = node.attributes ? attributesToReact( node.attributes ) : {}; | ||
const children = node.firstChild ? childrenToReact( node ) : []; | ||
|
||
return createElement( node.name, props, ...children ); | ||
} | ||
|
||
function nodeToReact( node ) { | ||
if ( ! node ) { | ||
return null; | ||
} else if ( node.type === ELEMENT_NODE ) { | ||
return elementToReact( node ); | ||
} else if ( node.type === TEXT_NODE ) { | ||
return node.value; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
export function childrenToReact( node ) { | ||
const children = []; | ||
|
||
for ( let child = node.firstChild; child; child = child.next ) { | ||
children.push( nodeToReact( child ) ); | ||
} | ||
|
||
return children; | ||
} |
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 use the "tree" format to set content as well?
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.
Currently we only support getting the contents out as the tree but since it's an internal thing we just need to expose I see no reason why we couldn't support setting contents as a tree as well that would by pass the parsing step in the setContent call so it would be more efficient obviously.
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.
That would be awesome, if we could send the same tree back 💯