Skip to content

Commit

Permalink
Merge pull request #137 from ckeditor/i/136
Browse files Browse the repository at this point in the history
Feature: The "Log editor data" button should copy to the clipboard if clicked with the Shift key. Closes #136.
  • Loading branch information
oleq authored Feb 3, 2022
2 parents d81e841 + 4938382 commit 1252b78
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 9 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"babel-loader": "^8.2.3",
"chai": "^4.3.4",
"chai-enzyme": "^1.0.0-beta.1",
"copy-to-clipboard": "^3.3.1",
"css-loader": "^2.1.1",
"cssnano": "^4.1.11",
"enzyme": "^3.11.0",
Expand Down
1 change: 1 addition & 0 deletions src/assets/img/checkmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/img/copy-to-clipboard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions src/editorquickactions.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,37 @@

& > .ck-inspector-button {
margin-left: .3em;

/* The checkmark icon shows up when the content was copied to clipboard */
&.ck-inspector-button_data-copied {
animation-duration: .5s;
animation-name: ck-inspector-bounce-in;
color: green;
}
}
}

@keyframes ck-inspector-bounce-in {
0% {
opacity: 0;
transform: scale3d(.5, .5, .5);
}

20% {
transform: scale3d(1.1, 1.1, 1.1);
}

40% {
transform: scale3d(.8, .8, .8);
}

60% {
opacity: 1;
transform: scale3d(1.05, 1.05, 1.05);
}

to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
93 changes: 86 additions & 7 deletions src/editorquickactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,46 @@
* For licensing, see LICENSE.md.
*/

/* global document */

import React, { Component } from 'react';
import { connect } from 'react-redux';
import copy from 'copy-to-clipboard';

import Button from './components/button';

import SourceIcon from './assets/img/source.svg';
import ConsoleIcon from './assets/img/console.svg';
import ReadOnlyIcon from './assets/img/read-only.svg';
import TrashIcon from './assets/img/trash.svg';
import CopyToClipboardIcon from './assets/img/copy-to-clipboard.svg';
import CheckmarkIcon from './assets/img/checkmark.svg';

import './editorquickactions.css';

class EditorQuickActions extends Component {
constructor( props ) {
super( props );

this.state = {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
};

this._keyDownHandler = this._handleKeyDown.bind( this );
this._keyUpHandler = this._handleKeyUp.bind( this );
this._editorDataJustCopiedTimeout = null;
}

render() {
return <div className="ck-inspector-editor-quick-actions">
return <div className='ck-inspector-editor-quick-actions'>
<Button
text="Log editor"
icon={<ConsoleIcon />}
isEnabled={!!this.props.editor}
onClick={() => console.log( this.props.editor )}
/>
<Button
text="Log editor data"
icon={<SourceIcon />}
isEnabled={!!this.props.editor}
onClick={() => console.log( this.props.editor.getData() )}
/>
{ this._getLogButton() }
<Button
text="Toggle read only"
icon={<ReadOnlyIcon />}
Expand All @@ -45,6 +58,72 @@ class EditorQuickActions extends Component {
/>
</div>;
}

componentDidMount() {
document.addEventListener( 'keydown', this._keyDownHandler );
document.addEventListener( 'keyup', this._keyUpHandler );
}

componentWillUnmount() {
// Stop reacting to Shift key press/release after the inspector was destroyed.
document.removeEventListener( 'keydown', this._keyDownHandler );
document.removeEventListener( 'keyup', this._keyUpHandler );

// Don't update the button look after the inspector was destroyed.
clearTimeout( this._editorDataJustCopiedTimeout );
}

_getLogButton() {
let icon, text;

if ( this.state.wasEditorDataJustCopied ) {
icon = <CheckmarkIcon />;
text = 'Data copied to clipboard.';
} else {
icon = this.state.isShiftKeyPressed ? <CopyToClipboardIcon /> : <SourceIcon />;
text = 'Log editor data (press with Shift to copy)';
}

return <Button
text={text}
icon={icon}
className={this.state.wasEditorDataJustCopied ? 'ck-inspector-button_data-copied' : ''}
isEnabled={!!this.props.editor}
onClick={this._handleLogEditorDataClick.bind( this )}
/>;
}

_handleLogEditorDataClick( { shiftKey } ) {
if ( shiftKey ) {
copy( this.props.editor.getData() );

this.setState( {
wasEditorDataJustCopied: true
} );

clearTimeout( this._editorDataJustCopiedTimeout );

this._editorDataJustCopiedTimeout = setTimeout( () => {
this.setState( {
wasEditorDataJustCopied: false
} );
}, 3000 );
} else {
console.log( this.props.editor.getData() );
}
}

_handleKeyDown( { key } ) {
this.setState( {
isShiftKeyPressed: key === 'Shift'
} );
}

_handleKeyUp() {
this.setState( {
isShiftKeyPressed: false
} );
}
}

const mapStateToProps = ( { editors, currentEditorName, currentEditorGlobals: { isReadOnly } } ) => {
Expand Down
176 changes: 174 additions & 2 deletions tests/inspector/editorquickactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* For licensing, see LICENSE.md.
*/

/* global document, window */
/* global document, window, KeyboardEvent */

import React from 'react';
import TestEditor from '../utils/testeditor';
Expand All @@ -12,6 +12,10 @@ import { Provider } from 'react-redux';

import EditorQuickActions from '../../src/editorquickactions';

import SourceIcon from '../../src/assets/img/source.svg';
import CopyToClipboardIcon from '../../src/assets/img/copy-to-clipboard.svg';
import CheckmarkIcon from '../../src/assets/img/checkmark.svg';

describe( '<EditorQuickActions />', () => {
let editor, store, wrapper, element;

Expand Down Expand Up @@ -50,7 +54,10 @@ describe( '<EditorQuickActions />', () => {
} );

afterEach( () => {
wrapper.unmount();
if ( wrapper.children().length ) {
wrapper.unmount();
}

element.remove();

return editor.destroy();
Expand Down Expand Up @@ -87,6 +94,171 @@ describe( '<EditorQuickActions />', () => {

logSpy.restore();
} );

it( 'should react to the Shift being pressed and turn into "copy to clipboard" button', () => {
const quickActions = wrapper.find( 'EditorQuickActions' );
let logButton = wrapper.find( 'Button' ).at( 1 );

expect( logButton.props().icon.type ).to.equal( SourceIcon );
expect( logButton.props().text ).to.equal( 'Log editor data (press with Shift to copy)' );

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
} );

document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Shift' } ) );
wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: false
} );

logButton = wrapper.find( 'Button' ).at( 1 );
expect( logButton.props().icon.type ).to.equal( CopyToClipboardIcon );
expect( logButton.props().text ).to.equal( 'Log editor data (press with Shift to copy)' );
} );

// Note: Due to limitations of the copy-to-clipboard library, this test is unable to check if the
// actual editor data is copied to clipboard :( It falls back to window.prompt().
it( 'should copy the content of the editor to the clipboard if clicked with Shift key', done => {
const clock = sinon.useFakeTimers();
const promptStub = sinon.stub( window, 'prompt' ).returns( '<p>foo</p>' );
const quickActions = wrapper.find( 'EditorQuickActions' );
let logButton = wrapper.find( 'Button' ).at( 1 );

expect( logButton.props().icon.type ).to.equal( SourceIcon );
expect( logButton.props().text ).to.equal( 'Log editor data (press with Shift to copy)' );

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
} );

// Press the Shift key. The buttons should start copying to clipboard instead of logging.
document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Shift' } ) );
logButton.simulate( 'click', { shiftKey: true } );

wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: true
} );

logButton = wrapper.find( 'Button' ).at( 1 );
expect( logButton.props().icon.type ).to.equal( CheckmarkIcon );
expect( logButton.props().text ).to.equal( 'Data copied to clipboard.' );

// Make sure the checkmark icon + text stay for 3000ms.
clock.tick( 2500 );
wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: true
} );

logButton = wrapper.find( 'Button' ).at( 1 );
expect( logButton.props().icon.type ).to.equal( CheckmarkIcon );
expect( logButton.props().text ).to.equal( 'Data copied to clipboard.' );

// Wait for the checkmark icon + text to disappear.
clock.tick( 1000 );
wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: false
} );

logButton = wrapper.find( 'Button' ).at( 1 );
expect( logButton.props().icon.type ).to.equal( CopyToClipboardIcon );
expect( logButton.props().text ).to.equal( 'Log editor data (press with Shift to copy)' );

// Release the Shift key.
document.dispatchEvent( new KeyboardEvent( 'keyup' ) );
wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
} );

promptStub.restore();
clock.restore();

done();
} );

// This one tests clearTimeout() that changes the icon + button text in componentWillUnmount().
it( 'should not throw if the inspector was destroyed immediatelly after the editor data was copied', done => {
const clock = sinon.useFakeTimers();
const promptStub = sinon.stub( window, 'prompt' ).returns( '<p>foo</p>' );
const errorSpy = sinon.stub( console, 'error' );
const quickActions = wrapper.find( 'EditorQuickActions' );
const logButton = wrapper.find( 'Button' ).at( 1 );

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
} );

// Press the Shift key. The button should start copying to clipboard instead of logging.
document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Shift' } ) );
logButton.simulate( 'click', { shiftKey: true } );

wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: true
} );

wrapper.unmount();
clock.tick( 5000 );

sinon.assert.notCalled( errorSpy );

promptStub.restore();
errorSpy.restore();
clock.restore();
done();
} );

// This one tests document.removeEventListener in componentWillUnmount().
it( 'should not throw if Shift key was pressed or released after the inspector was destroyed', done => {
const clock = sinon.useFakeTimers();
const errorSpy = sinon.stub( console, 'error' );
const quickActions = wrapper.find( 'EditorQuickActions' );

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: false,
wasEditorDataJustCopied: false
} );

// Press the Shift key. The buttons should start copying to clipboard instead of logging.
document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Shift' } ) );
wrapper.update();

expect( quickActions.state() ).to.deep.equal( {
isShiftKeyPressed: true,
wasEditorDataJustCopied: false
} );

wrapper.unmount();
clock.tick( 5000 );

document.dispatchEvent( new KeyboardEvent( 'keyup' ) );
document.dispatchEvent( new KeyboardEvent( 'keydown', { key: 'Shift' } ) );

sinon.assert.notCalled( errorSpy );

errorSpy.restore();
clock.restore();
done();
} );
} );

describe( 'toggle read only button', () => {
Expand Down
Loading

0 comments on commit 1252b78

Please sign in to comment.