Skip to content

Commit

Permalink
RichText: make window/document agnostic (#21105)
Browse files Browse the repository at this point in the history
* RichText: make window/document agnostic

* getComputedStyle

* Fix unit tests
  • Loading branch information
ellatrix authored Mar 24, 2020
1 parent 2ebf486 commit c6df2f4
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 67 deletions.
23 changes: 13 additions & 10 deletions packages/rich-text/src/component/boundary-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,6 @@
*/
import { useEffect } from '@wordpress/element';

/**
* Global stylesheet shared by all RichText instances.
*/
const globalStyle = document.createElement( 'style' );

const boundarySelector = '*[data-rich-text-format-boundary]';

document.head.appendChild( globalStyle );

/**
* Calculates and renders the format boundary style when the active formats
* change.
Expand All @@ -24,19 +15,31 @@ export function BoundaryStyle( { activeFormats, forwardedRef } ) {
return;
}

const boundarySelector = '*[data-rich-text-format-boundary]';
const element = forwardedRef.current.querySelector( boundarySelector );

if ( ! element ) {
return;
}

const computedStyle = window.getComputedStyle( element );
const { ownerDocument } = element;
const { defaultView } = ownerDocument;
const computedStyle = defaultView.getComputedStyle( element );
const newColor = computedStyle.color
.replace( ')', ', 0.2)' )
.replace( 'rgb', 'rgba' );
const selector = `.rich-text:focus ${ boundarySelector }`;
const rule = `background-color: ${ newColor }`;
const style = `${ selector } {${ rule }}`;
const globalStyleId = 'rich-text-boundary-style';

let globalStyle = ownerDocument.getElementById( globalStyleId );

if ( ! globalStyle ) {
globalStyle = ownerDocument.createElement( 'style' );
globalStyle.id = globalStyleId;
ownerDocument.head.appendChild( globalStyle );
}

if ( globalStyle.innerHTML !== style ) {
globalStyle.innerHTML = style;
Expand Down
54 changes: 34 additions & 20 deletions packages/rich-text/src/component/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ import withFormatTypes from './with-format-types';
import { BoundaryStyle } from './boundary-style';
import { InlineWarning } from './inline-warning';

/**
* Browser dependencies
*/

const { getSelection, getComputedStyle } = window;

/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */

/**
Expand Down Expand Up @@ -113,9 +107,11 @@ function createPrepareEditableTree( props, prefix ) {
/**
* If the selection is set on the placeholder element, collapse the selection to
* the start (before the placeholder).
*
* @param {Window} defaultView
*/
function fixPlaceholderSelection() {
const selection = window.getSelection();
function fixPlaceholderSelection( defaultView ) {
const selection = defaultView.getSelection();
const { anchorNode, anchorOffset } = selection;

if ( anchorNode.nodeType !== anchorNode.ELEMENT_NODE ) {
Expand All @@ -142,6 +138,8 @@ class RichText extends Component {
constructor( { value, selectionStart, selectionEnd } ) {
super( ...arguments );

this.getDocument = this.getDocument.bind( this );
this.getWindow = this.getWindow.bind( this );
this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
this.onChange = this.onChange.bind( this );
Expand Down Expand Up @@ -186,24 +184,32 @@ class RichText extends Component {
}

componentWillUnmount() {
document.removeEventListener(
this.getDocument().removeEventListener(
'selectionchange',
this.onSelectionChange
);
window.cancelAnimationFrame( this.rafId );
this.getWindow().cancelAnimationFrame( this.rafId );
}

componentDidMount() {
this.applyRecord( this.record, { domOnly: true } );
}

getDocument() {
return this.props.forwardedRef.current.ownerDocument;
}

getWindow() {
return this.getDocument().defaultView;
}

createRecord() {
const {
__unstableMultilineTag: multilineTag,
forwardedRef,
preserveWhiteSpace,
} = this.props;
const selection = getSelection();
const selection = this.getWindow().getSelection();
const range =
selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null;

Expand Down Expand Up @@ -401,9 +407,14 @@ class RichText extends Component {
// frame. The event listener for selection changes may be added too late
// at this point, but this focus event is still too early to calculate
// the selection.
this.rafId = window.requestAnimationFrame( this.onSelectionChange );
this.rafId = this.getWindow().requestAnimationFrame(
this.onSelectionChange
);

document.addEventListener( 'selectionchange', this.onSelectionChange );
this.getDocument().addEventListener(
'selectionchange',
this.onSelectionChange
);

if ( this.props.setFocusedElement ) {
deprecated( 'wp.blockEditor.RichText setFocusedElement prop', {
Expand All @@ -414,7 +425,7 @@ class RichText extends Component {
}

onBlur() {
document.removeEventListener(
this.getDocument().removeEventListener(
'selectionchange',
this.onSelectionChange
);
Expand Down Expand Up @@ -514,7 +525,7 @@ class RichText extends Component {
// Do not update the selection when characters are being composed as
// this rerenders the component and might distroy internal browser
// editing state.
document.removeEventListener(
this.getDocument().removeEventListener(
'selectionchange',
this.onSelectionChange
);
Expand All @@ -526,7 +537,10 @@ class RichText extends Component {
// input event after composition.
this.onInput( { inputType: 'insertText' } );
// Tracking selection changes can be resumed.
document.addEventListener( 'selectionchange', this.onSelectionChange );
this.getDocument().addEventListener(
'selectionchange',
this.onSelectionChange
);
}

/**
Expand Down Expand Up @@ -569,7 +583,7 @@ class RichText extends Component {
// element, in which case the caret is not visible. We need to set
// the caret before the placeholder if that's the case.
if ( value.text.length === 0 && start === 0 ) {
fixPlaceholderSelection();
fixPlaceholderSelection( this.getWindow() );
}

return;
Expand Down Expand Up @@ -830,7 +844,7 @@ class RichText extends Component {
const { text, formats, start, end, activeFormats = [] } = value;
const collapsed = isCollapsed( value );
// To do: ideally, we should look at visual position instead.
const { direction } = getComputedStyle(
const { direction } = this.getWindow().getComputedStyle(
this.props.forwardedRef.current
);
const reverseKey = direction === 'rtl' ? RIGHT : LEFT;
Expand Down Expand Up @@ -933,8 +947,8 @@ class RichText extends Component {

const { parentNode } = target;
const index = Array.from( parentNode.childNodes ).indexOf( target );
const range = target.ownerDocument.createRange();
const selection = getSelection();
const range = this.getDocument().createRange();
const selection = this.getWindow().getSelection();

range.setStart( target.parentNode, index );
range.setEnd( target.parentNode, index + 1 );
Expand Down
6 changes: 3 additions & 3 deletions packages/rich-text/src/component/inline-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { useEffect } from '@wordpress/element';
export function InlineWarning( { forwardedRef } ) {
useEffect( () => {
if ( process.env.NODE_ENV === 'development' ) {
const computedStyle = window.getComputedStyle(
forwardedRef.current
);
const target = forwardedRef.current;
const { defaultView } = target.ownerDocument;
const computedStyle = defaultView.getComputedStyle( target );

if ( computedStyle.display === 'inline' ) {
// eslint-disable-next-line no-console
Expand Down
16 changes: 6 additions & 10 deletions packages/rich-text/src/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ import {
ZWNBSP,
} from './special-characters';

/**
* Browser dependencies
*/

const { TEXT_NODE, ELEMENT_NODE } = window.Node;

function createEmptyValue() {
return {
formats: [],
Expand Down Expand Up @@ -160,6 +154,8 @@ export function create( {
}

if ( typeof html === 'string' && html.length > 0 ) {
// It does not matter which document this is, we're just using it to
// parse.
element = createElement( document, html );
}

Expand Down Expand Up @@ -208,7 +204,7 @@ function accumulateSelection( accumulator, node, range, value ) {
if ( value.start !== undefined ) {
accumulator.start = currentLength + value.start;
// Range indicates that the current node has selection.
} else if ( node === startContainer && node.nodeType === TEXT_NODE ) {
} else if ( node === startContainer && node.nodeType === node.TEXT_NODE ) {
accumulator.start = currentLength + startOffset;
// Range indicates that the current node is selected.
} else if (
Expand All @@ -231,7 +227,7 @@ function accumulateSelection( accumulator, node, range, value ) {
if ( value.end !== undefined ) {
accumulator.end = currentLength + value.end;
// Range indicates that the current node has selection.
} else if ( node === endContainer && node.nodeType === TEXT_NODE ) {
} else if ( node === endContainer && node.nodeType === node.TEXT_NODE ) {
accumulator.end = currentLength + endOffset;
// Range indicates that the current node is selected.
} else if (
Expand Down Expand Up @@ -342,7 +338,7 @@ function createFromElement( {
const node = element.childNodes[ index ];
const type = node.nodeName.toLowerCase();

if ( node.nodeType === TEXT_NODE ) {
if ( node.nodeType === node.TEXT_NODE ) {
let filter = removePadding;

if ( ! preserveWhiteSpace ) {
Expand All @@ -361,7 +357,7 @@ function createFromElement( {
continue;
}

if ( node.nodeType !== ELEMENT_NODE ) {
if ( node.nodeType !== node.ELEMENT_NODE ) {
continue;
}

Expand Down
45 changes: 21 additions & 24 deletions packages/rich-text/src/to-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@
import { toTree } from './to-tree';
import { createElement } from './create-element';

/**
* Browser dependencies
*/

const { TEXT_NODE } = window.Node;

/**
* Creates a path as an array of indices from the given root node to the given
* node.
Expand Down Expand Up @@ -59,18 +53,6 @@ function getNodeByPath( node, path ) {
};
}

/**
* Returns a new instance of a DOM tree upon which RichText operations can be
* applied.
*
* Note: The current implementation will return a shared reference, reset on
* each call to `createEmpty`. Therefore, you should not hold a reference to
* the value to operate upon asynchronously, as it may have unexpected results.
*
* @return {Object} RichText tree.
*/
const createEmpty = () => createElement( document, '' );

function append( element, child ) {
if ( typeof child === 'string' ) {
child = element.ownerDocument.createTextNode( child );
Expand Down Expand Up @@ -101,8 +83,8 @@ function getParent( { parentNode } ) {
return parentNode;
}

function isText( { nodeType } ) {
return nodeType === TEXT_NODE;
function isText( node ) {
return node.nodeType === node.TEXT_NODE;
}

function getText( { nodeValue } ) {
Expand All @@ -119,6 +101,7 @@ export function toDom( {
prepareEditableTree,
isEditableTree = true,
placeholder,
doc = document,
} ) {
let startPath = [];
let endPath = [];
Expand All @@ -130,6 +113,18 @@ export function toDom( {
};
}

/**
* Returns a new instance of a DOM tree upon which RichText operations can be
* applied.
*
* Note: The current implementation will return a shared reference, reset on
* each call to `createEmpty`. Therefore, you should not hold a reference to
* the value to operate upon asynchronously, as it may have unexpected results.
*
* @return {Object} RichText tree.
*/
const createEmpty = () => createElement( doc, '' );

const tree = toTree( {
value,
multilineTag,
Expand Down Expand Up @@ -186,6 +181,7 @@ export function apply( {
multilineTag,
prepareEditableTree,
placeholder,
doc: current.ownerDocument,
} );

applyValue( body, current );
Expand All @@ -207,7 +203,7 @@ export function applyValue( future, current ) {
} else if ( ! currentChild.isEqualNode( futureChild ) ) {
if (
currentChild.nodeName !== futureChild.nodeName ||
( currentChild.nodeType === TEXT_NODE &&
( currentChild.nodeType === currentChild.TEXT_NODE &&
currentChild.data !== futureChild.data )
) {
current.replaceChild( futureChild, currentChild );
Expand Down Expand Up @@ -282,8 +278,9 @@ export function applySelection( { startPath, endPath }, current ) {
current,
endPath
);
const selection = window.getSelection();
const { ownerDocument } = current;
const { defaultView } = ownerDocument;
const selection = defaultView.getSelection();
const range = ownerDocument.createRange();

range.setStart( startContainer, startOffset );
Expand All @@ -306,13 +303,13 @@ export function applySelection( { startPath, endPath }, current ) {
// This function is not intended to cause a shift in focus. Since the above
// selection manipulations may shift focus, ensure that focus is restored to
// its previous state.
if ( activeElement !== document.activeElement ) {
if ( activeElement !== ownerDocument.activeElement ) {
// The `instanceof` checks protect against edge cases where the focused
// element is not of the interface HTMLElement (does not have a `focus`
// or `blur` property).
//
// See: https://github.com/Microsoft/TypeScript/issues/5901#issuecomment-431649653
if ( activeElement instanceof window.HTMLElement ) {
if ( activeElement instanceof defaultView.HTMLElement ) {
activeElement.focus();
}
}
Expand Down

0 comments on commit c6df2f4

Please sign in to comment.