Ver código fonte

Merge pull request #127 from ckeditor/t/126

Feature: Introduced PendingActions plugin. Closes #126.
Piotr Jasiun 7 anos atrás
pai
commit
62d1e3d159

+ 150 - 0
packages/ckeditor5-core/src/pendingactions.js

@@ -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.
+	 */
+}

+ 11 - 0
packages/ckeditor5-core/tests/manual/pendingactions.html

@@ -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>

+ 80 - 0
packages/ckeditor5-core/tests/manual/pendingactions.js

@@ -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 );
+	} );

+ 6 - 0
packages/ckeditor5-core/tests/manual/pendingactions.md

@@ -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

+ 135 - 0
packages/ckeditor5-core/tests/pendingactions.js

@@ -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' ] );
+		} );
+	} );
+} );