MediaWiki:Gadget-progressDialog.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * This file is documented using
 * [jsduck-syntax](https://github.com/senchalabs/jsduck).
 * The source code is available at 
 * [Wikimedia Commons](http://commons.wikimedia.org/wiki/MediaWiki:Gadget-progressDialog.js)
 * ([raw](http://commons.wikimedia.org/wiki?title=MediaWiki:Gadget-progressDialog.js&action=raw&ctype=text/javascript),
 * [css](http://commons.wikimedia.org/wiki?title=MediaWiki:Gadget-progressDialog.css&action=raw&ctype=text/css))
 *
 * @class $progressDialog
 * @singleton
 *
 * Offers an awesome CSS3 progress dialog
 * that blocks users from interaction with the page.
 * {@img progressDialog.png Screenshot of progressDialog}
 *
 * Warning: There should be only one instance. 
 * Do not attempt to clone the dialog.
 *
 *
 * Quick start: Simple usage:
 *
        @example 
        // Load the gadget
        mw.loader.using('ext.gadget.progressDialog', function () {
            mw.libs.$progressDialog.showProgress({
                singleUnitProg: 0,
                singleUnitText: "Details of the current task running",
                totalProg: 0,
                totalText: "The current task. E.g. Replacing globally.",
                title: "My Application"
            });
            setTimeout(function () {
                mw.libs.$progressDialog.showProgress({
                    singleUnitProg: 100,
                    singleUnitText: "Done",
                    totalProg: 50,
                    totalText: "The next task."
                });
            }, 1000);
        });
 *
 *
 * @requires jQuery
 * @requires mediaWiki
 *
 * @author Rainer Rillke <https://commons.wikimedia.org/wiki/User:Rillke>
 **/

/*
 * @license
 *   The MIT License (MIT) [full text see below]
 *   GNU General Public License, version 3 (GPL-3.0)
 *   Creative Commons Attribution 3.0 (CC-BY-3.0)
 * Choose whichever license of these you like best.
 *
 * @jshint valid
 * <http://jshint.com/>
 */
/*jshint curly:false, smarttabs:true*/

/*!
	Copyright (c) 2013 Rainer Rillke <https://commons.wikimedia.org/wiki/User:Rillke>

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
**/
(function($, mw, undefined) {
	'use strict';

	var version = '0.0.2.0',
		cache = {},
		$def = $.Deferred(),
		inCache, $p, isInit, isReady, autoOpen, animationOff, defaults, onPageLeave, onDialogStateChange, orininalUnloadHandler;

	mw.messages.set({
		'prog-on-before-unload': "It looks like there is an ongoing opteration. Are you sure you would like to quit?"
	});

	/**
	 * @cfg defaults
	 *  Default configuration for progressDialog that can be overwritten
	 *  by either supplying the options-argument to ``.showProgress()``
	 *  or by setting ``mw.libs.$progressDialog.config``.
	 *  Since there is only one instance of progressDialog,
	 *  call ``.cleanUp()`` if you suspect it was used by another application
	 *  before, to restore the defaults.
	 * @cfg {String|string} [defaults.app='Progress']
	 *  Name of the application using progressDialog.
	 *  Displayed as the dialog title.
	 * @cfg {boolean} [defaults.warnOnPageLeave=true]
	 *  Ask user for confirmation when quitting the current page
	 *  while the progress dialog is shown.
	 */
	/**
	 * @property {Object} defaults
	 *  Confer to ``defaults`` configuration.
	 * @private
	 */
	defaults = {
		app: 'Progress',
		warnOnPageLeave: true
	};

	inCache = function(k, v) {
		if (v === cache[k]) return true;
		cache[k] = v;
	};

	onPageLeave = function() {
		return mw.msg('prog-on-before-unload');
	};

	onDialogStateChange = function() {
		if ($p.dialog('isOpen') === true && $p.config.warnOnPageLeave) {
			if (window.onbeforeunload !== onPageLeave) {
				orininalUnloadHandler = window.onbeforeunload;
				window.onbeforeunload = onPageLeave;
			}
		} else {
			if (window.onbeforeunload === onPageLeave) {
				window.onbeforeunload = orininalUnloadHandler;
			}
		}
	};

	$p = mw.libs.$progressDialog = $.extend($('<div id="prog-dlg"></div>'), {
		version: version,
		config: {},
		/**
		 * Restore defaults.
		 * 
		 * @chainable
		 */
		cleanUp: function() {
			$p.config = {};
			$def.done(function() {
				$p.setSingleUnitProg(0)
					.setSingleUnitText('')
					.setTotalProg(0)
					.setTotalText('')
					.setTitle('');
			});
			return $p;
		},
		/**
		 * Must be called prior to any other method except ``.cleanUp()``.
		 *
		 * @param {Object} values
		 *  Object with pattern:
		 *    {
					// The detailed progress
					singleUnitProg: 0,
					singleUnitText: "Done",
					// The total progress
					totalProg: 50,
					totalText: "The next task.",
					// Title to be shown in the dialog.
					// If not supplied, options.app is used.
					title: "My Application"
				}
		 * @param {Object} [options]
		 *  Confer to config defaults.
		 * @chainable
		 */
		showProgress: function(values, options) {
			$p.config = $.extend({}, defaults, $p.config, options);
			$p.init().done($.proxy($p.__showProgress, $p, values));
			autoOpen = true;
			$p.dialog('open');
			onDialogStateChange();
			return $p;
		},
		/**
		 * Set progress values. To be called after
		 * options are merged and the required modules
		 * were loaded
		 *
		 * @param {Object} values
		 * @return {undefined}
		 * @protected
		 */
		__showProgress: function(values) {
			$p.setSingleUnitProg(values.singleUnitProg)
				.setSingleUnitText(values.singleUnitText)
				.setTotalProg(values.totalProg)
				.setTotalText(values.totalText)
				.setTitle(values.title);
		},
		/**
		 * Set the progress text 
		 * for the detailed process.
		 *
		 * @param {jQuery} $target
		 *  The target to which the text to set.
		 * @param {string|String|jQuery} text
		 *  progress text or jQuery instance containing
		 *  this information
		 * @chainable
		 * @protected
		 */
		__setText: function($target, text) {
			var __complete;
			if ('string' !== $.type(text) && !(text instanceof $)) return $p;
			
			__complete = function() {
				$target.css({
					'top': 0,
					'position': 'static'
				}).fadeTo(0, 1).show();
			};
			if (animationOff) {
				animationOff = false;
				$target.text(text);
				return __complete();
			}
			$target.stop(true, true).hide({
				effect: 'drop',
				direction: 'down',
				duration: 'fast',
				complete: function() {
					if (text instanceof $) {
						$target.empty().append(text);
					} else {
						$target.text(text);
					}
				}
			}).show({
				effect: 'drop',
				direction: 'up',
				complete: __complete
			});
			return $p;
		},
		/**
		 * Set the progress value 
		 * for the detailed process.
		 * 
		 * @param {number} v
		 *  A number between 0 and 100.
		 * @chainable
		 */
		setSingleUnitProg: function(v) {
			$def.done(function() {
				if (!inCache('singleUnitProg', v) && $.isNumeric(v)) {
					$p.$suProgBarInner.width(v + '%');
				}
			});
			return $p;
		},
		/**
		 * Set the progress text 
		 * for the detailed process.
		 *
		 * @param {string|String|jQuery} t
		 *  progress text or jQuery instance containing
		 *  this information
		 * @chainable
		 */
		setSingleUnitText: function(t) {
			$def.done(function() {
				if (!inCache('singleUnitText', t)) {
					$p.__setText($p.$suText, t);
				}
			});
			return $p;
		},
		/**
		 * Set the progress value 
		 * for the total process.
		 * 
		 * @param {number} v
		 *  A number between 0 and 100.
		 * @chainable
		 */
		setTotalProg: function(v) {
			$def.done(function() {
				if (!inCache('totalProg', v) && $.isNumeric(v)) {
					$p.$totalProgBarInner.width(v + '%');
				}
			});
			return $p;
		},
		/**
		 * Set the progress text 
		 * for the total process.
		 *
		 * @param {string|String|jQuery} t
		 *  progress text or jQuery instance containing
		 *  this information
		 * @chainable
		 */
		setTotalText: function(t) {
			$def.done(function() {
				if (!inCache('totalText', t)) {
					$p.__setText($p.$totalText, t);
				}
			});
			return $p;
		},
		/**
		 * Set the dialog title.
		 * 
		 * @chainable
		 */
		setTitle: function(t) {
			$def.done(function() {
				if ($.type(t) === 'string') $p.$app.text(t || $p.config.app);
			});
			return $p;
		},
		/**
		 * Hide the progress bar and the label
		 * for the total process.
		 * 
		 * @chainable
		 */
		hideTotal: function() {
			$def.done(function() {
				$p.$totalContainer.show();
			});
			return $p;
		},
		/**
		 * Hide the progress bar and the label
		 * for detailed processes.
		 * 
		 * @chainable
		 */
		hideSingleUnit: function() {
			$def.done(function() {
				$p.$suContainer.hide();
			});
			return $p;
		},
		/**
		 * Show a progress bar and a lable
		 * for the total process.
		 * 
		 * @chainable
		 */
		showTotal: function() {
			$def.done(function() {
				$p.$totalContainer.show();
			});
			return $p;
		},
		/**
		 * Show a progress bar and a label
		 * for detailed processes.
		 * 
		 * @chainable
		 */
		showSingleUnit: function() {
			$def.done(function() {
				$p.$suContainer.show();
			});
			return $p;
		},
		/**
		 * Hide the dialog.
		 * Note that only ``.show()`` and ``.showProgress()``
		 * will open the dialog. Calling ``.setTotalProg()``,
		 * for example, will have no visible effect if the
		 * dialog is currently hidden.
		 *
		 * @param {number} [ms=800]
		 * @chainable
		 */
		hide: function(ms) {
			var effect = {
				effect: 'fade',
				duration: $.isNumeric(ms) ? ms : 800
			};
			$p.dialog('option', 'hide', effect);
			$p.dialog('close');
			autoOpen = false;
			return $p;
		},
		/**
		 * Open the dialog.
		 * Nothing will happen if it is already open.
		 * 
		 * @chainable
		 */
		show: function() {
			$p.dialog('open');
			autoOpen = true;
			return $p;
		},
		/**
		 * Eco-Friendly: Only consume resources when required.
		 *
		 * @param {Function} done
		 *  Invoked when the dialog is ready to use.
		 * @protected
		 */
		__init: function() {
			$p.$app = $('<div>').attr({
				id: 'prog-app'
			}).text($p.config.app).appendTo($p);

			$p.$suContainer = $('<div>').attr({
				id: 'prog-su-container',
				'class': 'prog-container'
			}).appendTo($p);
			$p.$suText = $('<div>').attr({
				id: 'prog-su-text',
				'class': 'prog-text'
			}).appendTo($p.$suContainer);
			$p.$suProgBar = $('<div>').attr({
				id: 'prog-su-prog',
				'class': 'prog-bar'
			}).appendTo($p.$suContainer);
			$p.$suProgBarInner = $('<div>').attr({
				id: 'prog-su-prog-inner',
				'class': 'prog-bar-inner'
			}).appendTo($p.$suProgBar);

			$p.$totalContainer = $('<div>').attr({
				id: 'prog-t-container',
				'class': 'prog-container'
			}).appendTo($p);
			$p.$totalText = $('<div>').attr({
				id: 'prog-t-text',
				'class': 'prog-text'
			}).appendTo($p.$totalContainer);
			$p.$totalProgBar = $('<div>').attr({
				id: 'prog-t-prog',
				'class': 'prog-bar'
			}).appendTo($p.$totalContainer);
			$p.$totalProgBarInner = $('<div>').attr({
				id: 'prog-t-prog-inner',
				'class': 'prog-bar-inner'
			}).appendTo($p.$totalProgBar);

			var $win = $(window),
				w = Math.min($win.width(), 600);
			$p.$widget = $p.dialog({
				modal: true,
				resizable: false,
				draggable: false,
				width: w,
				autoOpen: autoOpen,
				show: {
					effect: 'scale',
					duration: 500
				},
				close: function() {
					onDialogStateChange();
				},
				open: function() {
					onDialogStateChange();
					setTimeout(function() {
						var top = ($win.height() - Math.min($win.height(), $('.ui-dialog.ui-widget').height())) / 2;
						$p.closest('.ui-dialog').css({
							position: 'fixed',
							top: Math.round(top) + 'px'
						});
					}, 800);
				}
			}).dialog('widget');

			$p.$widget.find('.ui-dialog-titlebar').remove();
			$def.resolve();
			isReady = true;
		},
		/**
		 * Load the required modules and call ``.__init()``
		 * @param {Function} done
		 *  Invoked when the dialog is ready to use.
		 * @protected
		 * @return {jQuery.Deferred}
		 *  jQuery.Deferred that is resolved after the dialog
		 *  has been created.
		 */
		init: function() {
			if (!isInit) {
				isInit = true;
				animationOff = true;
				mw.loader.using([
						'jquery.ui',
				], $.proxy($p.__init, $p));
			}
			return $def;
		}
	});
})(jQuery, mediaWiki);