/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @fileOverview The "Notification Aggregator" plugin. * */ ( function() { 'use strict'; CKEDITOR.plugins.add( 'notificationaggregator', { requires: 'notification' } ); /** * An aggregator of multiple tasks (progresses) which should be displayed using one * {@link CKEDITOR.plugins.notification notification}. * * Once all the tasks are done, it means that the whole process is finished and the {@link #finished} * event will be fired. * * New tasks can be created after the previous set of tasks is finished. This will continue the process and * fire the {@link #finished} event again when it is done. * * Simple usage example: * * // Declare one aggregator that will be used for all tasks. * var aggregator; * * // ... * * // Create a new aggregator if the previous one finished all tasks. * if ( !aggregator || aggregator.isFinished() ) { * // Create a new notification aggregator instance. * aggregator = new CKEDITOR.plugins.notificationAggregator( editor, 'Loading process, step {current} of {max}...' ); * * // Change the notification type to 'success' on finish. * aggregator.on( 'finished', function() { * aggregator.notification.update( { message: 'Done', type: 'success' } ); * } ); * } * * // Create 3 tasks. * var taskA = aggregator.createTask(), * taskB = aggregator.createTask(), * taskC = aggregator.createTask(); * * // At this point the notification contains a message: "Loading process, step 0 of 3...". * * // Let's close the first one immediately. * taskA.done(); // "Loading process, step 1 of 3...". * * // One second later the message will be: "Loading process, step 2 of 3...". * setTimeout( function() { * taskB.done(); * }, 1000 ); * * // Two seconds after the previous message the last task will be completed, meaning that * // the notification will be closed. * setTimeout( function() { * taskC.done(); * }, 3000 ); * * @since 4.5 * @class CKEDITOR.plugins.notificationAggregator * @mixins CKEDITOR.event * @constructor Creates a notification aggregator instance. * @param {CKEDITOR.editor} editor * @param {String} message The template for the message to be displayed in the notification. The template can use * the following variables: * * * `{current}` – The number of completed tasks. * * `{max}` – The number of tasks. * * `{percentage}` – The progress (number 0-100). * * @param {String/null} [singularMessage=null] An optional template for the message to be displayed in the notification * when there is only one task remaining. This template can use the same variables as the `message` template. * If `null`, then the `message` template will be used. */ function Aggregator( editor, message, singularMessage ) { /** * @readonly * @property {CKEDITOR.editor} editor */ this.editor = editor; /** * Notification created by the aggregator. * * The notification object is modified as aggregator tasks are being closed. * * @readonly * @property {CKEDITOR.plugins.notification/null} */ this.notification = null; /** * A template for the notification message. * * The template can use the following variables: * * * `{current}` – The number of completed tasks. * * `{max}` – The number of tasks. * * `{percentage}` – The progress (number 0-100). * * @private * @property {CKEDITOR.template} */ this._message = new CKEDITOR.template( message ); /** * A template for the notification message used when only one task is loading. * * Sometimes there might be a need to specify a special message when there * is only one task loading, and to display standard messages in other cases. * * For example, you might want to show a message "Translating a widget" rather than * "Translating widgets (1 of 1)", but still you would want to have a message * "Translating widgets (2 of 3)" if more widgets are being translated at the same * time. * * Template variables are the same as in {@link #_message}. * * @private * @property {CKEDITOR.template/null} */ this._singularMessage = singularMessage ? new CKEDITOR.template( singularMessage ) : null; // Set the _tasks, _totalWeights, _doneWeights, _doneTasks properties. this._tasks = []; this._totalWeights = 0; this._doneWeights = 0; this._doneTasks = 0; /** * Array of tasks tracked by the aggregator. * * @private * @property {CKEDITOR.plugins.notificationAggregator.task[]} _tasks */ /** * Stores the sum of declared weights for all contained tasks. * * @private * @property {Number} _totalWeights */ /** * Stores the sum of done weights for all contained tasks. * * @private * @property {Number} _doneWeights */ /** * Stores the count of tasks done. * * @private * @property {Number} _doneTasks */ } Aggregator.prototype = { /** * Creates a new task that can be updated to indicate the progress. * * @param [options] Options object for the task creation. * @param [options.weight] For more information about weight, see the * {@link CKEDITOR.plugins.notificationAggregator.task} overview. * @returns {CKEDITOR.plugins.notificationAggregator.task} An object that represents the task state, and allows * for its manipulation. */ createTask: function( options ) { options = options || {}; var initialTask = !this.notification, task; if ( initialTask ) { // It's a first call. this.notification = this._createNotification(); } task = this._addTask( options ); task.on( 'updated', this._onTaskUpdate, this ); task.on( 'done', this._onTaskDone, this ); task.on( 'canceled', function() { this._removeTask( task ); }, this ); // Update the aggregator. this.update(); if ( initialTask ) { this.notification.show(); } return task; }, /** * Triggers an update on the aggregator, meaning that its UI will be refreshed. * * When all the tasks are done, the {@link #finished} event is fired. */ update: function() { this._updateNotification(); if ( this.isFinished() ) { this.fire( 'finished' ); } }, /** * Returns a number from `0` to `1` representing the done weights to total weights ratio * (showing how many of the tasks are done). * * Note: For an empty aggregator (without any tasks created) it will return `1`. * * @returns {Number} Returns the percentage of tasks done as a number ranging from `0` to `1`. */ getPercentage: function() { // In case there are no weights at all we'll return 1. if ( this.getTaskCount() === 0 ) { return 1; } return this._doneWeights / this._totalWeights; }, /** * @returns {Boolean} Returns `true` if all notification tasks are done * (or there are no tasks at all). */ isFinished: function() { return this.getDoneTaskCount() === this.getTaskCount(); }, /** * @returns {Number} Returns a total tasks count. */ getTaskCount: function() { return this._tasks.length; }, /** * @returns {Number} Returns the number of tasks done. */ getDoneTaskCount: function() { return this._doneTasks; }, /** * Updates the notification content. * * @private */ _updateNotification: function() { this.notification.update( { message: this._getNotificationMessage(), progress: this.getPercentage() } ); }, /** * Returns a message used in the notification. * * @private * @returns {String} */ _getNotificationMessage: function() { var tasksCount = this.getTaskCount(), doneTasks = this.getDoneTaskCount(), templateParams = { current: doneTasks, max: tasksCount, percentage: Math.round( this.getPercentage() * 100 ) }, template; // If there's only one remaining task and we have a singular message, we should use it. if ( tasksCount == 1 && this._singularMessage ) { template = this._singularMessage; } else { template = this._message; } return template.output( templateParams ); }, /** * Creates a notification object. * * @private * @returns {CKEDITOR.plugins.notification} */ _createNotification: function() { return new CKEDITOR.plugins.notification( this.editor, { type: 'progress' } ); }, /** * Creates a {@link CKEDITOR.plugins.notificationAggregator.task} instance based * on `options`, and adds it to the task list. * * @private * @param options Options object coming from the {@link #createTask} method. * @returns {CKEDITOR.plugins.notificationAggregator.task} */ _addTask: function( options ) { var task = new Task( options.weight ); this._tasks.push( task ); this._totalWeights += task._weight; return task; }, /** * Removes a given task from the {@link #_tasks} array and updates the UI. * * @private * @param {CKEDITOR.plugins.notificationAggregator.task} task Task to be removed. */ _removeTask: function( task ) { var index = CKEDITOR.tools.indexOf( this._tasks, task ); if ( index !== -1 ) { // If task was already updated with some weight, we need to remove // this weight from our cache. if ( task._doneWeight ) { this._doneWeights -= task._doneWeight; } this._totalWeights -= task._weight; this._tasks.splice( index, 1 ); // And we also should inform the UI about this change. this.update(); } }, /** * A listener called on the {@link CKEDITOR.plugins.notificationAggregator.task#update} event. * * @private * @param {CKEDITOR.eventInfo} evt Event object of the {@link CKEDITOR.plugins.notificationAggregator.task#update} event. */ _onTaskUpdate: function( evt ) { this._doneWeights += evt.data; this.update(); }, /** * A listener called on the {@link CKEDITOR.plugins.notificationAggregator.task#event-done} event. * * @private * @param {CKEDITOR.eventInfo} evt Event object of the {@link CKEDITOR.plugins.notificationAggregator.task#event-done} event. */ _onTaskDone: function() { this._doneTasks += 1; this.update(); } }; CKEDITOR.event.implementOn( Aggregator.prototype ); /** * # Overview * * This type represents a single task in the aggregator, and exposes methods to manipulate its state. * * ## Weights * * Task progess is based on its **weight**. * * As you create a task, you need to declare its weight. As you want the update to inform about the * progress, you will need to {@link #update} the task, telling how much of this weight is done. * * For example, if you declare that your task has a weight that equals `50` and then call `update` with `10`, * you will end up with telling that the task is done in 20%. * * ### Example Usage of Weights * * Let us say that you use tasks for file uploading. * * A single task is associated with a single file upload. You can use the file size in bytes as a weight, * and then as the file upload progresses you just call the `update` method with the number of bytes actually * downloaded. * * @since 4.5 * @class CKEDITOR.plugins.notificationAggregator.task * @mixins CKEDITOR.event * @constructor Creates a task instance for notification aggregator. * @param {Number} [weight=1] */ function Task( weight ) { /** * Total weight of the task. * * @private * @property {Number} */ this._weight = weight || 1; /** * Done weight of the task. * * @private * @property {Number} */ this._doneWeight = 0; /** * Indicates when the task is canceled. * * @private * @property {Boolean} */ this._isCanceled = false; } Task.prototype = { /** * Marks the task as done. */ done: function() { this.update( this._weight ); }, /** * Updates the done weight of a task. * * @param {Number} weight Number indicating how much of the total task {@link #_weight} is done. */ update: function( weight ) { // If task is already done or canceled there is no need to update it, and we don't expect // progress to be reversed. if ( this.isDone() || this.isCanceled() ) { return; } // Note that newWeight can't be higher than _doneWeight. var newWeight = Math.min( this._weight, weight ), weightChange = newWeight - this._doneWeight; this._doneWeight = newWeight; // Fire updated event even if task is done in order to correctly trigger updating the // notification's message. If we wouldn't do this, then the last weight change would be ignored. this.fire( 'updated', weightChange ); if ( this.isDone() ) { this.fire( 'done' ); } }, /** * Cancels the task (the task will be removed from the aggregator). */ cancel: function() { // If task is already done or canceled. if ( this.isDone() || this.isCanceled() ) { return; } // Mark task as canceled. this._isCanceled = true; // We'll fire cancel event it's up to aggregator to listen for this event, // and remove the task. this.fire( 'canceled' ); }, /** * Checks if the task is done. * * @returns {Boolean} */ isDone: function() { return this._weight === this._doneWeight; }, /** * Checks if the task is canceled. * * @returns {Boolean} */ isCanceled: function() { return this._isCanceled; } }; CKEDITOR.event.implementOn( Task.prototype ); /** * Fired when all tasks are done. When this event occurs, the notification may change its type to `success` or be hidden: * * aggregator.on( 'finished', function() { * if ( aggregator.getTaskCount() == 0 ) { * aggregator.notification.hide(); * } else { * aggregator.notification.update( { message: 'Done', type: 'success' } ); * } * } ); * * @event finished * @member CKEDITOR.plugins.notificationAggregator */ /** * Fired upon each weight update of the task. * * var myTask = new Task( 100 ); * myTask.update( 30 ); * // Fires updated event with evt.data = 30. * myTask.update( 40 ); * // Fires updated event with evt.data = 10. * myTask.update( 20 ); * // Fires updated event with evt.data = -20. * * @event updated * @param {Number} data The difference between the new weight and the previous one. * @member CKEDITOR.plugins.notificationAggregator.task */ /** * Fired when the task is done. * * @event done * @member CKEDITOR.plugins.notificationAggregator.task */ /** * Fired when the task is canceled. * * @event canceled * @member CKEDITOR.plugins.notificationAggregator.task */ // Expose Aggregator type. CKEDITOR.plugins.notificationAggregator = Aggregator; CKEDITOR.plugins.notificationAggregator.task = Task; } )();