Skip to content

Commit

Permalink
Remove TinyMCE so it doesn't interfere with our boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Jan 30, 2019
1 parent fa8722e commit e269593
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 392 deletions.
196 changes: 196 additions & 0 deletions packages/editor/src/components/rich-text/editable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* External dependencies
*/
import { isEqual } from 'lodash';
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { Component, createElement } from '@wordpress/element';
import { BACKSPACE, DELETE } from '@wordpress/keycodes';

/**
* Internal dependencies
*/
import { diffAriaProps, pickAriaProps } from './aria';

/**
* Browser dependencies
*/

const { userAgent } = window.navigator;

/**
* Applies a fix that provides `input` events for contenteditable in Internet Explorer.
*
* @param {Element} editorNode The root editor node.
*
* @return {Function} A function to remove the fix (for cleanup).
*/
function applyInternetExplorerInputFix( editorNode ) {
/**
* Dispatches `input` events in response to `textinput` events.
*
* IE provides a `textinput` event that is similar to an `input` event,
* and we use it to manually dispatch an `input` event.
* `textinput` is dispatched for text entry but for not deletions.
*
* @param {Event} textInputEvent An Internet Explorer `textinput` event.
*/
function mapTextInputEvent( textInputEvent ) {
textInputEvent.stopImmediatePropagation();

const inputEvent = document.createEvent( 'Event' );
inputEvent.initEvent( 'input', true, false );
inputEvent.data = textInputEvent.data;
textInputEvent.target.dispatchEvent( inputEvent );
}

/**
* Dispatches `input` events in response to Delete and Backspace keyup.
*
* It would be better dispatch an `input` event after each deleting
* `keydown` because the DOM is updated after each, but it is challenging
* to determine the right time to dispatch `input` since propagation of
* `keydown` can be stopped at any point.
*
* It's easier to listen for `keyup` in the capture phase and dispatch
* `input` before `keyup` propagates further. It's not perfect, but should
* be good enough.
*
* @param {KeyboardEvent} keyUp
* @param {Node} keyUp.target The event target.
* @param {number} keyUp.keyCode The key code.
*/
function mapDeletionKeyUpEvents( { target, keyCode } ) {
const isDeletion = BACKSPACE === keyCode || DELETE === keyCode;

if ( isDeletion && editorNode.contains( target ) ) {
const inputEvent = document.createEvent( 'Event' );
inputEvent.initEvent( 'input', true, false );
inputEvent.data = null;
target.dispatchEvent( inputEvent );
}
}

editorNode.addEventListener( 'textinput', mapTextInputEvent );
document.addEventListener( 'keyup', mapDeletionKeyUpEvents, true );
return function removeInternetExplorerInputFix() {
editorNode.removeEventListener( 'textinput', mapTextInputEvent );
document.removeEventListener( 'keyup', mapDeletionKeyUpEvents, true );
};
}

const IS_PLACEHOLDER_VISIBLE_ATTR_NAME = 'data-is-placeholder-visible';
const CLASS_NAME = 'editor-rich-text__editable';

/**
* Whether or not the user agent is Internet Explorer.
*
* @type {boolean}
*/
const IS_IE = userAgent.indexOf( 'Trident' ) >= 0;

export default class Editable extends Component {
constructor() {
super();
this.bindEditorNode = this.bindEditorNode.bind( this );
this.onFocus = this.onFocus.bind( this );
}

onFocus() {
if ( this.props.onFocus ) {
this.props.onFocus();
}
}

// We must prevent rerenders because the browser will modify the DOM. React
// will rerender the DOM fine, but we're losing selection and it would be
// more expensive to do so as it would just set the inner HTML through
// `dangerouslySetInnerHTML`. Instead RichText does it's own diffing and
// selection setting.
//
// Because we never update the component, we have to look through props and
// update the attributes on the wrapper nodes here. `componentDidUpdate`
// will never be called.
shouldComponentUpdate( nextProps ) {
this.configureIsPlaceholderVisible( nextProps.isPlaceholderVisible );

if ( ! isEqual( this.props.style, nextProps.style ) ) {
this.editorNode.setAttribute( 'style', '' );
Object.assign( this.editorNode.style, nextProps.style );
}

if ( ! isEqual( this.props.className, nextProps.className ) ) {
this.editorNode.className = classnames( nextProps.className, CLASS_NAME );
}

const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps );
removedKeys.forEach( ( key ) =>
this.editorNode.removeAttribute( key ) );
updatedKeys.forEach( ( key ) =>
this.editorNode.setAttribute( key, nextProps[ key ] ) );

return false;
}

configureIsPlaceholderVisible( isPlaceholderVisible ) {
const isPlaceholderVisibleString = String( !! isPlaceholderVisible );
if ( this.editorNode.getAttribute( IS_PLACEHOLDER_VISIBLE_ATTR_NAME ) !== isPlaceholderVisibleString ) {
this.editorNode.setAttribute( IS_PLACEHOLDER_VISIBLE_ATTR_NAME, isPlaceholderVisibleString );
}
}

bindEditorNode( editorNode ) {
this.editorNode = editorNode;

if ( this.props.setRef ) {
this.props.setRef( editorNode );
}

if ( IS_IE ) {
if ( editorNode ) {
// Mounting:
this.removeInternetExplorerInputFix = applyInternetExplorerInputFix( editorNode );
} else {
// Unmounting:
this.removeInternetExplorerInputFix();
}
}
}

render() {
const ariaProps = pickAriaProps( this.props );
const {
tagName = 'div',
style,
record,
valueToEditableHTML,
className,
isPlaceholderVisible,
onPaste,
onInput,
onKeyDown,
onCompositionEnd,
onBlur,
} = this.props;

return createElement( tagName, {
...ariaProps,
className: classnames( className, CLASS_NAME ),
contentEditable: true,
[ IS_PLACEHOLDER_VISIBLE_ATTR_NAME ]: isPlaceholderVisible,
ref: this.bindEditorNode,
style,
suppressContentEditableWarning: true,
dangerouslySetInnerHTML: { __html: valueToEditableHTML( record ) },
onPaste,
onInput,
onFocus: this.onFocus,
onBlur,
onKeyDown,
onCompositionEnd,
} );
}
}
17 changes: 8 additions & 9 deletions packages/editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import Autocomplete from '../autocomplete';
import BlockFormatControls from '../block-format-controls';
import FormatEdit from './format-edit';
import FormatToolbar from './format-toolbar';
import TinyMCE, { TINYMCE_ZWSP } from './tinymce';
import Editable from './editable';
import { pickAriaProps } from './aria';
import { getPatterns } from './patterns';
import { withBlockEditContext } from '../block-edit/context';
Expand Down Expand Up @@ -165,7 +165,7 @@ export class RichText extends Component {
removeNode: ( node ) => node.getAttribute( 'data-mce-bogus' ) === 'all',
unwrapNode: ( node ) => !! node.getAttribute( 'data-mce-bogus' ),
removeAttribute: ( attribute ) => attribute.indexOf( 'data-mce-' ) === 0,
filterString: ( string ) => string.replace( TINYMCE_ZWSP, '' ),
filterString: ( string ) => string.replace( '\uFEFF', '' ),
prepareEditableTree: this.props.prepareEditableTree,
} );
}
Expand Down Expand Up @@ -814,13 +814,12 @@ export class RichText extends Component {
onTagNameChange,
} = this.props;

// Generating a key that includes `tagName` ensures that if the tag
// changes, we replace the relevant element. This is needed because we
// prevent Editable component updates.
const key = Tagname;
const MultilineTag = this.multilineTag;
const ariaProps = pickAriaProps( this.props );

// Generating a key that includes `tagName` ensures that if the tag
// changes, we unmount and destroy the previous TinyMCE element, then
// mount and initialize a new child element in its place.
const key = [ 'editor', Tagname ].join();
const isPlaceholderVisible = placeholder && ( ! isSelected || keepPlaceholderOnFocus ) && this.isEmpty();
const classes = classnames( wrapperClassName, 'editor-rich-text' );
const record = this.getRecord();
Expand Down Expand Up @@ -855,7 +854,7 @@ export class RichText extends Component {
>
{ ( { listBoxId, activeId } ) => (
<Fragment>
<TinyMCE
<Editable
tagName={ Tagname }
style={ style }
record={ record }
Expand All @@ -880,7 +879,7 @@ export class RichText extends Component {
/>
{ isPlaceholderVisible &&
<Tagname
className={ classnames( 'editor-rich-text__tinymce', className ) }
className={ classnames( 'editor-rich-text__editable', className ) }
style={ style }
>
{ MultilineTag ? <MultilineTag>{ placeholder }</MultilineTag> : placeholder }
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/components/rich-text/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
position: relative;
}

.editor-rich-text__tinymce {
.editor-rich-text__editable {
margin: 0;
position: relative;
line-height: $editor-line-height;
Expand Down Expand Up @@ -113,7 +113,7 @@
}

// Placeholder text.
& + .editor-rich-text__tinymce {
& + .editor-rich-text__editable {
pointer-events: none;

// Use opacity to work in various editor styles.
Expand All @@ -126,7 +126,7 @@

// Captions may have lighter (gray) text, or be shown on a range of different background luminosites.
// To ensure legibility, we increase the default placeholder opacity to ensure contrast.
&[data-is-placeholder-visible="true"] + figcaption.editor-rich-text__tinymce {
&[data-is-placeholder-visible="true"] + figcaption.editor-rich-text__editable {
opacity: 0.8;
}
}
Expand Down
Loading

0 comments on commit e269593

Please sign in to comment.