Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 104 additions & 1 deletion editor/components/rich-text/tinymce.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,101 @@ import classnames from 'classnames';
* WordPress dependencies
*/
import { Component, createElement } from '@wordpress/element';
import { keycodes } from '@wordpress/utils';

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

const { BACKSPACE, DELETE } = keycodes;

/**
* Determines whether we need a fix to provide `input` events for contenteditable.
*
* @param {Element} editorNode The root editor node.
*
* @return {boolean} A boolean indicating whether the fix is needed.
*/
function needsInternetExplorerInputFix( editorNode ) {
return (
// Rely on userAgent in the absence of a reasonable feature test for contenteditable `input` events.
/Trident/.test( window.navigator.userAgent ) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we borrow what Modernizr does here? Or is that for a different input event?

https://github.com/Modernizr/Modernizr/blob/master/feature-detects/event/oninput.js

Copy link
Member Author

@brandonpayton brandonpayton Jun 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that too. Unfortunately, the test returns true for even a non-contenteditable <div> in IE11 because of the first part that sets an oninput attribute and checks the type of the element's oninput property. The browser does support the input event but doesn't dispatch it for contenteditable text input.

I tried to adapt the latter half of the test that dispatches a KeyboardEvent and listens for input but didn't have any luck getting it to work in IE11. It's possible I missed something but didn't think I should spend more time on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, thank you for taking the time to look and make this suggestion, @noisysocks . I actually forgot that I'd looked at this until I worked through it again. :)

// IE11 dispatches input events for `<input>` and `<textarea>`.
! /input/i.test( editorNode.tagName ) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we check this? A rich text field can never be an input or textarea.

! /textarea/i.test( editorNode.tagName )
);
}

/**
* 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';
export default class TinyMCE extends Component {
constructor() {
super();
this.bindEditorNode = this.bindEditorNode.bind( this );
}

componentDidMount() {
this.initialize();
}
Expand Down Expand Up @@ -96,6 +182,23 @@ export default class TinyMCE extends Component {
} );
}

bindEditorNode( editorNode ) {
this.editorNode = editorNode;

/**
* A ref function can be used for cleanup because React calls it with
* `null` when unmounting.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's cool!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I love being able to keep allocation and cleanup in one place while also handling real situations where a ref is removed and re-added during the life of a component.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we just use componentDidMount and componentWillUnmount ?

Related: The latest version of React includes a new React.createRef function which may become the recommended standard in place of the ref callback:

https://reactjs.org/docs/refs-and-the-dom.html#creating-refs

if ( this.removeInternetExplorerInputFix ) {
this.removeInternetExplorerInputFix();
this.removeInternetExplorerInputFix = null;
}

if ( editorNode && needsInternetExplorerInputFix( editorNode ) ) {
this.removeInternetExplorerInputFix = applyInternetExplorerInputFix( editorNode );
}
}

render() {
const { tagName = 'div', style, defaultValue, className, isPlaceholderVisible, format } = this.props;
const ariaProps = pickAriaProps( this.props );
Expand All @@ -112,7 +215,7 @@ export default class TinyMCE extends Component {
className: classnames( className, 'editor-rich-text__tinymce' ),
contentEditable: true,
[ IS_PLACEHOLDER_VISIBLE_ATTR_NAME ]: isPlaceholderVisible,
ref: ( node ) => this.editorNode = node,
ref: this.bindEditorNode,
style,
suppressContentEditableWarning: true,
dangerouslySetInnerHTML: { __html: valueToString( defaultValue, format ) },
Expand Down