Merge "Improve sorting on SpecialWanted*-Pages"
[mediawiki.git] / resources / src / jquery / jquery.confirmable.js
blob7931c8147fac3bbdf031d95e97b9ff20977759a7
1 /**
2  * jQuery confirmable plugin
3  *
4  * Released under the MIT License.
5  *
6  * @author Bartosz Dziewoński
7  *
8  * @class jQuery.plugin.confirmable
9  */
10 ( function ( $ ) {
11         var identity = function ( data ) {
12                 return data;
13         };
15         // eslint-disable-next-line valid-jsdoc
16         /**
17          * Enable inline confirmation for given clickable element (like `<a />` or `<button />`).
18          *
19          * An additional inline confirmation step being shown before the default action is carried out on
20          * click.
21          *
22          * Calling `.confirmable( { handler: function () { … } } )` will fire the handler only after the
23          * confirmation step.
24          *
25          * The element will have the `jquery-confirmable-element` class added to it when it's clicked for
26          * the first time, which has `white-space: nowrap;` and `display: inline-block;` defined in CSS.
27          * If the computed values for the element are different when you make it confirmable, you might
28          * encounter unexpected behavior.
29          *
30          * @param {Object} [options]
31          * @param {string} [options.events='click'] Events to hook to.
32          * @param {Function} [options.wrapperCallback] Callback to fire when preparing confirmable
33          *     interface. Receives the interface jQuery object as the only parameter.
34          * @param {Function} [options.buttonCallback] Callback to fire when preparing confirmable buttons.
35          *     It is fired separately for the 'Yes' and 'No' button. Receives the button jQuery object as
36          *     the first parameter and 'yes' or 'no' as the second.
37          * @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
38          *     the 'Yes' button).
39          * @param {string} [options.i18n] Text to use for interface elements.
40          * @param {string} [options.i18n.space] Word separator to place between the three text messages.
41          * @param {string} [options.i18n.confirm] Text to use for the confirmation question.
42          * @param {string} [options.i18n.yes] Text to use for the 'Yes' button.
43          * @param {string} [options.i18n.no] Text to use for the 'No' button.
44          * @param {string} [options.i18n.yesTitle] Title text to use for the 'Yes' button.
45          * @param {string} [options.i18n.noTitle] Title text to use for the 'No' button.
46          *
47          * @chainable
48          */
49         $.fn.confirmable = function ( options ) {
50                 options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
52                 return this.on( options.events, function ( e ) {
53                         var $element, $text, $buttonYes, $buttonNo, $wrapper, $interface, $elementClone,
54                                 interfaceWidth, elementWidth, rtl, positionOffscreen, positionRestore, sideMargin;
56                         $element = $( this );
58                         if ( $element.data( 'jquery-confirmable-button' ) ) {
59                                 // We're running on a clone of this element that represents the 'Yes' or 'No' button.
60                                 // (This should never happen for the 'No' case unless calling code does bad things.)
61                                 return;
62                         }
64                         // Only prevent native event handling. Stopping other JavaScript event handlers
65                         // is impossible because they might have already run (we have no control over the order).
66                         e.preventDefault();
68                         rtl = $element.css( 'direction' ) === 'rtl';
69                         if ( rtl ) {
70                                 positionOffscreen = { position: 'absolute', right: '-9999px' };
71                                 positionRestore = { position: '', right: '' };
72                                 sideMargin = 'marginRight';
73                         } else {
74                                 positionOffscreen = { position: 'absolute', left: '-9999px' };
75                                 positionRestore = { position: '', left: '' };
76                                 sideMargin = 'marginLeft';
77                         }
79                         if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
80                                 $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
81                                 $interface = $wrapper.find( '.jquery-confirmable-interface' );
82                                 $text = $interface.find( '.jquery-confirmable-text' );
83                                 $buttonYes = $interface.find( '.jquery-confirmable-button-yes' );
84                                 $buttonNo = $interface.find( '.jquery-confirmable-button-no' );
86                                 interfaceWidth = $interface.data( 'jquery-confirmable-width' );
87                                 elementWidth = $element.data( 'jquery-confirmable-width' );
88                         } else {
89                                 $elementClone = $element.clone( true );
90                                 $element.addClass( 'jquery-confirmable-element' );
92                                 elementWidth = $element.width();
93                                 $element.data( 'jquery-confirmable-width', elementWidth );
95                                 $wrapper = $( '<span>' )
96                                         .addClass( 'jquery-confirmable-wrapper' );
97                                 $element.wrap( $wrapper );
99                                 // Build the mini-dialog
100                                 $text = $( '<span>' )
101                                         .addClass( 'jquery-confirmable-text' )
102                                         .text( options.i18n.confirm );
104                                 // Clone original element along with event handlers to easily replicate its behavior.
105                                 // We could fiddle with .trigger() etc., but that is troublesome especially since
106                                 // Safari doesn't implement .click() on <a> links and jQuery follows suit.
107                                 $buttonYes = $elementClone.clone( true )
108                                         .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
109                                         .data( 'jquery-confirmable-button', true )
110                                         .text( options.i18n.yes );
111                                 if ( options.handler ) {
112                                         $buttonYes.on( options.events, options.handler );
113                                 }
114                                 if ( options.i18n.yesTitle ) {
115                                         $buttonYes.attr( 'title', options.i18n.yesTitle );
116                                 }
117                                 $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
119                                 // Clone it without any events and prevent default action to represent the 'No' button.
120                                 $buttonNo = $elementClone.clone( false )
121                                         .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
122                                         .data( 'jquery-confirmable-button', true )
123                                         .text( options.i18n.no )
124                                         .on( options.events, function ( e ) {
125                                                 $element.css( sideMargin, 0 );
126                                                 $interface.css( 'width', 0 );
127                                                 e.preventDefault();
128                                         } );
129                                 if ( options.i18n.noTitle ) {
130                                         $buttonNo.attr( 'title', options.i18n.noTitle );
131                                 } else {
132                                         $buttonNo.removeAttr( 'title' );
133                                 }
134                                 $buttonNo = options.buttonCallback( $buttonNo, 'no' );
136                                 // Prevent memory leaks
137                                 $elementClone.remove();
139                                 $interface = $( '<span>' )
140                                         .addClass( 'jquery-confirmable-interface' )
141                                         .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
142                                 $interface = options.wrapperCallback( $interface );
144                                 // Render offscreen to measure real width
145                                 $interface.css( positionOffscreen );
146                                 // Insert it in the correct place while we're at it
147                                 $element.after( $interface );
148                                 interfaceWidth = $interface.width();
149                                 $interface.data( 'jquery-confirmable-width', interfaceWidth );
150                                 $interface.css( positionRestore );
152                                 // Hide to animate the transition later
153                                 $interface.css( 'width', 0 );
154                         }
156                         // Hide element, show interface. This triggers both transitions.
157                         // In a timeout to trigger the 'width' transition.
158                         setTimeout( function () {
159                                 $element.css( sideMargin, -elementWidth );
160                                 $interface.css( 'width', interfaceWidth );
161                         }, 1 );
162                 } );
163         };
165         /**
166          * Default options. Overridable primarily for internationalisation handling.
167          * @property {Object} defaultOptions
168          */
169         $.fn.confirmable.defaultOptions = {
170                 events: 'click',
171                 wrapperCallback: identity,
172                 buttonCallback: identity,
173                 handler: null,
174                 i18n: {
175                         space: ' ',
176                         confirm: 'Are you sure?',
177                         yes: 'Yes',
178                         no: 'No',
179                         yesTitle: undefined,
180                         noTitle: undefined
181                 }
182         };
183 }( jQuery ) );