Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit e1af648

Browse files
author
Piotr Jasiun
authored
Merge pull request #127 from ckeditor/t/126
Feature: Introduced PendingActions plugin. Closes #126.
2 parents 7e7b950 + 7da48ce commit e1af648

File tree

5 files changed

+382
-0
lines changed

5 files changed

+382
-0
lines changed

src/pendingactions.js

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module core/pendingactions
8+
*/
9+
10+
import Plugin from './plugin';
11+
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
12+
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
13+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
14+
15+
/**
16+
* List of editor pending actions.
17+
*
18+
* This plugin should be used to synchronise plugins that execute long-lasting actions
19+
* (i.e. file upload) with the editor integration. It gives a developer, who integrates the editor,
20+
* an easy way to check if there are any pending action whenever such information is needed.
21+
* All plugins, which register pending action provides also a message what action is ongoing
22+
* which can be displayed to a user and let him decide if he wants to interrupt the action or wait.
23+
*
24+
* Adding and updating pending action:
25+
*
26+
* const pendingActions = editor.plugins.get( 'PendingActions' );
27+
* const action = pendingActions.add( 'Upload in progress 0%' );
28+
*
29+
* action.message = 'Upload in progress 10%';
30+
*
31+
* Removing pending action:
32+
*
33+
* const pendingActions = editor.plugins.get( 'PendingActions' );
34+
* const action = pendingActions.add( 'Unsaved changes.' );
35+
*
36+
* pendingActions.remove( action );
37+
*
38+
* Getting pending actions:
39+
*
40+
* const pendingActions = editor.plugins.get( 'PendingActions' );
41+
*
42+
* const action1 = pendingActions.add( 'Action 1' );
43+
* const action2 = pendingActions.add( 'Action 2' );
44+
*
45+
* pendingActions.first // Returns action1
46+
* Array.from( pendingActions ) // Returns [ action1, action2 ]
47+
*
48+
* @extends module:core/plugin~Plugin
49+
*/
50+
export default class PendingActions extends Plugin {
51+
/**
52+
* @inheritDoc
53+
*/
54+
static get pluginName() {
55+
return 'PendingActions';
56+
}
57+
58+
/**
59+
* @inheritDoc
60+
*/
61+
init() {
62+
/**
63+
* Defines whether there is any registered pending action or not.
64+
*
65+
* @readonly
66+
* @observable
67+
* @type {Boolean} #isPending
68+
*/
69+
this.set( 'isPending', false );
70+
71+
/**
72+
* List of pending actions.
73+
*
74+
* @private
75+
* @type {module:utils/collection~Collection}
76+
*/
77+
this._actions = new Collection( { idProperty: '_id' } );
78+
this._actions.delegate( 'add', 'remove' ).to( this );
79+
}
80+
81+
/**
82+
* Adds action to the list of pending actions.
83+
*
84+
* This method returns an action object with observable message property.
85+
* The action object can be later used in the remove method. It also allows you to change the message.
86+
*
87+
* @param {String} message Action message.
88+
* @returns {Object} Observable object that represents a pending action.
89+
*/
90+
add( message ) {
91+
if ( typeof message !== 'string' ) {
92+
/**
93+
* Message has to be a string.
94+
*
95+
* @error pendingactions-add-invalid-message
96+
*/
97+
throw new CKEditorError( 'pendingactions-add-invalid-message: Message has to be a string.' );
98+
}
99+
100+
const action = Object.create( ObservableMixin );
101+
102+
action.set( 'message', message );
103+
this._actions.add( action );
104+
this.isPending = true;
105+
106+
return action;
107+
}
108+
109+
/**
110+
* Removes action from the list of pending actions.
111+
*
112+
* @param {Object} action Action object.
113+
*/
114+
remove( action ) {
115+
this._actions.remove( action );
116+
this.isPending = !!this._actions.length;
117+
}
118+
119+
/**
120+
* Returns first action from the list.
121+
*
122+
* returns {Object} Pending action object.
123+
*/
124+
get first() {
125+
return this._actions.get( 0 );
126+
}
127+
128+
/**
129+
* Iterable interface.
130+
*
131+
* @returns {Iterable.<*>}
132+
*/
133+
[ Symbol.iterator ]() {
134+
return this._actions[ Symbol.iterator ]();
135+
}
136+
137+
/**
138+
* Fired when an action is added to the list.
139+
*
140+
* @event add
141+
* @param {Object} action The added action.
142+
*/
143+
144+
/**
145+
* Fired when an action is removed from the list.
146+
*
147+
* @event remove
148+
* @param {Object} action The removed action.
149+
*/
150+
}

tests/manual/pendingactions.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div>
2+
<button id="add-action">Add pending action</button>
3+
</div>
4+
5+
<h3>Pending actions list:</h3>
6+
<ol class="pending-actions"></ol>
7+
8+
<div id="editor">
9+
<h2>Heading 1</h2>
10+
<p>Paragraph</p>
11+
</div>

tests/manual/pendingactions.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* globals console, window, document, setTimeout */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
9+
10+
import ArticlePluginSet from '../_utils/articlepluginset';
11+
import PendingActions from '../../src/pendingactions';
12+
13+
ClassicEditor
14+
.create( document.querySelector( '#editor' ), {
15+
plugins: [ ArticlePluginSet, PendingActions ],
16+
toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ],
17+
image: {
18+
toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ],
19+
}
20+
} )
21+
.then( editor => {
22+
window.editor = editor;
23+
24+
const pendingActions = editor.plugins.get( PendingActions );
25+
const actionsEl = document.querySelector( '.pending-actions' );
26+
27+
document.querySelector( '#add-action' ).addEventListener( 'click', () => {
28+
const action = pendingActions.add( 'Pending action 0%.' );
29+
30+
wait( 1000 )
31+
.then( () => ( action.message = 'Pending action 0%.' ) )
32+
.then( () => wait( 500 ) )
33+
.then( () => ( action.message = 'Pending action 20%.' ) )
34+
.then( () => wait( 500 ) )
35+
.then( () => ( action.message = 'Pending action 40%.' ) )
36+
.then( () => wait( 500 ) )
37+
.then( () => ( action.message = 'Pending action 60%.' ) )
38+
.then( () => wait( 500 ) )
39+
.then( () => ( action.message = 'Pending action 80%.' ) )
40+
.then( () => wait( 500 ) )
41+
.then( () => ( action.message = 'Pending action 100%.' ) )
42+
.then( () => wait( 500 ) )
43+
.then( () => pendingActions.remove( action ) );
44+
} );
45+
46+
window.addEventListener( 'beforeunload', evt => {
47+
if ( pendingActions.isPending ) {
48+
evt.returnValue = pendingActions.first.message;
49+
}
50+
} );
51+
52+
pendingActions.on( 'add', () => displayActions() );
53+
pendingActions.on( 'remove', () => displayActions() );
54+
55+
function displayActions() {
56+
const frag = document.createDocumentFragment();
57+
58+
for ( const action of pendingActions ) {
59+
const item = document.createElement( 'li' );
60+
61+
item.textContent = action.message;
62+
63+
action.on( 'change:message', () => {
64+
item.textContent = action.message;
65+
} );
66+
67+
frag.appendChild( item );
68+
}
69+
70+
actionsEl.innerHTML = '';
71+
actionsEl.appendChild( frag );
72+
}
73+
74+
function wait( ms ) {
75+
return new Promise( resolve => setTimeout( resolve, ms ) );
76+
}
77+
} )
78+
.catch( err => {
79+
console.error( err.stack );
80+
} );

tests/manual/pendingactions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## Pending actions
2+
3+
1. Click `Add pending actions`
4+
2. Check if action message is changing (action should disappear when progress reaches 100%)
5+
3. Try to reload page or close the browser tab when pending action is displayed, you should see a prompt
6+
4. Try to reload page when there are no pending actions, you shouldn't see a prompt

tests/pendingactions.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
import VirtaulTestEditor from './_utils/virtualtesteditor';
7+
import PendingActions from '../src/pendingactions';
8+
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
9+
10+
let editor, pendingActions;
11+
12+
beforeEach( () => {
13+
return VirtaulTestEditor.create( {
14+
plugins: [ PendingActions ],
15+
} ).then( newEditor => {
16+
editor = newEditor;
17+
pendingActions = editor.plugins.get( PendingActions );
18+
} );
19+
} );
20+
21+
afterEach( () => {
22+
return editor.destroy();
23+
} );
24+
25+
describe( 'PendingActions', () => {
26+
it( 'should define static pluginName property', () => {
27+
expect( PendingActions ).to.have.property( 'pluginName', 'PendingActions' );
28+
} );
29+
30+
describe( 'init()', () => {
31+
it( 'should have isPending observable', () => {
32+
const spy = sinon.spy();
33+
34+
pendingActions.on( 'change:isPending', spy );
35+
36+
expect( pendingActions ).to.have.property( 'isPending', false );
37+
38+
pendingActions.isPending = true;
39+
40+
sinon.assert.calledOnce( spy );
41+
} );
42+
} );
43+
44+
describe( 'add()', () => {
45+
it( 'should register and return pending action', () => {
46+
const action = pendingActions.add( 'Action' );
47+
48+
expect( action ).be.an( 'object' );
49+
expect( action.message ).to.equal( 'Action' );
50+
} );
51+
52+
it( 'should return observable', () => {
53+
const spy = sinon.spy();
54+
const action = pendingActions.add( 'Action' );
55+
56+
action.on( 'change:message', spy );
57+
58+
action.message = 'New message';
59+
60+
sinon.assert.calledOnce( spy );
61+
} );
62+
63+
it( 'should update isPending observable', () => {
64+
expect( pendingActions ).to.have.property( 'isPending', false );
65+
66+
pendingActions.add( 'Action' );
67+
68+
expect( pendingActions ).to.have.property( 'isPending', true );
69+
} );
70+
71+
it( 'should throw an error when invalid message is given', () => {
72+
expect( () => {
73+
pendingActions.add( {} );
74+
} ).to.throw( CKEditorError, /^pendingactions-add-invalid-message/ );
75+
} );
76+
77+
it( 'should fire add event with added item', () => {
78+
const spy = sinon.spy();
79+
80+
pendingActions.on( 'add', spy );
81+
82+
const action = pendingActions.add( 'Some action' );
83+
84+
sinon.assert.calledWith( spy, sinon.match.any, action );
85+
} );
86+
} );
87+
88+
describe( 'remove()', () => {
89+
it( 'should remove given pending action and update observable', () => {
90+
const action1 = pendingActions.add( 'Action 1' );
91+
const action2 = pendingActions.add( 'Action 2' );
92+
93+
expect( pendingActions ).to.have.property( 'isPending', true );
94+
95+
pendingActions.remove( action1 );
96+
97+
expect( pendingActions ).to.have.property( 'isPending', true );
98+
99+
pendingActions.remove( action2 );
100+
101+
expect( pendingActions ).to.have.property( 'isPending', false );
102+
} );
103+
104+
it( 'should fire remove event with removed item', () => {
105+
const spy = sinon.spy();
106+
107+
pendingActions.on( 'remove', spy );
108+
109+
const action = pendingActions.add( 'Some action' );
110+
111+
pendingActions.remove( action );
112+
113+
sinon.assert.calledWith( spy, sinon.match.any, action );
114+
} );
115+
} );
116+
117+
describe( 'first', () => {
118+
it( 'should return first pending action from the list', () => {
119+
const action = pendingActions.add( 'Action 1' );
120+
121+
pendingActions.add( 'Action 2' );
122+
123+
expect( pendingActions.first ).to.equal( action );
124+
} );
125+
} );
126+
127+
describe( 'iterator', () => {
128+
it( 'should return all panding actions', () => {
129+
pendingActions.add( 'Action 1' );
130+
pendingActions.add( 'Action 2' );
131+
132+
expect( Array.from( pendingActions, action => action.message ) ).to.have.members( [ 'Action 1', 'Action 2' ] );
133+
} );
134+
} );
135+
} );

0 commit comments

Comments
 (0)