This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #127 from ckeditor/t/126
Feature: Introduced PendingActions plugin. Closes #126.
- Loading branch information
Showing
5 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/** | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/** | ||
* @module core/pendingactions | ||
*/ | ||
|
||
import Plugin from './plugin'; | ||
import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; | ||
import Collection from '@ckeditor/ckeditor5-utils/src/collection'; | ||
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; | ||
|
||
/** | ||
* List of editor pending actions. | ||
* | ||
* This plugin should be used to synchronise plugins that execute long-lasting actions | ||
* (i.e. file upload) with the editor integration. It gives a developer, who integrates the editor, | ||
* an easy way to check if there are any pending action whenever such information is needed. | ||
* All plugins, which register pending action provides also a message what action is ongoing | ||
* which can be displayed to a user and let him decide if he wants to interrupt the action or wait. | ||
* | ||
* Adding and updating pending action: | ||
* | ||
* const pendingActions = editor.plugins.get( 'PendingActions' ); | ||
* const action = pendingActions.add( 'Upload in progress 0%' ); | ||
* | ||
* action.message = 'Upload in progress 10%'; | ||
* | ||
* Removing pending action: | ||
* | ||
* const pendingActions = editor.plugins.get( 'PendingActions' ); | ||
* const action = pendingActions.add( 'Unsaved changes.' ); | ||
* | ||
* pendingActions.remove( action ); | ||
* | ||
* Getting pending actions: | ||
* | ||
* const pendingActions = editor.plugins.get( 'PendingActions' ); | ||
* | ||
* const action1 = pendingActions.add( 'Action 1' ); | ||
* const action2 = pendingActions.add( 'Action 2' ); | ||
* | ||
* pendingActions.first // Returns action1 | ||
* Array.from( pendingActions ) // Returns [ action1, action2 ] | ||
* | ||
* @extends module:core/plugin~Plugin | ||
*/ | ||
export default class PendingActions extends Plugin { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get pluginName() { | ||
return 'PendingActions'; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
/** | ||
* Defines whether there is any registered pending action or not. | ||
* | ||
* @readonly | ||
* @observable | ||
* @type {Boolean} #isPending | ||
*/ | ||
this.set( 'isPending', false ); | ||
|
||
/** | ||
* List of pending actions. | ||
* | ||
* @private | ||
* @type {module:utils/collection~Collection} | ||
*/ | ||
this._actions = new Collection( { idProperty: '_id' } ); | ||
this._actions.delegate( 'add', 'remove' ).to( this ); | ||
} | ||
|
||
/** | ||
* Adds action to the list of pending actions. | ||
* | ||
* This method returns an action object with observable message property. | ||
* The action object can be later used in the remove method. It also allows you to change the message. | ||
* | ||
* @param {String} message Action message. | ||
* @returns {Object} Observable object that represents a pending action. | ||
*/ | ||
add( message ) { | ||
if ( typeof message !== 'string' ) { | ||
/** | ||
* Message has to be a string. | ||
* | ||
* @error pendingactions-add-invalid-message | ||
*/ | ||
throw new CKEditorError( 'pendingactions-add-invalid-message: Message has to be a string.' ); | ||
} | ||
|
||
const action = Object.create( ObservableMixin ); | ||
|
||
action.set( 'message', message ); | ||
this._actions.add( action ); | ||
this.isPending = true; | ||
|
||
return action; | ||
} | ||
|
||
/** | ||
* Removes action from the list of pending actions. | ||
* | ||
* @param {Object} action Action object. | ||
*/ | ||
remove( action ) { | ||
this._actions.remove( action ); | ||
this.isPending = !!this._actions.length; | ||
} | ||
|
||
/** | ||
* Returns first action from the list. | ||
* | ||
* returns {Object} Pending action object. | ||
*/ | ||
get first() { | ||
return this._actions.get( 0 ); | ||
} | ||
|
||
/** | ||
* Iterable interface. | ||
* | ||
* @returns {Iterable.<*>} | ||
*/ | ||
[ Symbol.iterator ]() { | ||
return this._actions[ Symbol.iterator ](); | ||
} | ||
|
||
/** | ||
* Fired when an action is added to the list. | ||
* | ||
* @event add | ||
* @param {Object} action The added action. | ||
*/ | ||
|
||
/** | ||
* Fired when an action is removed from the list. | ||
* | ||
* @event remove | ||
* @param {Object} action The removed action. | ||
*/ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<div> | ||
<button id="add-action">Add pending action</button> | ||
</div> | ||
|
||
<h3>Pending actions list:</h3> | ||
<ol class="pending-actions"></ol> | ||
|
||
<div id="editor"> | ||
<h2>Heading 1</h2> | ||
<p>Paragraph</p> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
/* globals console, window, document, setTimeout */ | ||
|
||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; | ||
|
||
import ArticlePluginSet from '../_utils/articlepluginset'; | ||
import PendingActions from '../../src/pendingactions'; | ||
|
||
ClassicEditor | ||
.create( document.querySelector( '#editor' ), { | ||
plugins: [ ArticlePluginSet, PendingActions ], | ||
toolbar: [ 'heading', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ], | ||
image: { | ||
toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ], | ||
} | ||
} ) | ||
.then( editor => { | ||
window.editor = editor; | ||
|
||
const pendingActions = editor.plugins.get( PendingActions ); | ||
const actionsEl = document.querySelector( '.pending-actions' ); | ||
|
||
document.querySelector( '#add-action' ).addEventListener( 'click', () => { | ||
const action = pendingActions.add( 'Pending action 0%.' ); | ||
|
||
wait( 1000 ) | ||
.then( () => ( action.message = 'Pending action 0%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => ( action.message = 'Pending action 20%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => ( action.message = 'Pending action 40%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => ( action.message = 'Pending action 60%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => ( action.message = 'Pending action 80%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => ( action.message = 'Pending action 100%.' ) ) | ||
.then( () => wait( 500 ) ) | ||
.then( () => pendingActions.remove( action ) ); | ||
} ); | ||
|
||
window.addEventListener( 'beforeunload', evt => { | ||
if ( pendingActions.isPending ) { | ||
evt.returnValue = pendingActions.first.message; | ||
} | ||
} ); | ||
|
||
pendingActions.on( 'add', () => displayActions() ); | ||
pendingActions.on( 'remove', () => displayActions() ); | ||
|
||
function displayActions() { | ||
const frag = document.createDocumentFragment(); | ||
|
||
for ( const action of pendingActions ) { | ||
const item = document.createElement( 'li' ); | ||
|
||
item.textContent = action.message; | ||
|
||
action.on( 'change:message', () => { | ||
item.textContent = action.message; | ||
} ); | ||
|
||
frag.appendChild( item ); | ||
} | ||
|
||
actionsEl.innerHTML = ''; | ||
actionsEl.appendChild( frag ); | ||
} | ||
|
||
function wait( ms ) { | ||
return new Promise( resolve => setTimeout( resolve, ms ) ); | ||
} | ||
} ) | ||
.catch( err => { | ||
console.error( err.stack ); | ||
} ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
## Pending actions | ||
|
||
1. Click `Add pending actions` | ||
2. Check if action message is changing (action should disappear when progress reaches 100%) | ||
3. Try to reload page or close the browser tab when pending action is displayed, you should see a prompt | ||
4. Try to reload page when there are no pending actions, you shouldn't see a prompt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/** | ||
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
import VirtaulTestEditor from './_utils/virtualtesteditor'; | ||
import PendingActions from '../src/pendingactions'; | ||
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; | ||
|
||
let editor, pendingActions; | ||
|
||
beforeEach( () => { | ||
return VirtaulTestEditor.create( { | ||
plugins: [ PendingActions ], | ||
} ).then( newEditor => { | ||
editor = newEditor; | ||
pendingActions = editor.plugins.get( PendingActions ); | ||
} ); | ||
} ); | ||
|
||
afterEach( () => { | ||
return editor.destroy(); | ||
} ); | ||
|
||
describe( 'PendingActions', () => { | ||
it( 'should define static pluginName property', () => { | ||
expect( PendingActions ).to.have.property( 'pluginName', 'PendingActions' ); | ||
} ); | ||
|
||
describe( 'init()', () => { | ||
it( 'should have isPending observable', () => { | ||
const spy = sinon.spy(); | ||
|
||
pendingActions.on( 'change:isPending', spy ); | ||
|
||
expect( pendingActions ).to.have.property( 'isPending', false ); | ||
|
||
pendingActions.isPending = true; | ||
|
||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
} ); | ||
|
||
describe( 'add()', () => { | ||
it( 'should register and return pending action', () => { | ||
const action = pendingActions.add( 'Action' ); | ||
|
||
expect( action ).be.an( 'object' ); | ||
expect( action.message ).to.equal( 'Action' ); | ||
} ); | ||
|
||
it( 'should return observable', () => { | ||
const spy = sinon.spy(); | ||
const action = pendingActions.add( 'Action' ); | ||
|
||
action.on( 'change:message', spy ); | ||
|
||
action.message = 'New message'; | ||
|
||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
|
||
it( 'should update isPending observable', () => { | ||
expect( pendingActions ).to.have.property( 'isPending', false ); | ||
|
||
pendingActions.add( 'Action' ); | ||
|
||
expect( pendingActions ).to.have.property( 'isPending', true ); | ||
} ); | ||
|
||
it( 'should throw an error when invalid message is given', () => { | ||
expect( () => { | ||
pendingActions.add( {} ); | ||
} ).to.throw( CKEditorError, /^pendingactions-add-invalid-message/ ); | ||
} ); | ||
|
||
it( 'should fire add event with added item', () => { | ||
const spy = sinon.spy(); | ||
|
||
pendingActions.on( 'add', spy ); | ||
|
||
const action = pendingActions.add( 'Some action' ); | ||
|
||
sinon.assert.calledWith( spy, sinon.match.any, action ); | ||
} ); | ||
} ); | ||
|
||
describe( 'remove()', () => { | ||
it( 'should remove given pending action and update observable', () => { | ||
const action1 = pendingActions.add( 'Action 1' ); | ||
const action2 = pendingActions.add( 'Action 2' ); | ||
|
||
expect( pendingActions ).to.have.property( 'isPending', true ); | ||
|
||
pendingActions.remove( action1 ); | ||
|
||
expect( pendingActions ).to.have.property( 'isPending', true ); | ||
|
||
pendingActions.remove( action2 ); | ||
|
||
expect( pendingActions ).to.have.property( 'isPending', false ); | ||
} ); | ||
|
||
it( 'should fire remove event with removed item', () => { | ||
const spy = sinon.spy(); | ||
|
||
pendingActions.on( 'remove', spy ); | ||
|
||
const action = pendingActions.add( 'Some action' ); | ||
|
||
pendingActions.remove( action ); | ||
|
||
sinon.assert.calledWith( spy, sinon.match.any, action ); | ||
} ); | ||
} ); | ||
|
||
describe( 'first', () => { | ||
it( 'should return first pending action from the list', () => { | ||
const action = pendingActions.add( 'Action 1' ); | ||
|
||
pendingActions.add( 'Action 2' ); | ||
|
||
expect( pendingActions.first ).to.equal( action ); | ||
} ); | ||
} ); | ||
|
||
describe( 'iterator', () => { | ||
it( 'should return all panding actions', () => { | ||
pendingActions.add( 'Action 1' ); | ||
pendingActions.add( 'Action 2' ); | ||
|
||
expect( Array.from( pendingActions, action => action.message ) ).to.have.members( [ 'Action 1', 'Action 2' ] ); | ||
} ); | ||
} ); | ||
} ); |