2 * Enable inline confirmation for clickable elements.
4 * @module jquery.confirmable
5 * @author Bartosz Dziewoński
9 const identity = ( data ) => data;
12 * Enable inline confirmation for given clickable element (like `<a />` or `<button />`).
14 * An additional inline confirmation step being shown before the default action is carried out on
17 * Calling `.confirmable( { handler: function () { … } } )` will fire the handler only after the
20 * The element will have the `jquery-confirmable-element` class added to it when it's clicked for
21 * the first time, which has `white-space: nowrap;` and `display: inline-block;` defined in CSS.
22 * If the computed values for the element are different when you make it confirmable, you might
23 * encounter unexpected behavior.
25 * To use this {@link jQuery} plugin, load the `jquery.confirmable` module with {@link mw.loader}.
28 * mw.loader.using( 'jquery.confirmable' ).then( () => {
29 * $( 'button' ).confirmable();
31 * @memberof module:jquery.confirmable
33 * @param {Object} [options]
34 * @param {string} [options.events='click'] Events to hook to.
35 * @param {Function} [options.wrapperCallback] Callback to fire when preparing confirmable
36 * interface. Receives the interface jQuery object as the only parameter.
37 * @param {Function} [options.buttonCallback] Callback to fire when preparing confirmable buttons.
38 * It is fired separately for the 'Yes' and 'No' button. Receives the button jQuery object as
39 * the first parameter and 'yes' or 'no' as the second.
40 * @param {Function} [options.handler] Callback to fire when the action is confirmed (user clicks
42 * @param {string} [options.delegate] Optional selector used for jQuery event delegation
43 * @param {string} [options.i18n] Text to use for interface elements.
44 * @param {string} [options.i18n.space=' '] Word separator to place between the three text messages.
45 * @param {string} [options.i18n.confirm='Are you sure?'] Text to use for the confirmation question.
46 * @param {string} [options.i18n.yes='Yes'] Text to use for the 'Yes' button.
47 * @param {string} [options.i18n.no='No'] Text to use for the 'No' button.
48 * @param {string} [options.i18n.yesTitle] Optional title text to use for the 'Yes' button.
49 * @param {string} [options.i18n.noTitle] Optional title text to use for the 'No' button.
52 $.fn.confirmable = function ( options ) {
53 options = $.extend( true, {}, $.fn.confirmable.defaultOptions, options || {} );
55 if ( options.delegate === null ) {
56 return this.on( options.events, ( e ) => {
57 $.fn.confirmable.handler( e, options );
61 return this.on( options.events, options.delegate, ( e ) => {
62 $.fn.confirmable.handler( e, options );
66 $.fn.confirmable.handler = function ( event, options ) {
67 const $element = $( event.target );
69 if ( $element.data( 'jquery-confirmable-button' ) ) {
70 // We're running on a clone of this element that represents the 'Yes' or 'No' button.
71 // (This should never happen for the 'No' case unless calling code does bad things.)
75 // Only prevent native event handling. Stopping other JavaScript event handlers
76 // is impossible because they might have already run (we have no control over the order).
77 event.preventDefault();
79 const rtl = $element.css( 'direction' ) === 'rtl';
80 let positionOffscreen, positionRestore, sideMargin, elementSideMargin;
82 positionOffscreen = { position: 'absolute', right: '-9999px' };
83 positionRestore = { position: '', right: '' };
84 sideMargin = 'marginRight';
85 elementSideMargin = parseInt( $element.css( 'margin-right' ) );
87 positionOffscreen = { position: 'absolute', left: '-9999px' };
88 positionRestore = { position: '', left: '' };
89 sideMargin = 'marginLeft';
90 elementSideMargin = parseInt( $element.css( 'margin-left' ) );
93 $element.addClass( 'hidden' );
94 let $wrapper, $interface, interfaceWidth, elementWidth, elementPadding;
95 // eslint-disable-next-line no-jquery/no-class-state
96 if ( $element.hasClass( 'jquery-confirmable-element' ) ) {
97 $wrapper = $element.closest( '.jquery-confirmable-wrapper' );
98 $interface = $wrapper.find( '.jquery-confirmable-interface' );
100 interfaceWidth = $interface.data( 'jquery-confirmable-width' );
101 elementWidth = $element.data( 'jquery-confirmable-width' );
102 elementPadding = $element.data( 'jquery-confirmable-padding' );
103 // Restore visibility to interface text if it is opened again after being cancelled.
104 const $existingText = $interface.find( '.jquery-confirmable-text' );
105 $existingText.removeClass( 'hidden' );
107 const $elementClone = $element.clone( true );
108 $element.addClass( 'jquery-confirmable-element' );
110 elementWidth = $element.width();
111 elementPadding = parseInt( $element.css( 'padding-left' ) ) + parseInt( $element.css( 'padding-right' ) );
112 $element.data( 'jquery-confirmable-width', elementWidth );
113 $element.data( 'jquery-confirmable-padding', elementPadding );
115 $wrapper = $( '<span>' )
116 .addClass( 'jquery-confirmable-wrapper' );
117 $element.wrap( $wrapper );
119 // Build the mini-dialog
120 const $text = $( '<span>' )
121 .addClass( 'jquery-confirmable-text' )
122 .text( options.i18n.confirm );
124 // Clone original element along with event handlers to easily replicate its behavior.
125 // We could fiddle with .trigger() etc., but that is troublesome especially since
126 // Safari doesn't implement .click() on <a> links and jQuery follows suit.
127 let $buttonYes = $elementClone.clone( true )
128 .addClass( 'jquery-confirmable-button jquery-confirmable-button-yes' )
129 .removeClass( 'hidden' )
130 .data( 'jquery-confirmable-button', true )
131 .text( options.i18n.yes );
132 if ( options.handler ) {
133 $buttonYes.on( options.events, options.handler );
135 if ( options.i18n.yesTitle ) {
136 $buttonYes.attr( 'title', options.i18n.yesTitle );
138 $buttonYes = options.buttonCallback( $buttonYes, 'yes' );
140 // Clone it without any events and prevent default action to represent the 'No' button.
141 let $buttonNo = $elementClone.clone( false )
142 .addClass( 'jquery-confirmable-button jquery-confirmable-button-no' )
143 .removeClass( 'hidden' )
144 .data( 'jquery-confirmable-button', true )
145 .text( options.i18n.no )
146 .on( options.events, ( e ) => {
148 .css( sideMargin, elementSideMargin )
149 .removeClass( 'hidden' );
150 $interface.css( 'width', 0 );
153 if ( options.i18n.noTitle ) {
154 $buttonNo.attr( 'title', options.i18n.noTitle );
156 $buttonNo.removeAttr( 'title' );
158 $buttonNo = options.buttonCallback( $buttonNo, 'no' );
160 // Prevent memory leaks
161 $elementClone.remove();
163 $interface = $( '<span>' )
164 .addClass( 'jquery-confirmable-interface' )
165 .append( $text, options.i18n.space, $buttonYes, options.i18n.space, $buttonNo );
166 $interface = options.wrapperCallback( $interface );
168 // Render offscreen to measure real width
169 $interface.css( positionOffscreen );
170 // Insert it in the correct place while we're at it
171 $element.after( $interface );
172 interfaceWidth = $interface.width();
174 .data( 'jquery-confirmable-width', interfaceWidth )
175 .css( positionRestore )
176 // Hide to animate the transition later
180 // Hide element, show interface. This triggers both transitions.
181 // In a timeout to trigger the 'width' transition.
183 $element.css( sideMargin, -elementWidth - elementPadding );
185 .css( 'width', interfaceWidth )
186 .css( sideMargin, elementSideMargin );
191 * Default options. Overridable primarily for internationalisation handling.
193 * @property {Object} defaultOptions
195 $.fn.confirmable.defaultOptions = {
197 wrapperCallback: identity,
198 buttonCallback: identity,
203 confirm: 'Are you sure?',