From a87280a8dbac6897ee88cf8ed6cae270a7d35216 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca Date: Fri, 2 Feb 2018 17:48:43 +0000 Subject: [PATCH 1/2] Add withSafeTimeout higher-order component --- .../higher-order/with-safe-timeout/README.md | 25 ++++++++ .../higher-order/with-safe-timeout/index.js | 63 +++++++++++++++++++ components/index.js | 1 + 3 files changed, 89 insertions(+) create mode 100644 components/higher-order/with-safe-timeout/README.md create mode 100644 components/higher-order/with-safe-timeout/index.js diff --git a/components/higher-order/with-safe-timeout/README.md b/components/higher-order/with-safe-timeout/README.md new file mode 100644 index 00000000000000..d03e3eaa243edb --- /dev/null +++ b/components/higher-order/with-safe-timeout/README.md @@ -0,0 +1,25 @@ +withSafeTimeout +=============== + +`withSafeTimeout` is a React [higher-order component](https://facebook.github.io/react/docs/higher-order-components.html) which provides a special version of `window.setTimeout` which respects the original component's lifecycle. Simply put, a function set to be called in the future via `setSafeTimeout` will never be called if the original component instance ceases to exist in the meantime. + +## Usage + +```jsx +/** + * WordPress dependencies + */ +import { withSafeTimeout } from '@wordpress/components'; + +function MyEffectfulComponent( { setSafeTimeout } ) { + return ( + { + setSafeTimeout( delayedAction, 0 ); + } } + /> + ); +} + +export default withSafeTimeout( MyEffectfulComponent ); +``` diff --git a/components/higher-order/with-safe-timeout/index.js b/components/higher-order/with-safe-timeout/index.js new file mode 100644 index 00000000000000..79e92531ef26ec --- /dev/null +++ b/components/higher-order/with-safe-timeout/index.js @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import { without } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; + +/** + * Browser dependencies + */ +const { clearTimeout, setTimeout } = window; + +/** + * A higher-order component used to provide and manage delayed function calls + * that ought to be bound to a component's lifecycle. + * + * @param {Component} OriginalComponent Component requiring setTimeout + * + * @return {Component} Wrapped component. + */ +function withSafeTimeout( OriginalComponent ) { + return class WrappedComponent extends Component { + constructor() { + super( ...arguments ); + this.timeouts = []; + this.setTimeout = this.setTimeout.bind( this ); + this.clearTimeout = this.clearTimeout.bind( this ); + } + + componentWillUnmount() { + this.timeouts.forEach( clearTimeout ); + } + + setTimeout( fn, delay ) { + const id = setTimeout( () => { + fn(); + this.clearTimeout( id ); + }, delay ); + this.timeouts.push( id ); + return id; + } + + clearTimeout( id ) { + clearTimeout( id ); + this.timeouts = without( this.timeouts, id ); + } + + render() { + return ( + + ); + } + }; +} + +export default withSafeTimeout; diff --git a/components/index.js b/components/index.js index 96ecef165b8add..c8f6a3d826c38e 100644 --- a/components/index.js +++ b/components/index.js @@ -44,5 +44,6 @@ export { default as withFilters } from './higher-order/with-filters'; export { default as withFocusOutside } from './higher-order/with-focus-outside'; export { default as withFocusReturn } from './higher-order/with-focus-return'; export { default as withInstanceId } from './higher-order/with-instance-id'; +export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; export { default as withState } from './higher-order/with-state'; From cd735cb2fa1fe014f7be2ed2312d279cb212a45f Mon Sep 17 00:00:00 2001 From: mcsf Date: Tue, 6 Feb 2018 16:29:26 +0000 Subject: [PATCH 2/2] RichText: Adopt withSafeTimeout --- blocks/rich-text/index.js | 10 ++++++---- blocks/rich-text/patterns.js | 21 +++------------------ blocks/rich-text/test/index.js | 2 +- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/blocks/rich-text/index.js b/blocks/rich-text/index.js index a1f8a6709c695a..9d523af9512e0e 100644 --- a/blocks/rich-text/index.js +++ b/blocks/rich-text/index.js @@ -22,7 +22,7 @@ import 'element-closest'; */ import { createElement, Component, renderToString } from '@wordpress/element'; import { keycodes, createBlobURL } from '@wordpress/utils'; -import { Slot, Fill } from '@wordpress/components'; +import { withSafeTimeout, Slot, Fill } from '@wordpress/components'; /** * Internal dependencies @@ -72,7 +72,7 @@ export function getFormatProperties( formatName, parents ) { const DEFAULT_FORMATS = [ 'bold', 'italic', 'strikethrough', 'link' ]; -export default class RichText extends Component { +export class RichText extends Component { constructor( props ) { super( ...arguments ); @@ -267,11 +267,11 @@ export default class RichText extends Component { if ( isEmpty && this.props.onReplace ) { // Necessary to allow the paste bin to be removed without errors. - setTimeout( () => this.props.onReplace( content ) ); + this.props.setTimeout( () => this.props.onReplace( content ) ); } else if ( this.props.onSplit ) { // Necessary to get the right range. // Also done in the TinyMCE paste plugin. - setTimeout( () => this.splitContent( content ) ); + this.props.setTimeout( () => this.splitContent( content ) ); } event.preventDefault(); @@ -855,3 +855,5 @@ RichText.defaultProps = { formattingControls: DEFAULT_FORMATS, formatters: [], }; + +export default withSafeTimeout( RichText ); diff --git a/blocks/rich-text/patterns.js b/blocks/rich-text/patterns.js index 0ca2292ef5eb4e..fd20d944527de7 100644 --- a/blocks/rich-text/patterns.js +++ b/blocks/rich-text/patterns.js @@ -14,26 +14,11 @@ import { keycodes } from '@wordpress/utils'; */ import { getBlockTypes } from '../api/registration'; -/** - * Browser dependencies - */ -const { setTimeout } = window; - const { ESCAPE, ENTER, SPACE, BACKSPACE } = keycodes; -/** - * Sets a timeout and checks if the given editor still exists. - * - * @param {Editor} editor TinyMCE editor instance. - * @param {Function} callback The function to call. - */ -function setSafeTimeout( editor, callback ) { - setTimeout( () => ! editor.removed && callback() ); -} - export default function( editor ) { const getContent = this.getContent.bind( this ); - const { onReplace } = this.props; + const { setTimeout, onReplace } = this.props; const VK = tinymce.util.VK; const settings = editor.settings.wptextpattern || {}; @@ -74,9 +59,9 @@ export default function( editor ) { enter(); // Wait for the browser to insert the character. } else if ( keyCode === SPACE ) { - setSafeTimeout( editor, () => searchFirstText( spacePatterns ) ); + setTimeout( () => searchFirstText( spacePatterns ) ); } else if ( keyCode > 47 && ! ( keyCode >= 91 && keyCode <= 93 ) ) { - setSafeTimeout( editor, inline ); + setTimeout( inline ); } }, true ); diff --git a/blocks/rich-text/test/index.js b/blocks/rich-text/test/index.js index cd9bf8a04b04cf..70829ef3994d1e 100644 --- a/blocks/rich-text/test/index.js +++ b/blocks/rich-text/test/index.js @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; /** * Internal dependencies */ -import RichText, { createTinyMCEElement, isLinkBoundary, getFormatProperties } from '../'; +import { RichText, createTinyMCEElement, isLinkBoundary, getFormatProperties } from '../'; import { diffAriaProps, pickAriaProps } from '../aria'; describe( 'createTinyMCEElement', () => {