Current File : /home/jvzmxxx/wiki1/extensions/Flow/modules/engine/misc/mw-ui.modal.js
/*!
 * mw-ui-modal
 * Implements mw.Modal functionality.
 */

( function ( mw, $ ) {
	// Make it easier to remove this later on, should it be implemented in Core
	if ( mw.Modal ) {
		return;
	}

	/**
	 * Accepts an element or HTML string as contents. If none given,
	 * modal will start in hidden state.
	 * Settings keys:
	 * - open (same arguments as open method)
	 * - title string
	 * - disableCloseOnOutsideClick boolean (if true, ESC and background clicks do not close it)
	 *
	 * Simple modal:
	 *
	 *     @example
	 *     modal1 = mw.Modal();
	 *
	 * Modal with contents and title:
	 *
	 *     @example
	 *     modal2 = mw.Modal( { open: 'Contents!!', title: 'Title!!' } );
	 *
	 * Named modal:
	 *
	 *     @example
	 *     modal3 = mw.Modal( 'special_modal' );
	 *
	 * @todo Implement multi-step
	 * @todo Implement data-mwui handlers
	 * @todo Implement OOJS & events
	 * @class
	 * @constructor
	 * @param {string} [name] Name of modal (may be omitted)
	 * @param {Object} [settings]
	 */
	function MwUiModal( name, settings ) {
		// allow calling this method with or without "new" keyword
		if ( this.constructor !== MwUiModal ) {
			return new MwUiModal( name, settings );
		}

		// Defaults and ordering
		if ( !settings && typeof name === 'object' ) {
			settings = name;
			name = null;
		}
		settings = settings || {};

		// Set name
		this.name = name;

		// Set title
		this.setTitle( settings.title );

		// Set disableCloseOnOutsideClick
		this.disableCloseOnOutsideClick = !!settings.disableCloseOnOutsideClick;

		// Auto-open
		if ( settings.open ) {
			this.open( settings.open );
		}

		return this;
	}

	/** Stores template
	 * @todo use data-mwui attributes instead of data-flow **/
	MwUiModal.prototype.template =
		'<div class="flow-ui-modal">' +
		'<div class="flow-ui-modal-layout">' +
		'<div class="flow-ui-modal-heading">' +
		'<a href="#" class="mw-ui-anchor mw-ui-quiet mw-ui-destructive flow-ui-modal-heading-prev" data-flow-interactive-handler="modalPrevOrClose"><span class="mw-ui-icon mw-ui-icon-before mw-ui-icon-close"></span></a>' +
		'<a href="#" class="mw-ui-anchor mw-ui-quiet mw-ui-progressive flow-ui-modal-heading-next" data-flow-interactive-handler="modalNextOrSubmit"><span class="mw-ui-icon mw-ui-icon-before mw-ui-icon-check"></span></a>' +
			// title
		'</div>' +

		'<div class="flow-ui-modal-content">' +
			// content
		'</div>' +
		'</div>' +
		'</div>';

	/** Stores modal wrapper selector **/
	MwUiModal.prototype.wrapperSelector = '.flow-ui-modal';
	/** Stores content wrapper selector **/
	MwUiModal.prototype.contentSelector = '.flow-ui-modal-content';
	/** Stores heading wrapper selector, which contains prev/next links **/
	MwUiModal.prototype.headingSelector = '.flow-ui-modal-heading';
	/** Stores prev link selector **/
	MwUiModal.prototype.prevSelector = '.flow-ui-modal-heading-prev';
	/** Stores next link selector **/
	MwUiModal.prototype.nextSelector = '.flow-ui-modal-heading-next';

	// Primary functions

	/**
	 * Closes and destroys the given instance of mw.Modal.
	 *
	 * @return {boolean} false on failure, true on success
	 */
	MwUiModal.prototype.close = function () {
		// Remove references
		this._contents = this._title = null;

		if ( this.$node ) {
			// Remove whole thing from page
			this.getNode().remove();

			return true;
		}

		return false;
	};

	/**
	 * You can visually render the modal using this method. Opens up by displaying it on the page.
	 *
	 * - Multi-step modals with an Array. You can pass [ Element, Element ] to have two steps.
	 * - Multi-step modals with an Object to have named step keys. Pass this for three steps:
	 *   { steps: [ 'first', 'second', 'foobar' ], first: Element, second: Element, foobar: Element }
	 *
	 * @todo Currently only supports string|jQuery|HTMLElement. Implement multi-step modals.
	 *
	 * @param {Object|HTMLElement|HTMLElement[]|jQuery|string} [contents]
	 * @return {MwUiModal}
	 */
	MwUiModal.prototype.open = function ( contents ) {
		var $node = this.getNode(),
			$contentNode = this.getContentNode(),
			$fields;

		// Only update content if it's new
		if ( contents && contents !== this._contents ) {
			this._contents = contents;

			$contentNode
				// Remove children (this way we can unbind events)
				.children()
				.remove()
				.end()
				// Remove any plain text left over
				.empty()
				// Add the new content
				.append( contents );
		}

		// Drop it into the page
		$node.appendTo( 'body' );

		// Hide the tick box @todo implement multi-step and event handling / form binding
		$node.find( this.nextSelector ).hide();

		// If something in here did not auto-focus, let's focus something
		$fields = $node.find( 'textarea, input, select' ).filter( ':visible' );
		if ( !$fields.filter( ':focus' ).length ) {
			// Try to focus on an autofocus field
			$fields = $fields.filter( '[autofocus]' );
			if ( $fields.length ) {
				$fields.trigger( 'focus' );
			} else {
				// Try to focus on ANY input
				$fields = $fields.end().filter( ':first' );
				if ( $fields.length ) {
					$fields.trigger( 'focus' );
				} else {
					// Give focus to the wrapper itself
					$node.focus();
				}
			}
		}

		return this;
	};

	/**
	 * Changes the title of the modal.
	 *
	 * @param {string|null} title
	 * @return MwUiModal
	 */
	MwUiModal.prototype.setTitle = function ( title ) {
		var $heading = this.getNode().find( this.headingSelector ),
			$children;

		title = title || '';

		// Only update title if it's new
		if ( title !== this._title ) {
			this._title = title;

			// Remove any element children temporarily, so we can set the title here
			$children = $heading.children().detach();

			$heading
				// Set the new title
				.text( title )
				// Add the child nodes back
				.prepend( $children );
		}

		// Show the heading if there's a title; hide otherwise
		$heading[ title ? 'show' : 'hide' ]();

		return this;
	};

	/**
	 * @todo Implement data-mwui handlers, currently using data-flow
	 */
	MwUiModal.prototype.setInteractiveHandler = function () {
		return false;
	};

	/**
	 * Returns modal name.
	 */
	MwUiModal.prototype.getName = function () {
		return this.name;
	};

	// Nodes

	/**
	 * Returns the modal's wrapper Element, which contains the header node and content node.
	 * @return {jQuery}
	 */
	MwUiModal.prototype.getNode = function () {
		var self = this,
			$node = this.$node;

		// Create our template instance
		if ( !$node ) {
			$node = this.$node = $( this.template );

			// Store a self-reference
			$node.data( 'MwUiModal', this );

			// Bind close handlers
			$node.on( 'click', function ( event ) {
				// If we are clicking on the modal itself, it's the outside area, so close it;
				// make sure we aren't clicking INSIDE the modal content!
				if ( !self.disableCloseOnOutsideClick && this === $node[ 0 ] && event.target === $node[ 0 ] ) {
					self.close();
				}
			} );
		}

		return $node;
	};

	/**
	 * Returns the wrapping Element on which you can bind bubbling events for your content.
	 * @return {jQuery}
	 */
	MwUiModal.prototype.getContentNode = function () {
		return this.getNode().find( this.contentSelector );
	};

	// Step creation

	/**
	 * Adds one or more steps, using the same arguments as modal.open.
	 * May overwrite steps if any exist with the same key in Object mode.
	 *
	 * @todo Implement multi-step.
	 *
	 * @param {Object|HTMLElement|HTMLElement[]|jQuery|string} contents
	 * @return {MwUiModal}
	 */
	MwUiModal.prototype.addSteps = function ( contents ) {
		return false;
	};

	/**
	 * Changes a given step. If String to does not exist in the list of steps, throws an exception;
	 * int to always succeeds. If the given step is the currently-active one, rerenders the modal contents.
	 * Theoretically, you could use setStep to keep changing step 1 to create a pseudo-multi-step modal.
	 *
	 * @todo Implement multi-step.
	 *
	 * @param {number|string} to
	 * @param {HTMLElement|jQuery|string} contents
	 * @return {MwUiModal}
	 */
	MwUiModal.prototype.setStep = function ( to, contents ) {
		return false;
	};

	/**
	 * Returns an Object with steps, and their contents.
	 *
	 * @todo Implement multi-step.
	 *
	 * @return {Object}
	 */
	MwUiModal.prototype.getSteps = function ( to, contents ) {
		return {};
	};

	// Step interaction

	/**
	 * For a multi-step modal, goes to the previous step, otherwise, closes the modal.
	 *
	 * @return {MwUiModal|boolean} false if none, MwUiModal on prev, true on close
	 */
	MwUiModal.prototype.prevOrClose = function () {
		if ( this.prev() === false ) {
			return this.close();
		}
	};

	/**
	 * For a multi-step modal, goes to the next step (if any), otherwise, submits the form.
	 *
 * @return {MwUiModal|boolean} false if no next step and no button to click, MwUiModal on success
	 */
	MwUiModal.prototype.nextOrSubmit = function () {
		var $button;

		if ( this.next() === false && this.$node ) {
			// Find an anchor or button with role=primary
			$button = this.$node.find( this.contentSelector ).find( 'a, input, button' ).filter( ':visible' ).filter( '[type=submit], [data-role=submit]' );

			if ( !$button.length ) {
				return false;
			}

			$button.trigger( 'click' );
		}
	};

	/**
	 * For a multi-step modal, goes to the previous step, if any are left.
	 *
	 * @todo Implement multi-step.
	 *
	 * @return {MwUiModal|boolean} false if invalid step, MwUiModal on success
	 */
	MwUiModal.prototype.prev = function () {
		return false;
	};

	/**
	 * For a multi-step modal, goes to the next step, if any are left.
	 *
	 * @todo Implement multi-step.
	 *
	 * @return {MwUiModal|boolean} false if invalid step, MwUiModal on success
	 */
	MwUiModal.prototype.next = function () {
		return false;
	};

	/**
	 * For a multi-step modal, goes to a specific step by number or name.
	 *
	 * @todo Implement multi-step.
	 *
	 * @param {number|string} to
	 * @return {MwUiModal|boolean} false if invalid step, MwUiModal on success
	 */
	MwUiModal.prototype.go = function ( to ) {
		return false;
	};

	/**
	 * MW UI Modal access through JS API.
	 *
	 *    mw.Modal( "<p>lorem</p>" );
	 */
	mw.Modal = MwUiModal;

	/**
	 * Returns an instance of mw.Modal if one is currently being displayed on the page.
	 * If node is given, tries to find which modal (if any) that node is within.
	 * Returns false if none found.
	 *
	 * @param {HTMLElement|jQuery} [node]
	 * @return {boolean|MwUiModal}
	 */
	mw.Modal.getModal = function ( node ) {
		if ( node ) {
			// Node was given; try to find a parent modal
			return $( node ).closest( MwUiModal.prototype.wrapperSelector ).data( 'MwUiModal' ) || false;
		}

		// No node given; return the last-opened modal on the page
		return $( 'body' ).children( MwUiModal.prototype.wrapperSelector ).filter( ':last' ).data( 'MwUiModal' ) || false;
	};

	// Transforms: automatically map these functions to call their mw.Modal methods globally, on any active instance
	$.each( [ 'close', 'getName', 'prev', 'next', 'prevOrClose', 'nextOrSubmit', 'go' ], function ( i, fn ) {
		mw.Modal[ fn ] = function () {
			var args = Array.prototype.splice.call( arguments, 0, arguments.length - 1 ),
				node = arguments[ arguments.length - 1 ],
				modal;

			// Find the node, if any was given
			if ( !node || ( typeof node.is === 'function' && !node.is( '*' ) ) || node.nodeType !== 1 ) {
				// The last argument to this function was not a node, assume none was intended to be given
				node = null;
				args = arguments;
			}

			// Try to find that modal
			modal = mw.Modal.getModal( node );

			// Call the intended function locally
			if ( modal ) {
				modal[ fn ].apply( modal, args );
			}
		};
	} );
}( mw, jQuery ) );